打包下载问题修改

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.system.api.domain.DownloadTask;
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.model.UploadFileVo;
import org.springframework.cloud.openfeign.FeignClient;
@ -145,5 +144,5 @@ public interface RemoteUploadUtilsService {
@RequestHeader(SecurityConstants.FROM_SOURCE) String source);
@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.domain.DownloadTask;
import com.bonus.system.api.domain.FileVo;
import com.bonus.system.api.domain.ZipFileMapping;
import com.bonus.system.api.model.UploadFileVo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -78,11 +77,10 @@ public class RemoteUploadUtilsFallbackFactory implements FallbackFactory<RemoteU
}
@Override
public R<DownloadTask> zipFile(List<ZipFileMapping> objectNames, String source) {
public R<DownloadTask> zipFile(String objectNames, String source) {
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.system.api.domain.DownloadTask;
import com.bonus.system.api.domain.ZipFileMapping;
import io.minio.GetObjectArgs;
import io.minio.MinioClient;
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.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
@ -19,7 +14,9 @@ import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
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.Path;
import java.nio.file.Paths;
@ -44,10 +41,16 @@ public class ZipDownloadController {
@PostMapping("/createZipTask")
public AjaxResult createZipTask(@RequestBody BmWorkerAtt o) {
//查询文件地址
List<ZipFileMapping> objectNames = service.getFileList(o);
DownloadTask task = fileUploadUtils.zipFile(objectNames);
return AjaxResult.success(task);
try {
//查询文件地址
List<ZipFileMapping> objectNames = service.getFileList(o);
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}")
public AjaxResult getTaskStatus(@PathVariable String taskId) {
String redisKey = TASK_PREFIX + taskId;
DownloadTask task = redisTemplate.opsForValue().get(redisKey);
if (task == null) {
return AjaxResult.error("打包下载任务未查询");
try {
String redisKey = TASK_PREFIX + taskId;
DownloadTask task = redisTemplate.opsForValue().get(redisKey);
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:.+}")
public void serveZipFile(@PathVariable String filename, HttpServletResponse response) throws IOException {
// 1. 基础安全禁止路径遍历
if (filename.contains("..") || filename.startsWith("/") || filename.startsWith("\\")) {
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);
try {
// 1. 基础安全禁止路径遍历
if (filename.contains("..") || filename.startsWith("/") || filename.startsWith("\\")) {
response.sendError(403, "Path traversal not allowed");
return;
}
}
}
/**
* 使用 Apache Commons Compress MinIO 打包 ZIP支持 UTF-8 中文
*/
public static void generateZipFromMinIO(MinioClient minioClient, String bucket, List<ZipFileMapping> fileMappings, String zipPath) throws Exception {
Path outputPath = Paths.get(zipPath);
Files.createDirectories(outputPath.getParent());
try (FileOutputStream fos = new FileOutputStream(outputPath.toFile());
BufferedOutputStream bos = new BufferedOutputStream(fos, 64 * 1024);
ZipArchiveOutputStream zipOut = new ZipArchiveOutputStream(bos)) {
zipOut.setEncoding("UTF-8");
zipOut.setUseZip64(Zip64Mode.AsNeeded);
for (ZipFileMapping mapping : fileMappings) {
String objectName = mapping.getObjectName();
String targetPath = mapping.getTargetPath();
// 安全校验
if (objectName == null || targetPath == null ||
objectName.contains("..") || targetPath.contains("..") ||
objectName.startsWith("/") || targetPath.startsWith("/")) {
continue; // 跳过非法项
}
// 统一使用正斜杠兼容 Windows
String entryName = targetPath.replace('\\', '/');
try (InputStream objectStream = minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucket)
.object(objectName)
.build())) {
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);
// 可选择继续或中断
// 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);
}
}
zipOut.finish();
} catch (Exception e) {
log.error("下载文件异常", e);
}
}
/**
* 定时清理过期文件
*/

View File

@ -1,5 +1,6 @@
package com.bonus.bmw.service.impl;
import com.alibaba.fastjson.JSON;
import com.bonus.common.core.constant.SecurityConstants;
import com.bonus.common.core.domain.R;
import com.bonus.system.api.RemoteUploadUtilsService;
@ -28,36 +29,39 @@ public class FileUploadUtils {
/**
* 单文件上传
* @param file -必填
*
* @param file -必填
* @param sourceTable -必填
* @param sourceId -必填
* @param sourceType -非必填
* @param prefix -非必填
* @param sourceId -必填
* @param sourceType -非必填
* @param prefix -非必填
* @param bucketName -非必填
* @return
*/
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);
if(re.getCode()==R.SUCCESS){
UploadFileVo vo=re.getData();
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);
if (re.getCode() == R.SUCCESS) {
UploadFileVo vo = re.getData();
return re.getData();
}
return null;
}
/**
* 单文件上传-bast64
* @param file -必填
* @param sourceTable -必填
* @param sourceId -必填
* @param sourceType -非必填
* @param prefix -非必填
*
* @param file -必填
* @param sourceTable -必填
* @param sourceId -必填
* @param sourceType -非必填
* @param prefix -非必填
* @param bucketName -非必填
* @return
*/
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);
R<UploadFileVo> re=service.uploadBast64(fileVo, SecurityConstants.INNER);
if(re.getCode()==R.SUCCESS){
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);
R<UploadFileVo> re = service.uploadBast64(fileVo, SecurityConstants.INNER);
if (re.getCode() == R.SUCCESS) {
return re.getData();
}
return null;
@ -66,18 +70,19 @@ public class FileUploadUtils {
/**
* 多文件上传-bast64
* @param files 必填
*
* @param files 必填
* @param sourceTable 必填
* @param sourceId 必填
* @param sourceType 非必填 -如果填了 必须和files长度一直
* @param prefix 件存储路径文件架名称 -非必填
* @param bucketName 存储桶名称 -非必填
* @param sourceId 必填
* @param sourceType 非必填 -如果填了 必须和files长度一直
* @param prefix 件存储路径文件架名称 -非必填
* @param bucketName 存储桶名称 -非必填
* @return
*/
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);
if(re.getCode()==R.SUCCESS){
List<UploadFileVo> vo=re.getData();
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);
if (re.getCode() == R.SUCCESS) {
List<UploadFileVo> vo = re.getData();
return re.getData();
}
return null;
@ -85,35 +90,35 @@ public class FileUploadUtils {
/**
* 多文件上传
* @param files 必填
*
* @param files 必填
* @param sourceTable 必填
* @param sourceId 必填
* @param sourceType 非必填 -如果填了 必须和files长度一直
* @param prefix 件存储路径文件架名称 -非必填
* @param bucketName 存储桶名称 -非必填
* @param sourceId 必填
* @param sourceType 非必填 -如果填了 必须和files长度一直
* @param prefix 件存储路径文件架名称 -非必填
* @param bucketName 存储桶名称 -非必填
* @return
*/
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);
if(re.getCode()==R.SUCCESS){
List<UploadFileVo> vo=re.getData();
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);
if (re.getCode() == R.SUCCESS) {
List<UploadFileVo> vo = re.getData();
return re.getData();
}
return null;
}
/**
*
* @param id -选择性必填 id查询数据
* @param sourceId --选择性必填 依据资源id查询
* @param sourceTable --非必填 依据资源id和 表机构id查询
* @param sourceType --非必填 依据资源id和 表机构id查询
* @param id -选择性必填 id查询数据
* @param sourceId --选择性必填 依据资源id查询
* @param sourceTable --非必填 依据资源id和 表机构id查询
* @param sourceType --非必填 依据资源id和 表机构id查询
* @return
*/
public List<UploadFileVo> getFileList(String id, String sourceId,String sourceTable,String sourceType ){
R<List<UploadFileVo>> res=service.getFileList(id,sourceId,sourceTable,sourceType, SecurityConstants.INNER);
if(res.getCode()==R.SUCCESS){
List<UploadFileVo> vo=res.getData();
public List<UploadFileVo> getFileList(String id, String sourceId, String sourceTable, String sourceType) {
R<List<UploadFileVo>> res = service.getFileList(id, sourceId, sourceTable, sourceType, SecurityConstants.INNER);
if (res.getCode() == R.SUCCESS) {
List<UploadFileVo> vo = res.getData();
return res.getData();
}
return null;
@ -122,58 +127,68 @@ public class FileUploadUtils {
/**
* id /source 必传一个
* @param id -非必填 依据fileid删除
* @param sourceId --非必填 依据资源id删除
* @param sourceTable --非必填 依据资源id和 表机构id删除
* @param sourceTyp --非必填 依据资源id和 表机构id删除-及文件类型删除
*
* @param id -非必填 依据fileid删除
* @param sourceId --非必填 依据资源id删除
* @param sourceTable --非必填 依据资源id和 表机构id删除
* @param sourceTyp --非必填 依据资源id和 表机构id删除-及文件类型删除
* @return
*/
public String delFileList(String[] id,String[] sourceId,String[] sourceTable,String[] sourceTyp){
R<Integer> res=service.delFileList(id,sourceId,sourceTable,sourceTyp, SecurityConstants.INNER);
if(res.getCode()==R.SUCCESS){
Integer vo=res.getData();
public String delFileList(String[] id, String[] sourceId, String[] sourceTable, String[] sourceTyp) {
R<Integer> res = service.delFileList(id, sourceId, sourceTable, sourceTyp, SecurityConstants.INNER);
if (res.getCode() == R.SUCCESS) {
Integer vo = res.getData();
return String.valueOf(R.SUCCESS);
}
return String.valueOf(R.FAIL);
return String.valueOf(R.FAIL);
}
/**
* id /source 必传一个
* @param id -选择性必填 依据fileid删除
* @param sourceId --选择性必填 依据资源id删除
* @param sourceTable --非必填 依据资源id和 表机构id删除
* @param sourceTyp --非必填 依据资源id和 表机构id删除-及文件类型删除
*
* @param id -选择性必填 依据fileid删除
* @param sourceId --选择性必填 依据资源id删除
* @param sourceTable --非必填 依据资源id和 表机构id删除
* @param sourceTyp --非必填 依据资源id和 表机构id删除-及文件类型删除
* @return
*/
public String delFileListById( String id,String sourceId,String sourceTable,String sourceTyp){
R<Integer> res=service.delFileListById(id,sourceId,sourceTable,sourceTyp, SecurityConstants.INNER);
if(res.getCode()==R.SUCCESS){
Integer num=res.getData();
if(num==1){
public String delFileListById(String id, String sourceId, String sourceTable, String sourceTyp) {
R<Integer> res = service.delFileListById(id, sourceId, sourceTable, sourceTyp, SecurityConstants.INNER);
if (res.getCode() == R.SUCCESS) {
Integer num = res.getData();
if (num == 1) {
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){
R<UploadFileVo> res=service.getFileBast64(id,sourceId,sourceTable,sourceTyp, SecurityConstants.INNER);
if(res.getCode()==R.SUCCESS){
UploadFileVo vo=res.getData();
return vo;
public UploadFileVo getFileBast64(String id, String sourceId, String sourceTable, String sourceTyp) {
R<UploadFileVo> res = service.getFileBast64(id, sourceId, sourceTable, sourceTyp, SecurityConstants.INNER);
if (res.getCode() == R.SUCCESS) {
UploadFileVo vo = res.getData();
return vo;
}
return null;
}
/**
* 打包文件
* 打包文件
*/
public DownloadTask zipFile(List<ZipFileMapping> objectNames){
R<DownloadTask> res = service.zipFile(objectNames, SecurityConstants.INNER);
if(res.getCode()==R.SUCCESS){
public DownloadTask zipFile(List<ZipFileMapping> objectNames) {
String params= JSON.toJSONString(objectNames);
R<DownloadTask> res = service.zipFile(params, SecurityConstants.INNER);
if (res.getCode() == R.SUCCESS) {
return res.getData();
}
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;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson2.JSON;
import com.bonus.common.core.domain.R;
import com.bonus.common.core.utils.StringUtils;
import com.bonus.file.service.impl.FileUtilsServiceImpl;
import com.bonus.system.api.domain.DownloadTask;
import com.bonus.system.api.domain.FileVo;
@ -234,12 +236,12 @@ public class FileUtilController {
}
@PostMapping("/zipFile")
public R<DownloadTask> zipFile(@RequestBody List<ZipFileMapping> objectNames) {
if (objectNames == null || objectNames.isEmpty()) {
log.warn("提示:Received null or empty zip file list");
public R<DownloadTask> zipFile(@RequestBody String objectNames) {
if(StringUtils.isEmpty(objectNames)){
return R.fail("文件列表不能为空");
}
return R.ok(service.zipFile(objectNames));
List<ZipFileMapping> objectNameList = JSON.parseArray(objectNames, ZipFileMapping.class);
return R.ok(service.zipFile(objectNameList));
}
}