Spring Boot 集成 Dufs 通过 WebDAV 实现文件管理

发布于:2025-07-04 ⋅ 阅读:(18) ⋅ 点赞:(0)

Spring Boot 集成 Dufs 通过 WebDAV 实现文件管理

在这里插入图片描述

引言

在现代应用开发中,文件存储和管理是一个常见需求。Dufs 是一个轻量级的文件服务器,支持 WebDAV 协议,可以方便地集成到 Spring Boot 应用中。本文将详细介绍如何使用 WebDAV 协议在 Spring Boot 中集成 Dufs 文件服务器。

1. 准备工作

1.1 添加项目依赖

pom.xml 中添加必要依赖:

<dependencies>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- Sardine WebDAV 客户端 -->
	<dependency>
	    <groupId>com.github.lookfirst</groupId>
	    <artifactId>sardine</artifactId>
	    <version>5.10</version>
	</dependency>
</dependencies>

2. 核心实现

2.1 配置类

@Configuration
@ConfigurationProperties(prefix = "dufs.webdav")
@Data
public class DufsWebDavConfig {
    private String url = "http://localhost:5000";
    private String username = "admin";
    private String password = "password";
    private String basePath = "/";
    
    @Bean
    public Sardine sardine() {
        Sardine sardine = SardineFactory.begin();
        sardine.setCredentials(username, password);
        return sardine;
    }
}

2.2 服务层实现

@Service
@RequiredArgsConstructor
@Slf4j
public class DufsWebDavService {
    private final Sardine sardine;
    private final DufsWebDavConfig config;

    /**
     * 自定义 WebDAV 异常
     */
    public static class DufsWebDavException extends RuntimeException {
        public DufsWebDavException(String message) {
            super(message);
        }

        public DufsWebDavException(String message, Throwable cause) {
            super(message, cause);
        }
    }

    /**
     * 构建完整的 WebDAV 路径
     */
    private String buildFullPath(String path) {
        return config.getUrl() + config.getBasePath() + (path.startsWith("/") ? path : "/" + path);
    }

    /**
     * 上传文件
     */
    public String uploadFile(String path, InputStream inputStream) throws IOException {
        String fullPath = buildFullPath(path);
        sardine.put(fullPath, inputStream);
        return fullPath;
    }

    /**
     * 下载文件实现(方案1)
     * ByteArrayResource(适合小文件)
     */
    public Resource downloadFileByte(String path) {
        String fullPath = buildFullPath(path);
        try {
            InputStream is = sardine.get(fullPath);
            byte[] bytes = IOUtils.toByteArray(is); // 使用 Apache Commons IO
            return new ByteArrayResource(bytes) {
                @Override
                public String getFilename() {
                    return path.substring(path.lastIndexOf('/') + 1);
                }
            };
        } catch (IOException e) {
            throw new DufsWebDavException("Failed to download file: " + path, e);
        }
    }

    /**
     * 下载文件实现(方案2)
     * StreamingResponseBody(适合大文件)
     */
    public StreamingResponseBody downloadFileStreaming(String path) {
        String fullPath = buildFullPath(path);
        return outputStream -> {
            try (InputStream is = sardine.get(fullPath)) {
                byte[] buffer = new byte[8192];
                int bytesRead;
                while ((bytesRead = is.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, bytesRead);
                }
            }
        };
    }

    /**
     * 下载文件实现(方案3)
     * 自定义可重复读取的 Resource(推荐)
     */
    public Resource downloadFile(String path) {
        String fullPath = buildFullPath(path);
        try {
            // 测试文件是否存在
            if (!sardine.exists(fullPath)) {
                throw new DufsWebDavException("File not found: " + path);
            }

            return new AbstractResource() {
                @Override
                public String getDescription() {
                    return "WebDAV resource [" + fullPath + "]";
                }

                @Override
                public InputStream getInputStream() throws IOException {
                    // 每次调用都获取新的流
                    return sardine.get(fullPath);
                }

                @Override
                public String getFilename() {
                    return path.substring(path.lastIndexOf('/') + 1);
                }
            };
        } catch (IOException e) {
            throw new DufsWebDavException("Failed to download file: " + path, e);
        }
    }


    /**
     * 列出目录下的文件信息
     */
    public List<WebDavFileInfo> listDirectory(String path) {
        String fullPath = buildFullPath(path);
        try {
            List<DavResource> resources = sardine.list(fullPath);
            return resources.stream().filter(res -> !res.getHref().toString().equals(fullPath + "/")).map(res -> new WebDavFileInfo(res.getHref().getPath(), res.isDirectory(), res.getContentLength(), res.getModified())).collect(Collectors.toList());
        } catch (IOException e) {
            throw new DufsWebDavException("Failed to list directory: " + path, e);
        }
    }

    /**
     * 创建目录
     */
    public void createDirectory(String path) {
        String fullPath = buildFullPath(path);
        try {
            sardine.createDirectory(fullPath);
        } catch (IOException e) {
            throw new DufsWebDavException("Failed to create directory: " + path, e);
        }
    }


    /**
     * 删除文件/目录
     */
    public void delete(String path) {
        String fullPath = buildFullPath(path);
        try {
            sardine.delete(fullPath);
        } catch (IOException e) {
            throw new DufsWebDavException("Failed to delete: " + path, e);
        }
    }

    /**
     * 检查文件/目录是否存在
     */
    public boolean exists(String path) {
        String fullPath = buildFullPath(path);
        try {
            sardine.exists(fullPath);
            return true;
        } catch (IOException e) {
            return false;
        }
    }


    /**
     * 锁定文件
     */
    public String lockFile(String path) {
        String fullPath = buildFullPath(path);
        try {
            return sardine.lock(fullPath);
        } catch (IOException e) {
            throw new DufsWebDavException("Failed to lock file: " + path, e);
        }
    }

    /**
     * 解锁文件
     */
    public void unlockFile(String path, String lockToken) {
        String fullPath = buildFullPath(path);
        try {
            sardine.unlock(fullPath, lockToken);
        } catch (IOException e) {
            throw new DufsWebDavException("Failed to unlock file: " + path, e);
        }
    }

    /**
     * 文件信息DTO
     */
    @Data
    @AllArgsConstructor
    public static class WebDavFileInfo implements java.io.Serializable {
        private String name;
        private boolean directory;
        private Long size;
        private Date lastModified;
    }
}

2.3 控制器层

@RestController
@RequestMapping("/api/webdav")
@RequiredArgsConstructor
public class WebDavController {
    private final DufsWebDavService webDavService;

    @PostMapping("/upload")
    public ResponseEntity<?> uploadFile(
            @RequestParam("file") MultipartFile file,
            @RequestParam(value = "path", defaultValue = "") String path) {
        try {
            String filePath = path.isEmpty() ? file.getOriginalFilename()
                    : path + "/" + file.getOriginalFilename();
            String uploadPath = webDavService.uploadFile(filePath, file.getInputStream());
            return ResponseEntity.ok().body(uploadPath);
        } catch (IOException e) {
            throw new DufsWebDavService.DufsWebDavException("File upload failed", e);
        }
    }

    @GetMapping("/download")
    public ResponseEntity<Resource> downloadFile(@RequestParam String path) {
        Resource resource = webDavService.downloadFileByte(path);
        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION,
                        "attachment; filename=\"" + resource.getFilename() + "\"")
                .body(resource);
    }

    @GetMapping("/downloadFileStreaming")
    public ResponseEntity<StreamingResponseBody> downloadFileStreaming(@RequestParam String path) {
        StreamingResponseBody responseBody = webDavService.downloadFileStreaming(path);
        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + path + "\"")
                .body(responseBody);
    }

    @GetMapping("/list")
    public ResponseEntity<List<DufsWebDavService.WebDavFileInfo>> listDirectory(
            @RequestParam(required = false) String path) {
        return ResponseEntity.ok(webDavService.listDirectory(path == null ? "" : path));
    }

    @PostMapping("/directory")
    public ResponseEntity<?> createDirectory(@RequestParam String path) {
        webDavService.createDirectory(path);
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }

    @DeleteMapping
    public ResponseEntity<?> delete(@RequestParam String path) {
        webDavService.delete(path);
        return ResponseEntity.noContent().build();
    }

    @GetMapping("/exists")
    public ResponseEntity<Boolean> exists(@RequestParam String path) {
        return ResponseEntity.ok(webDavService.exists(path));
    }

3. 高级功能

3.1 大文件分块上传

public void chunkedUpload(String path, InputStream inputStream, long size) {
    String fullPath = buildFullPath(path);
    try {
        sardine.enableChunkedUpload();
        Map<String, String> headers = new HashMap<>();
        headers.put("Content-Length", String.valueOf(size));
        sardine.put(fullPath, inputStream, headers);
    } catch (IOException e) {
        throw new RuntimeException("Chunked upload failed", e);
    }
}

3.2 异步操作

@Async
public CompletableFuture<String> asyncUpload(String path, MultipartFile file) {
    try {
        uploadFile(path, file.getInputStream());
        return CompletableFuture.completedFuture("Upload success");
    } catch (IOException e) {
        CompletableFuture<String> future = new CompletableFuture<>();
        future.completeExceptionally(e);
        return future;
    }
}

4. 性能优化

  1. 连接池配置

    @Bean
    public Sardine sardine() {
        Sardine sardine = SardineFactory.begin(username, password);
        sardine.setConnectionTimeout(5000);
        sardine.setReadTimeout(10000);
        return sardine;
    }
    
  2. 流式下载大文件

    @GetMapping("/download-large")
    public ResponseEntity<StreamingResponseBody> downloadLargeFile(@RequestParam String path) {
        return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + path + "\"")
            .body(outputStream -> {
                try (InputStream is = sardine.get(buildFullPath(path))) {
                    byte[] buffer = new byte[8192];
                    int bytesRead;
                    while ((bytesRead = is.read(buffer)) != -1) {
                        outputStream.write(buffer, 0, bytesRead);
                    }
                }
            });
    }
    

5. 常见问题解决

5.1 InputStream 重复读取问题

使用 ByteArrayResource 或缓存文件内容解决:

public Resource downloadFile(String path) {
    return new ByteArrayResource(getFileBytes(path)) {
        @Override
        public String getFilename() {
            return path.substring(path.lastIndexOf('/') + 1);
        }
    };
}

5.2 异步方法返回错误

Java 8 兼容的失败 Future 创建方式:

@Async
public CompletableFuture<String> asyncOperation() {
    try {
        // 业务逻辑
        return CompletableFuture.completedFuture("success");
    } catch (Exception e) {
        CompletableFuture<String> future = new CompletableFuture<>();
        future.completeExceptionally(e);
        return future;
    }
}

结语

通过本文的介绍,我们实现了 Spring Boot 应用与 Dufs 文件服务器通过 WebDAV 协议的完整集成。这种方案具有以下优势:

  1. 轻量级:Dufs 服务器非常轻量
  2. 功能全面:支持标准 WebDAV 协议的所有操作
  3. 易于集成:Spring Boot 提供了良好的异步支持
  4. 性能良好:支持大文件流式传输

在实际项目中,可以根据需求进一步扩展功能,如添加文件预览、权限控制等。


网站公告

今日签到

点亮在社区的每一天
去签到