diff --git a/bonus-api/bonus-api-system/src/main/java/com/bonus/system/api/RemoteUploadUtilsService.java b/bonus-api/bonus-api-system/src/main/java/com/bonus/system/api/RemoteUploadUtilsService.java index a9857bc..311d6f3 100644 --- a/bonus-api/bonus-api-system/src/main/java/com/bonus/system/api/RemoteUploadUtilsService.java +++ b/bonus-api/bonus-api-system/src/main/java/com/bonus/system/api/RemoteUploadUtilsService.java @@ -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 zipFile(@RequestBody List objectNames, @RequestHeader(SecurityConstants.FROM_SOURCE) String source); + R zipFile(@RequestBody String objectNames, @RequestHeader(SecurityConstants.FROM_SOURCE) String source); } diff --git a/bonus-api/bonus-api-system/src/main/java/com/bonus/system/api/factory/RemoteUploadUtilsFallbackFactory.java b/bonus-api/bonus-api-system/src/main/java/com/bonus/system/api/factory/RemoteUploadUtilsFallbackFactory.java index bcd45f3..b8e61b2 100644 --- a/bonus-api/bonus-api-system/src/main/java/com/bonus/system/api/factory/RemoteUploadUtilsFallbackFactory.java +++ b/bonus-api/bonus-api-system/src/main/java/com/bonus/system/api/factory/RemoteUploadUtilsFallbackFactory.java @@ -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 zipFile(List objectNames, String source) { + public R zipFile(String objectNames, String source) { return R.fail("文件打包异常:" + throwable.getMessage()); } - }; } diff --git a/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/controller/ZipDownloadController.java b/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/controller/ZipDownloadController.java index 67a96b6..f3a2691 100644 --- a/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/controller/ZipDownloadController.java +++ b/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/controller/ZipDownloadController.java @@ -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 objectNames = service.getFileList(o); - DownloadTask task = fileUploadUtils.zipFile(objectNames); - return AjaxResult.success(task); + try { + //查询文件地址 + List 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 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); } + } + /** * 定时清理过期文件 */ diff --git a/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/service/impl/FileUploadUtils.java b/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/service/impl/FileUploadUtils.java index bb9d5ff..6e8a8b3 100644 --- a/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/service/impl/FileUploadUtils.java +++ b/bonus-modules/bonus-bmw/src/main/java/com/bonus/bmw/service/impl/FileUploadUtils.java @@ -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 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 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 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 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 uploadBast64List( String[] files, String sourceTable, String sourceId, String[] sourceType, String prefix, String bucketName ){ - R> re=service.uploadBast64List(files,sourceTable,sourceId,sourceType,prefix,bucketName, SecurityConstants.INNER); - if(re.getCode()==R.SUCCESS){ - List vo=re.getData(); + public List uploadBast64List(String[] files, String sourceTable, String sourceId, String[] sourceType, String prefix, String bucketName) { + R> re = service.uploadBast64List(files, sourceTable, sourceId, sourceType, prefix, bucketName, SecurityConstants.INNER); + if (re.getCode() == R.SUCCESS) { + List 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 uploadFile( MultipartFile[] files, String sourceTable, String sourceId, String[] sourceType, String prefix, String bucketName ){ - R> re=service.uploadFiles(files,sourceTable,sourceId,sourceType,prefix,bucketName, SecurityConstants.INNER); - if(re.getCode()==R.SUCCESS){ - List vo=re.getData(); + public List uploadFile(MultipartFile[] files, String sourceTable, String sourceId, String[] sourceType, String prefix, String bucketName) { + R> re = service.uploadFiles(files, sourceTable, sourceId, sourceType, prefix, bucketName, SecurityConstants.INNER); + if (re.getCode() == R.SUCCESS) { + List 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 getFileList(String id, String sourceId,String sourceTable,String sourceType ){ - R> res=service.getFileList(id,sourceId,sourceTable,sourceType, SecurityConstants.INNER); - if(res.getCode()==R.SUCCESS){ - List vo=res.getData(); + public List getFileList(String id, String sourceId, String sourceTable, String sourceType) { + R> res = service.getFileList(id, sourceId, sourceTable, sourceType, SecurityConstants.INNER); + if (res.getCode() == R.SUCCESS) { + List 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 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 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 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 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 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 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 objectNames){ - R res = service.zipFile(objectNames, SecurityConstants.INNER); - if(res.getCode()==R.SUCCESS){ + public DownloadTask zipFile(List objectNames) { + String params= JSON.toJSONString(objectNames); + R 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]", "_"); + } } diff --git a/bonus-modules/bonus-file/src/main/java/com/bonus/file/controller/FileUtilController.java b/bonus-modules/bonus-file/src/main/java/com/bonus/file/controller/FileUtilController.java index 4ddec7e..aa8ec0b 100644 --- a/bonus-modules/bonus-file/src/main/java/com/bonus/file/controller/FileUtilController.java +++ b/bonus-modules/bonus-file/src/main/java/com/bonus/file/controller/FileUtilController.java @@ -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 zipFile(@RequestBody List objectNames) { - if (objectNames == null || objectNames.isEmpty()) { - log.warn("提示:Received null or empty zip file list"); + public R zipFile(@RequestBody String objectNames) { + if(StringUtils.isEmpty(objectNames)){ return R.fail("文件列表不能为空"); } - return R.ok(service.zipFile(objectNames)); + List objectNameList = JSON.parseArray(objectNames, ZipFileMapping.class); + return R.ok(service.zipFile(objectNameList)); } }