背景:多文件下载需要压缩zip并返回前端,文件夹层级结构保持不变
思路:先通过递归构建出所有的目录结构并生成zip,再服务器下载文件写入zip
实现:
1. 构建出每个文件的fullName,如果为顶层文件fullName即为当前文件名,如果有父级文件fullName为父级文件+ “/” + 子文件
示例: /opt/compose/docker-compose.yaml
private void loadFullName(StorageVO storage) {
if (storage.getParentId() == null) {
storage.setFullName(storage.getName());
} else {
Optional<Storage> optional = this.repository.findById(storage.getParentId());
if (optional.isPresent()) {
StorageVO parent = StorageVO.of(optional.get());
loadFullName(parent);
storage.setFullName(parent.getFullName() + File.separator + storage.getName());
} else {
storage.setFullName(storage.getName());
}
}
}
2. 前端传入需要下载的文件或文件夹id,查出所有的文件和文件夹,调用fullName方法,构建出zip需要的数据结构
public List<StorageVO> buildZip(List<String> ids) {
BooleanBuilder predicate = new BooleanBuilder();
predicate.and(QStorage.storage.deleted.eq(false));
predicate.and(QStorage.storage.id.in(ids));
List<StorageVO> storages = ListUtil.toList(this.repository.findAll(predicate))
.stream()
.map(storage -> StorageVO.of(storage).withChildren(storage.getChildren()))
.toList();
storages.addAll(storages.stream().map(StorageVO::getChildren).filter(Objects::nonNull).flatMap(Collection::stream).toList());
storages.forEach(this::loadFullName);
return storages;
}
3.把步骤2构建好的数据结构写入到zip流,并根据是否为文件夹创建目录结构,在服务器下载文件写入到zip
public void downloadFileToServer(String tmpDir, List<StorageVO> treeParams, ZipArchiveOutputStream zipArchiveOutputStream) throws IOException {
if (CollUtil.isNotEmpty(treeParams)) {
for (StorageVO child : treeParams) {
if (child.getIsDirectory()) {
tmpDir = tmpDir + File.separator + child.getFullName();
File mkdir = FileUtil.mkdir(tmpDir);
ZipArchiveEntry entry = new ZipArchiveEntry(mkdir, child.getFullName());
zipArchiveOutputStream.putArchiveEntry(entry);
} else {
tmpDir = tmpDir + File.separator + child.getFullName();
File file = new File(tmpDir);
ZipArchiveEntry entry = new ZipArchiveEntry(file, child.getFullName());
zipArchiveOutputStream.putArchiveEntry(entry);
try (InputStream inputStream = minioHandle.download(child.getPath())) {
int i;
byte[] bytes = new byte[1024];
while ((i = inputStream.read(bytes)) != -1) {
zipArchiveOutputStream.write(bytes, 0, i);
}
}
zipArchiveOutputStream.closeArchiveEntry();
zipArchiveOutputStream.flush();
}
}
}
}
4. 下载文件,并把文件流写入response
public void downloadFiles(List<StorageVO> storages, HttpServletResponse response) {
String tmpDir = System.getProperty("java.io.tmpdir") + "downloads";
if (!new File(tmpDir).exists()) {
FileUtil.mkdir(tmpDir);
}
ZipArchiveOutputStream zipArchiveOutputStream = null;
try {
String name = storages.size() > 1 ? storages.get(0).getName() + "等" + storages.stream()
.filter(storageVO -> !storageVO.getIsDirectory()).toList().size() + "个文件" : storages.get(0).getName();
response.reset();
response.setHeader("Accept-Ranges", "bytes");
String fileName = URLEncoder.encode(name + ".zip", StandardCharsets.UTF_8).replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName);
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
response.setContentType("application/json;charset=utf-8");
zipArchiveOutputStream = new ZipArchiveOutputStream(response.getOutputStream());
zipArchiveOutputStream.setUseZip64(Zip64Mode.AsNeeded);
downloadFileToServer(tmpDir, storages, zipArchiveOutputStream);
} catch (IOException e) {
e.printStackTrace();
} finally {
IoUtil.close(zipArchiveOutputStream);
FileUtil.del(tmpDir);
}
}
5. controller代码.
@RequestMapping("/{ids}/downloads")
public Result<Object> downloads(@PathVariable("ids") String ids, HttpServletResponse response) {
try {
storageService.downloadFiles(storageService.buildZip(Arrays.stream(ids.split(",")).toList()), response);
} catch (Exception e) {
e.printStackTrace();
throw new BizException("downloadFileError", "下载失败,请重试");
}
return Result.success();
}