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. 性能优化
连接池配置:
@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)) {
@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 协议的完整集成。这种方案具有以下优势:
- 轻量级:Dufs 服务器非常轻量
- 功能全面:支持标准 WebDAV 协议的所有操作
- 易于集成:Spring Boot 提供了良好的异步支持
- 性能良好:支持大文件流式传输
在实际项目中,可以根据需求进一步扩展功能,如添加文件预览、权限控制等。