打包下载修改
This commit is contained in:
parent
c5bc8d14c6
commit
cf7b9087f9
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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; // 向上抛出,表示任务整体失败
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue