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 提供了良好的異步支持
- 性能良好:支持大文件流式傳輸
在實際項目中,可以根據需求進一步擴展功能,如添加文件預覽、權限控制等。