打包下载问题修改

This commit is contained in:
方亮 2026-01-05 10:57:49 +08:00
parent 5da3559551
commit 6265691055
5 changed files with 160 additions and 175 deletions

View File

@ -5,7 +5,6 @@ import com.bonus.common.core.constant.ServiceNameConstants;
import com.bonus.common.core.domain.R; import com.bonus.common.core.domain.R;
import com.bonus.system.api.domain.DownloadTask; import com.bonus.system.api.domain.DownloadTask;
import com.bonus.system.api.domain.FileVo; import com.bonus.system.api.domain.FileVo;
import com.bonus.system.api.domain.ZipFileMapping;
import com.bonus.system.api.factory.RemoteUploadUtilsFallbackFactory; import com.bonus.system.api.factory.RemoteUploadUtilsFallbackFactory;
import com.bonus.system.api.model.UploadFileVo; import com.bonus.system.api.model.UploadFileVo;
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.openfeign.FeignClient;
@ -145,5 +144,5 @@ public interface RemoteUploadUtilsService {
@RequestHeader(SecurityConstants.FROM_SOURCE) String source); @RequestHeader(SecurityConstants.FROM_SOURCE) String source);
@PostMapping(value = "/uploadFile/zipFile") @PostMapping(value = "/uploadFile/zipFile")
R<DownloadTask> zipFile(@RequestBody List<ZipFileMapping> objectNames, @RequestHeader(SecurityConstants.FROM_SOURCE) String source); R<DownloadTask> zipFile(@RequestBody String objectNames, @RequestHeader(SecurityConstants.FROM_SOURCE) String source);
} }

View File

@ -4,7 +4,6 @@ import com.bonus.common.core.domain.R;
import com.bonus.system.api.RemoteUploadUtilsService; import com.bonus.system.api.RemoteUploadUtilsService;
import com.bonus.system.api.domain.DownloadTask; import com.bonus.system.api.domain.DownloadTask;
import com.bonus.system.api.domain.FileVo; import com.bonus.system.api.domain.FileVo;
import com.bonus.system.api.domain.ZipFileMapping;
import com.bonus.system.api.model.UploadFileVo; import com.bonus.system.api.model.UploadFileVo;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -78,11 +77,10 @@ public class RemoteUploadUtilsFallbackFactory implements FallbackFactory<RemoteU
} }
@Override @Override
public R<DownloadTask> zipFile(List<ZipFileMapping> objectNames, String source) { public R<DownloadTask> zipFile(String objectNames, String source) {
return R.fail("文件打包异常:" + throwable.getMessage()); return R.fail("文件打包异常:" + throwable.getMessage());
} }
}; };
} }

View File

@ -6,12 +6,7 @@ import com.bonus.bmw.service.impl.FileUploadUtils;
import com.bonus.common.core.web.domain.AjaxResult; 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.MinioClient;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.archivers.zip.Zip64Mode;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
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.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
@ -19,7 +14,9 @@ import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.*; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
@ -44,10 +41,16 @@ public class ZipDownloadController {
@PostMapping("/createZipTask") @PostMapping("/createZipTask")
public AjaxResult createZipTask(@RequestBody BmWorkerAtt o) { public AjaxResult createZipTask(@RequestBody BmWorkerAtt o) {
//查询文件地址 try {
List<ZipFileMapping> objectNames = service.getFileList(o); //查询文件地址
DownloadTask task = fileUploadUtils.zipFile(objectNames); List<ZipFileMapping> objectNames = service.getFileList(o);
return AjaxResult.success(task); DownloadTask task = fileUploadUtils.zipFile(objectNames);
return AjaxResult.success(task);
} catch (Exception e) {
log.error("打包任务异常", e);
return AjaxResult.error("打包任务异常");
}
} }
/** /**
@ -55,12 +58,18 @@ public class ZipDownloadController {
*/ */
@GetMapping("/taskStatus/{taskId}") @GetMapping("/taskStatus/{taskId}")
public AjaxResult getTaskStatus(@PathVariable String taskId) { public AjaxResult getTaskStatus(@PathVariable String taskId) {
String redisKey = TASK_PREFIX + taskId; try {
DownloadTask task = redisTemplate.opsForValue().get(redisKey); String redisKey = TASK_PREFIX + taskId;
if (task == null) { DownloadTask task = redisTemplate.opsForValue().get(redisKey);
return AjaxResult.error("打包下载任务未查询"); if (task == null) {
return AjaxResult.error("未查询打包任务");
}
return AjaxResult.success(task);
} catch (Exception e) {
log.error("查询打包任务异常", e);
return AjaxResult.error("查询打包任务异常");
} }
return AjaxResult.success(task);
} }
/** /**
@ -68,90 +77,52 @@ public class ZipDownloadController {
*/ */
@GetMapping("/downloadFile/{filename:.+}") @GetMapping("/downloadFile/{filename:.+}")
public void serveZipFile(@PathVariable String filename, HttpServletResponse response) throws IOException { public void serveZipFile(@PathVariable String filename, HttpServletResponse response) throws IOException {
// 1. 基础安全禁止路径遍历 try {
if (filename.contains("..") || filename.startsWith("/") || filename.startsWith("\\")) { // 1. 基础安全禁止路径遍历
response.sendError(403, "Path traversal not allowed"); if (filename.contains("..") || filename.startsWith("/") || filename.startsWith("\\")) {
return; response.sendError(403, "Path traversal not allowed");
} return;
// 2. 必须以 .zip 结尾
if (!filename.endsWith(".zip")) {
response.sendError(400, "Only .zip files allowed");
return;
}
// 3. 文件名只能包含字母数字中文连字符(-)下划线(_)(.)
// 且不能有连续多个点或非法字符
if (!filename.matches("^[\\w\\u4e00-\\u9fff.-]+$")) {
response.sendError(400, "Invalid characters in filename");
return;
}
// 4. 防止超长文件名可选
if (filename.length() > 255) {
response.sendError(400, "Filename too long");
return;
}
Path zipPath = Paths.get("/tmp/downloads/", filename);
if (!Files.exists(zipPath)) {
response.sendError(404);
return;
}
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment; filename=" + filename);
response.setContentLengthLong(Files.size(zipPath));
try (InputStream is = Files.newInputStream(zipPath);
OutputStream os = response.getOutputStream()) {
byte[] buffer = new byte[8192];
int n;
while ((n = is.read(buffer)) > 0) {
os.write(buffer, 0, n);
} }
} // 2. 必须以 .zip 结尾
} if (!filename.endsWith(".zip")) {
response.sendError(400, "Only .zip files allowed");
/** return;
* 使用 Apache Commons Compress MinIO 打包 ZIP支持 UTF-8 中文 }
*/ // 3. 文件名只能包含字母数字中文连字符(-)下划线(_)(.)
public static void generateZipFromMinIO(MinioClient minioClient, String bucket, List<ZipFileMapping> fileMappings, String zipPath) throws Exception { // 且不能有连续多个点或非法字符
Path outputPath = Paths.get(zipPath); if (!filename.matches("^[\\w\\u4e00-\\u9fff.-]+$")) {
Files.createDirectories(outputPath.getParent()); response.sendError(400, "Invalid characters in filename");
try (FileOutputStream fos = new FileOutputStream(outputPath.toFile()); return;
BufferedOutputStream bos = new BufferedOutputStream(fos, 64 * 1024); }
ZipArchiveOutputStream zipOut = new ZipArchiveOutputStream(bos)) { // 4. 防止超长文件名可选
zipOut.setEncoding("UTF-8"); if (filename.length() > 255) {
zipOut.setUseZip64(Zip64Mode.AsNeeded); response.sendError(400, "Filename too long");
for (ZipFileMapping mapping : fileMappings) { return;
String objectName = mapping.getObjectName(); }
String targetPath = mapping.getTargetPath(); Path zipPath = Paths.get("/tmp/downloads/", filename);
// 安全校验 if (!Files.exists(zipPath)) {
if (objectName == null || targetPath == null || response.sendError(404);
objectName.contains("..") || targetPath.contains("..") || return;
objectName.startsWith("/") || targetPath.startsWith("/")) { }
continue; // 跳过非法项 response.setContentType("application/zip");
} response.setHeader("Content-Disposition", "attachment; filename=" + filename);
// 统一使用正斜杠兼容 Windows response.setContentLengthLong(Files.size(zipPath));
String entryName = targetPath.replace('\\', '/'); try (InputStream is = Files.newInputStream(zipPath);
try (InputStream objectStream = minioClient.getObject( OutputStream os = response.getOutputStream()) {
GetObjectArgs.builder() byte[] buffer = new byte[8192];
.bucket(bucket) int n;
.object(objectName) while ((n = is.read(buffer)) > 0) {
.build())) { os.write(buffer, 0, n);
ZipArchiveEntry entry = new ZipArchiveEntry(entryName);
zipOut.putArchiveEntry(entry);
byte[] buffer = new byte[8192];
int len;
while ((len = objectStream.read(buffer)) > 0) {
zipOut.write(buffer, 0, len);
}
zipOut.closeArchiveEntry();
} catch (Exception e) {
System.err.println("Failed to process object: " + objectName + " -> " + targetPath);
// 可选择继续或中断
} }
} }
zipOut.finish();
} catch (Exception e) {
log.error("下载文件异常", e);
} }
} }
/** /**
* 定时清理过期文件 * 定时清理过期文件
*/ */

View File

@ -1,5 +1,6 @@
package com.bonus.bmw.service.impl; package com.bonus.bmw.service.impl;
import com.alibaba.fastjson.JSON;
import com.bonus.common.core.constant.SecurityConstants; import com.bonus.common.core.constant.SecurityConstants;
import com.bonus.common.core.domain.R; import com.bonus.common.core.domain.R;
import com.bonus.system.api.RemoteUploadUtilsService; import com.bonus.system.api.RemoteUploadUtilsService;
@ -28,36 +29,39 @@ public class FileUploadUtils {
/** /**
* 单文件上传 * 单文件上传
* @param file -必填 *
* @param file -必填
* @param sourceTable -必填 * @param sourceTable -必填
* @param sourceId -必填 * @param sourceId -必填
* @param sourceType -非必填 * @param sourceType -非必填
* @param prefix -非必填 * @param prefix -非必填
* @param bucketName -非必填 * @param bucketName -非必填
* @return * @return
*/ */
public UploadFileVo uploadFile( MultipartFile file, String sourceTable, String sourceId, String sourceType, String prefix, String bucketName ){ public UploadFileVo uploadFile(MultipartFile file, String sourceTable, String sourceId, String sourceType, String prefix, String bucketName) {
R<UploadFileVo> re=service.upload(file,sourceTable,sourceId,sourceType,prefix,bucketName, SecurityConstants.INNER); R<UploadFileVo> re = service.upload(file, sourceTable, sourceId, sourceType, prefix, bucketName, SecurityConstants.INNER);
if(re.getCode()==R.SUCCESS){ if (re.getCode() == R.SUCCESS) {
UploadFileVo vo=re.getData(); UploadFileVo vo = re.getData();
return re.getData(); return re.getData();
} }
return null; return null;
} }
/** /**
* 单文件上传-bast64 * 单文件上传-bast64
* @param file -必填 *
* @param sourceTable -必填 * @param file -必填
* @param sourceId -必填 * @param sourceTable -必填
* @param sourceType -非必填 * @param sourceId -必填
* @param prefix -非必填 * @param sourceType -非必填
* @param prefix -非必填
* @param bucketName -非必填 * @param bucketName -非必填
* @return * @return
*/ */
public UploadFileVo uploadBast64( String file, String sourceTable, String sourceId, String sourceType, String prefix, String bucketName ){ public UploadFileVo uploadBast64(String file, String sourceTable, String sourceId, String sourceType, String prefix, String bucketName) {
FileVo fileVo=new FileVo(file,sourceTable,sourceType,prefix,bucketName,sourceId); FileVo fileVo = new FileVo(file, sourceTable, sourceType, prefix, bucketName, sourceId);
R<UploadFileVo> re=service.uploadBast64(fileVo, SecurityConstants.INNER); R<UploadFileVo> re = service.uploadBast64(fileVo, SecurityConstants.INNER);
if(re.getCode()==R.SUCCESS){ if (re.getCode() == R.SUCCESS) {
return re.getData(); return re.getData();
} }
return null; return null;
@ -66,18 +70,19 @@ public class FileUploadUtils {
/** /**
* 多文件上传-bast64 * 多文件上传-bast64
* @param files 必填 *
* @param files 必填
* @param sourceTable 必填 * @param sourceTable 必填
* @param sourceId 必填 * @param sourceId 必填
* @param sourceType 非必填 -如果填了 必须和files长度一直 * @param sourceType 非必填 -如果填了 必须和files长度一直
* @param prefix 件存储路径文件架名称 -非必填 * @param prefix 件存储路径文件架名称 -非必填
* @param bucketName 存储桶名称 -非必填 * @param bucketName 存储桶名称 -非必填
* @return * @return
*/ */
public List<UploadFileVo> uploadBast64List( String[] files, String sourceTable, String sourceId, String[] sourceType, String prefix, String bucketName ){ public List<UploadFileVo> uploadBast64List(String[] files, String sourceTable, String sourceId, String[] sourceType, String prefix, String bucketName) {
R<List<UploadFileVo>> re=service.uploadBast64List(files,sourceTable,sourceId,sourceType,prefix,bucketName, SecurityConstants.INNER); R<List<UploadFileVo>> re = service.uploadBast64List(files, sourceTable, sourceId, sourceType, prefix, bucketName, SecurityConstants.INNER);
if(re.getCode()==R.SUCCESS){ if (re.getCode() == R.SUCCESS) {
List<UploadFileVo> vo=re.getData(); List<UploadFileVo> vo = re.getData();
return re.getData(); return re.getData();
} }
return null; return null;
@ -85,35 +90,35 @@ public class FileUploadUtils {
/** /**
* 多文件上传 * 多文件上传
* @param files 必填 *
* @param files 必填
* @param sourceTable 必填 * @param sourceTable 必填
* @param sourceId 必填 * @param sourceId 必填
* @param sourceType 非必填 -如果填了 必须和files长度一直 * @param sourceType 非必填 -如果填了 必须和files长度一直
* @param prefix 件存储路径文件架名称 -非必填 * @param prefix 件存储路径文件架名称 -非必填
* @param bucketName 存储桶名称 -非必填 * @param bucketName 存储桶名称 -非必填
* @return * @return
*/ */
public List<UploadFileVo> uploadFile( MultipartFile[] files, String sourceTable, String sourceId, String[] sourceType, String prefix, String bucketName ){ public List<UploadFileVo> uploadFile(MultipartFile[] files, String sourceTable, String sourceId, String[] sourceType, String prefix, String bucketName) {
R<List<UploadFileVo>> re=service.uploadFiles(files,sourceTable,sourceId,sourceType,prefix,bucketName, SecurityConstants.INNER); R<List<UploadFileVo>> re = service.uploadFiles(files, sourceTable, sourceId, sourceType, prefix, bucketName, SecurityConstants.INNER);
if(re.getCode()==R.SUCCESS){ if (re.getCode() == R.SUCCESS) {
List<UploadFileVo> vo=re.getData(); List<UploadFileVo> vo = re.getData();
return re.getData(); return re.getData();
} }
return null; return null;
} }
/** /**
* * @param id -选择性必填 id查询数据
* @param id -选择性必填 id查询数据 * @param sourceId --选择性必填 依据资源id查询
* @param sourceId --选择性必填 依据资源id查询 * @param sourceTable --非必填 依据资源id和 表机构id查询
* @param sourceTable --非必填 依据资源id和 表机构id查询 * @param sourceType --非必填 依据资源id和 表机构id查询
* @param sourceType --非必填 依据资源id和 表机构id查询
* @return * @return
*/ */
public List<UploadFileVo> getFileList(String id, String sourceId,String sourceTable,String sourceType ){ public List<UploadFileVo> getFileList(String id, String sourceId, String sourceTable, String sourceType) {
R<List<UploadFileVo>> res=service.getFileList(id,sourceId,sourceTable,sourceType, SecurityConstants.INNER); R<List<UploadFileVo>> res = service.getFileList(id, sourceId, sourceTable, sourceType, SecurityConstants.INNER);
if(res.getCode()==R.SUCCESS){ if (res.getCode() == R.SUCCESS) {
List<UploadFileVo> vo=res.getData(); List<UploadFileVo> vo = res.getData();
return res.getData(); return res.getData();
} }
return null; return null;
@ -122,58 +127,68 @@ public class FileUploadUtils {
/** /**
* id /source 必传一个 * id /source 必传一个
* @param id -非必填 依据fileid删除 *
* @param sourceId --非必填 依据资源id删除 * @param id -非必填 依据fileid删除
* @param sourceTable --非必填 依据资源id和 表机构id删除 * @param sourceId --非必填 依据资源id删除
* @param sourceTyp --非必填 依据资源id和 表机构id删除-及文件类型删除 * @param sourceTable --非必填 依据资源id和 表机构id删除
* @param sourceTyp --非必填 依据资源id和 表机构id删除-及文件类型删除
* @return * @return
*/ */
public String delFileList(String[] id,String[] sourceId,String[] sourceTable,String[] sourceTyp){ public String delFileList(String[] id, String[] sourceId, String[] sourceTable, String[] sourceTyp) {
R<Integer> res=service.delFileList(id,sourceId,sourceTable,sourceTyp, SecurityConstants.INNER); R<Integer> res = service.delFileList(id, sourceId, sourceTable, sourceTyp, SecurityConstants.INNER);
if(res.getCode()==R.SUCCESS){ if (res.getCode() == R.SUCCESS) {
Integer vo=res.getData(); Integer vo = res.getData();
return String.valueOf(R.SUCCESS); return String.valueOf(R.SUCCESS);
} }
return String.valueOf(R.FAIL); return String.valueOf(R.FAIL);
} }
/** /**
* id /source 必传一个 * id /source 必传一个
* @param id -选择性必填 依据fileid删除 *
* @param sourceId --选择性必填 依据资源id删除 * @param id -选择性必填 依据fileid删除
* @param sourceTable --非必填 依据资源id和 表机构id删除 * @param sourceId --选择性必填 依据资源id删除
* @param sourceTyp --非必填 依据资源id和 表机构id删除-及文件类型删除 * @param sourceTable --非必填 依据资源id和 表机构id删除
* @param sourceTyp --非必填 依据资源id和 表机构id删除-及文件类型删除
* @return * @return
*/ */
public String delFileListById( String id,String sourceId,String sourceTable,String sourceTyp){ public String delFileListById(String id, String sourceId, String sourceTable, String sourceTyp) {
R<Integer> res=service.delFileListById(id,sourceId,sourceTable,sourceTyp, SecurityConstants.INNER); R<Integer> res = service.delFileListById(id, sourceId, sourceTable, sourceTyp, SecurityConstants.INNER);
if(res.getCode()==R.SUCCESS){ if (res.getCode() == R.SUCCESS) {
Integer num=res.getData(); Integer num = res.getData();
if(num==1){ if (num == 1) {
return String.valueOf(R.SUCCESS); return String.valueOf(R.SUCCESS);
} }
} }
return String.valueOf(R.FAIL); return String.valueOf(R.FAIL);
} }
public UploadFileVo getFileBast64( String id,String sourceId,String sourceTable,String sourceTyp){ public UploadFileVo getFileBast64(String id, String sourceId, String sourceTable, String sourceTyp) {
R<UploadFileVo> res=service.getFileBast64(id,sourceId,sourceTable,sourceTyp, SecurityConstants.INNER); R<UploadFileVo> res = service.getFileBast64(id, sourceId, sourceTable, sourceTyp, SecurityConstants.INNER);
if(res.getCode()==R.SUCCESS){ if (res.getCode() == R.SUCCESS) {
UploadFileVo vo=res.getData(); UploadFileVo vo = res.getData();
return vo; return vo;
} }
return null; return null;
} }
/** /**
* 打包文件 * 打包文件
*/ */
public DownloadTask zipFile(List<ZipFileMapping> objectNames){ public DownloadTask zipFile(List<ZipFileMapping> objectNames) {
R<DownloadTask> res = service.zipFile(objectNames, SecurityConstants.INNER); String params= JSON.toJSONString(objectNames);
if(res.getCode()==R.SUCCESS){ R<DownloadTask> res = service.zipFile(params, SecurityConstants.INNER);
if (res.getCode() == R.SUCCESS) {
return res.getData(); return res.getData();
} }
return null; return null;
}
}
// 工具方法过滤字符串中的非法控制字符复用之前的逻辑
public String cleanIllegalChars(String str) {
if (str == null) return "";
// 匹配ASCII 0-31除\r\n\t的控制字符替换为下划线
return str.replaceAll("[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]", "_");
}
} }

View File

@ -1,7 +1,9 @@
package com.bonus.file.controller; package com.bonus.file.controller;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson2.JSON;
import com.bonus.common.core.domain.R; import com.bonus.common.core.domain.R;
import com.bonus.common.core.utils.StringUtils;
import com.bonus.file.service.impl.FileUtilsServiceImpl; import com.bonus.file.service.impl.FileUtilsServiceImpl;
import com.bonus.system.api.domain.DownloadTask; import com.bonus.system.api.domain.DownloadTask;
import com.bonus.system.api.domain.FileVo; import com.bonus.system.api.domain.FileVo;
@ -234,12 +236,12 @@ public class FileUtilController {
} }
@PostMapping("/zipFile") @PostMapping("/zipFile")
public R<DownloadTask> zipFile(@RequestBody List<ZipFileMapping> objectNames) { public R<DownloadTask> zipFile(@RequestBody String objectNames) {
if (objectNames == null || objectNames.isEmpty()) { if(StringUtils.isEmpty(objectNames)){
log.warn("提示:Received null or empty zip file list");
return R.fail("文件列表不能为空"); return R.fail("文件列表不能为空");
} }
return R.ok(service.zipFile(objectNames)); List<ZipFileMapping> objectNameList = JSON.parseArray(objectNames, ZipFileMapping.class);
return R.ok(service.zipFile(objectNameList));
} }
} }