打包下载修改

This commit is contained in:
方亮 2026-01-04 10:58:35 +08:00
parent c5bc8d14c6
commit cf7b9087f9
3 changed files with 54 additions and 19 deletions

View File

@ -3,6 +3,7 @@ package com.bonus.bmw.controller;
import com.bonus.bmw.domain.vo.BmWorkerAtt; import com.bonus.bmw.domain.vo.BmWorkerAtt;
import com.bonus.bmw.service.ZipDownloadService; import com.bonus.bmw.service.ZipDownloadService;
import com.bonus.bmw.service.impl.FileUploadUtils; import com.bonus.bmw.service.impl.FileUploadUtils;
import com.bonus.common.core.web.domain.AjaxResult;
import com.bonus.system.api.domain.DownloadTask; import com.bonus.system.api.domain.DownloadTask;
import com.bonus.system.api.domain.ZipFileMapping; import com.bonus.system.api.domain.ZipFileMapping;
import io.minio.GetObjectArgs; import io.minio.GetObjectArgs;
@ -13,7 +14,6 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -43,24 +43,24 @@ public class ZipDownloadController {
private static final String TASK_PREFIX = "zip_task:"; // key 前缀 private static final String TASK_PREFIX = "zip_task:"; // key 前缀
@PostMapping("/createZipTask") @PostMapping("/createZipTask")
public ResponseEntity<DownloadTask> createZipTask(@RequestBody BmWorkerAtt o) { public AjaxResult createZipTask(@RequestBody BmWorkerAtt o) {
//查询文件地址 //查询文件地址
List<ZipFileMapping> objectNames = service.getFileList(o); List<ZipFileMapping> objectNames = service.getFileList(o);
DownloadTask task = fileUploadUtils.zipFile(objectNames); DownloadTask task = fileUploadUtils.zipFile(objectNames);
return ResponseEntity.accepted().body(task); return AjaxResult.success(task);
} }
/** /**
* 查询任务状态 * 查询任务状态
*/ */
@GetMapping("/taskStatus/{taskId}") @GetMapping("/taskStatus/{taskId}")
public ResponseEntity<DownloadTask> getTaskStatus(@PathVariable String taskId) { public AjaxResult getTaskStatus(@PathVariable String taskId) {
String redisKey = TASK_PREFIX + taskId; String redisKey = TASK_PREFIX + taskId;
DownloadTask task = redisTemplate.opsForValue().get(redisKey); DownloadTask task = redisTemplate.opsForValue().get(redisKey);
if (task == null) { if (task == null) {
return ResponseEntity.notFound().build(); return AjaxResult.error("打包下载任务未查询");
} }
return ResponseEntity.ok(task); return AjaxResult.success(task);
} }
/** /**

View File

@ -236,7 +236,7 @@ public class FileUtilController {
@PostMapping("/zipFile") @PostMapping("/zipFile")
public R<DownloadTask> zipFile(@RequestBody List<ZipFileMapping> objectNames) { public R<DownloadTask> zipFile(@RequestBody List<ZipFileMapping> objectNames) {
if (objectNames == null || objectNames.isEmpty()) { if (objectNames == null || objectNames.isEmpty()) {
log.warn("Received null or empty zip file list"); log.warn("提示:Received null or empty zip file list");
return R.fail("文件列表不能为空"); return R.fail("文件列表不能为空");
} }
return R.ok(service.zipFile(objectNames)); return R.ok(service.zipFile(objectNames));

View File

@ -26,6 +26,7 @@ import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@ -539,8 +540,15 @@ public class FileUtilsServiceImpl {
/** /**
* 使用 Apache Commons Compress MinIO 打包 ZIP支持 UTF-8 中文 * 使用 Apache Commons Compress MinIO 打包 ZIP支持 UTF-8 中文
*
* @param fileMappings 要打包的文件映射列表MinIO 对象名 ZIP 内路径
* @param zipPath 生成的 ZIP 文件绝对路径
* @throws IOException 若无法创建 ZIP 文件或写入失败非单个文件错误
*/ */
public void generateZipFromMinIO( List<ZipFileMapping> fileMappings, String zipPath) throws Exception { public void generateZipFromMinIO(List<ZipFileMapping> fileMappings, String zipPath) throws IOException {
if (fileMappings == null || fileMappings.isEmpty()) {
throw new IllegalArgumentException("文件映射列表不能为空");
}
Path outputPath = Paths.get(zipPath); Path outputPath = Paths.get(zipPath);
Files.createDirectories(outputPath.getParent()); Files.createDirectories(outputPath.getParent());
try (FileOutputStream fos = new FileOutputStream(outputPath.toFile()); try (FileOutputStream fos = new FileOutputStream(outputPath.toFile());
@ -549,15 +557,25 @@ public class FileUtilsServiceImpl {
zipOut.setEncoding("UTF-8"); zipOut.setEncoding("UTF-8");
zipOut.setUseZip64(Zip64Mode.AsNeeded); zipOut.setUseZip64(Zip64Mode.AsNeeded);
for (ZipFileMapping mapping : fileMappings) { for (ZipFileMapping mapping : fileMappings) {
String objectName = mapping.getObjectName(); // 安全校验防止路径穿越和空值
String targetPath = mapping.getTargetPath(); if (mapping == null
// 安全校验 || mapping.getObjectName() == null
if (objectName == null || targetPath == null || || mapping.getTargetPath() == null
objectName.contains("..") || targetPath.contains("..") || || mapping.getObjectName().contains("..")
objectName.startsWith("/") || targetPath.startsWith("/")) { || mapping.getTargetPath().contains("..")
continue; // 跳过非法项 || mapping.getObjectName().startsWith("/")
|| mapping.getTargetPath().startsWith("/")) {
log.warn("跳过非法或空的文件映射: {}", mapping);
continue;
} }
// 统一使用正斜杠兼容 Windows String objectName = mapping.getObjectName().trim();
String targetPath = mapping.getTargetPath().trim();
if (objectName.isEmpty() || targetPath.isEmpty()) {
log.warn("跳过空路径映射: objectName='{}', targetPath='{}'", objectName, targetPath);
continue;
}
// 统一使用正斜杠兼容 Windows 解压
String entryName = targetPath.replace('\\', '/'); String entryName = targetPath.replace('\\', '/');
try (InputStream objectStream = minioClient.getObject( try (InputStream objectStream = minioClient.getObject(
GetObjectArgs.builder() GetObjectArgs.builder()
@ -565,6 +583,8 @@ public class FileUtilsServiceImpl {
.object(objectName) .object(objectName)
.build())) { .build())) {
ZipArchiveEntry entry = new ZipArchiveEntry(entryName); ZipArchiveEntry entry = new ZipArchiveEntry(entryName);
// 可选设置时间戳为当前时间避免 MinIO 时间暴露
entry.setTime(System.currentTimeMillis());
zipOut.putArchiveEntry(entry); zipOut.putArchiveEntry(entry);
byte[] buffer = new byte[8192]; byte[] buffer = new byte[8192];
int len; int len;
@ -572,13 +592,28 @@ public class FileUtilsServiceImpl {
zipOut.write(buffer, 0, len); zipOut.write(buffer, 0, len);
} }
zipOut.closeArchiveEntry(); zipOut.closeArchiveEntry();
log.debug("成功添加文件到 ZIP: {} -> {}", objectName, entryName);
} catch (Exception e) { } catch (Exception e) {
log.error(e.toString(), e); // 记录具体错误但继续处理其他文件
System.err.println("Failed to process object: " + objectName + " -> " + targetPath); log.error("从 MinIO 下载文件失败,已跳过: objectName='{}', targetPath='{}'",
// 可选择继续或中断 objectName, entryName, e);
// 注意不要在这里 closeArchiveEntry因为 putArchiveEntry 后若未写完就异常
// Apache Commons Compress 会自动清理未完成的 entry只要不调用 closeArchiveEntry
// 所以直接 continue 即可
} }
} }
// 确保 ZIP 结构完整
zipOut.finish(); zipOut.finish();
log.info("ZIP 文件生成完成: {}", zipPath);
} catch (IOException e) {
log.error("写入 ZIP 文件时发生严重错误: {}", zipPath, e);
// 尝试删除可能损坏的 ZIP 文件
try {
Files.deleteIfExists(outputPath);
} catch (IOException deleteEx) {
log.warn("无法删除损坏的 ZIP 文件: {}", outputPath, deleteEx);
}
throw e; // 向上抛出表示任务整体失败
} }
} }