diff --git a/backend/src/main/java/com/yfd/platform/data/controller/FishDraftDataController.java b/backend/src/main/java/com/yfd/platform/data/controller/FishDraftDataController.java index 1e1a0d5..318650e 100644 --- a/backend/src/main/java/com/yfd/platform/data/controller/FishDraftDataController.java +++ b/backend/src/main/java/com/yfd/platform/data/controller/FishDraftDataController.java @@ -3,7 +3,9 @@ package com.yfd.platform.data.controller; import java.io.File; import java.io.FileInputStream; import java.io.OutputStream; +import java.net.URLDecoder; import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -495,6 +497,9 @@ public class FishDraftDataController { @GetMapping("/previewFile") @Operation(summary = "预览临时文件内容") public void previewFile(@RequestParam String taskId, @RequestParam String filename, @RequestParam String type, HttpServletRequest request, HttpServletResponse response) { + // 解码 URL 编码的 filename(处理中文文件名) + String decodedFilename = URLDecoder.decode(filename, StandardCharsets.UTF_8); + log.debug("原始文件名: {}, 解码后: {}", filename, decodedFilename); ImportTask importTask = importTaskService.getById(taskId); String resultJson = importTask.getResultJson(); @@ -502,9 +507,9 @@ public class FishDraftDataController { String dir = "1".equals(type) ? "images" : "videos"; if (resultJson != null && !resultJson.isEmpty()) { try { - FishImportResult importResult = objectMapper.readValue(resultJson, FishImportResult.class); - String tempDir = importResult.getTempDir(); - filePath = tempDir + File.separator + dir + File.separator + filename; +// FishImportResult importResult = objectMapper.readValue(resultJson, FishImportResult.class); + String tempDir = importTask.getTempDir(); + filePath = tempDir + File.separator + dir + File.separator + decodedFilename; } catch (Exception e) { e.printStackTrace(); // ignore parse error diff --git a/backend/src/main/java/com/yfd/platform/data/utils/ZipFileUtil.java b/backend/src/main/java/com/yfd/platform/data/utils/ZipFileUtil.java index 662fb4b..d183bca 100644 --- a/backend/src/main/java/com/yfd/platform/data/utils/ZipFileUtil.java +++ b/backend/src/main/java/com/yfd/platform/data/utils/ZipFileUtil.java @@ -1,5 +1,7 @@ package com.yfd.platform.data.utils; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.StrUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -364,22 +366,99 @@ public class ZipFileUtil { } private static String saveFileToDir(InputStream is, File tempDir, String subFolder, String fileName) throws IOException { + // 使用 Hutool 构建文件路径 + String safeFileName = sanitizeFileName(fileName); File folder = new File(tempDir, subFolder); - if (!folder.exists()) { - folder.mkdirs(); + + // 使用 Hutool 创建目录(会自动创建父目录) + FileUtil.mkdir(folder); + + log.info("保存文件: fileName{} -> safeFileName{}->subFolder{}", fileName, safeFileName,subFolder); + // 构建完整文件路径 + File file = new File(folder, safeFileName); + + // 安全检查:防止目录穿越 + String canonicalPath = file.getCanonicalPath(); + String canonicalDir = folder.getCanonicalPath(); + if (!canonicalPath.startsWith(canonicalDir)) { + throw new IOException("非法的文件路径: " + fileName); } - File file = new File(folder, fileName); - try (FileOutputStream fos = new FileOutputStream(file)) { - byte[] buffer = new byte[4096]; - int len; - while ((len = is.read(buffer)) > 0) { - fos.write(buffer, 0, len); - } + // 使用 Hutool 从流复制到文件 + try { + FileUtil.writeFromStream(is, file); + log.debug("保存文件: {} -> {}, 大小: {} bytes", fileName, safeFileName, file.length()); + } catch (Exception e) { + throw new IOException("保存文件失败: " + fileName, e); } + return file.getAbsolutePath(); } + + /** + * 清理和标准化文件名,确保在 Linux 上正确显示中文 + */ + private static String sanitizeFileName(String fileName) { + if (StrUtil.isBlank(fileName)) { + return "unnamed_" + System.currentTimeMillis(); + } + + // 使用 Hutool 去除首尾空格 + String sanitized = StrUtil.trim(fileName); + + // 替换非法字符(Linux/Windows 都不允许的字符) + sanitized = sanitized.replaceAll("[\\\\/:*?\"<>|]", "_"); + + // 去除连续的下划线 + sanitized = sanitized.replaceAll("_+", "_"); + + // 如果文件名过长,截断(Linux 文件名最大 255 字节) + byte[] bytes = sanitized.getBytes(StandardCharsets.UTF_8); + if (bytes.length > 200) { + String extension = ""; + int dotIndex = sanitized.lastIndexOf('.'); + if (dotIndex > 0 && dotIndex < sanitized.length() - 1) { + extension = sanitized.substring(dotIndex); + sanitized = sanitized.substring(0, dotIndex); + } + + // 重新计算长度 + bytes = sanitized.getBytes(StandardCharsets.UTF_8); + int maxNameLength = 200 - extension.getBytes(StandardCharsets.UTF_8).length; + + if (bytes.length > maxNameLength) { + // 安全截断,避免切断多字节字符 + sanitized = StrUtil.subPre(sanitized, maxNameLength); + } + sanitized = sanitized + extension; + } + + // 确保文件名不为空 + if (StrUtil.isBlank(sanitized) || ".".equals(sanitized)) { + return "file_" + System.currentTimeMillis(); + } + + return sanitized; + } + +// private static String saveFileToDir(InputStream is, File tempDir, String subFolder, String fileName) throws IOException { +// File folder = new File(tempDir, subFolder); +// if (!folder.exists()) { +// folder.mkdirs(); +// } +// +// File file = new File(folder, fileName); +// try (FileOutputStream fos = new FileOutputStream(file)) { +// byte[] buffer = new byte[4096]; +// int len; +// while ((len = is.read(buffer)) > 0) { +// fos.write(buffer, 0, len); +// } +// } +// return file.getAbsolutePath(); +// } + private static boolean isImageFile(String fileName) { return fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") || fileName.endsWith(".png") || fileName.endsWith(".gif") ||