Spring Boot 集成 Dufs 通过 WebDAV 实现文件管理
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 提供了良好的异步支持
- 性能良好:支持大文件流式传输
在实际项目中,可以根据需求进一步扩展功能,如添加文件预览、权限控制等。