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 = "/";@Beanpublic 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 IOreturn new ByteArrayResource(bytes) {@Overridepublic 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() {@Overridepublic String getDescription() {return "WebDAV resource [" + fullPath + "]";}@Overridepublic InputStream getInputStream() throws IOException {// 每次调用都获取新的流return sardine.get(fullPath);}@Overridepublic 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@AllArgsConstructorpublic 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();}@DeleteMappingpublic 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. 性能优化
-
连接池配置:
@Bean public Sardine sardine() {Sardine sardine = SardineFactory.begin(username, password);sardine.setConnectionTimeout(5000);sardine.setReadTimeout(10000);return sardine; }
-
流式下载大文件:
@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)) {@Overridepublic 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 协议的完整集成。这种方案具有以下优势:
- 轻量级:Dufs 服务器非常轻量
- 功能全面:支持标准 WebDAV 协议的所有操作
- 易于集成:Spring Boot 提供了良好的异步支持
- 性能良好:支持大文件流式传输
在实际项目中,可以根据需求进一步扩展功能,如添加文件预览、权限控制等。