diff --git a/java/src/main/java/com/yfd/platform/modules/experimentalData/controller/TsFilesController.java b/java/src/main/java/com/yfd/platform/modules/experimentalData/controller/TsFilesController.java index 20b61b3..8274339 100644 --- a/java/src/main/java/com/yfd/platform/modules/experimentalData/controller/TsFilesController.java +++ b/java/src/main/java/com/yfd/platform/modules/experimentalData/controller/TsFilesController.java @@ -174,11 +174,11 @@ public class TsFilesController { @PostMapping("/deleteTsFilesByIds") @ApiOperation("批量删除试验数据管理文档内容") @PreAuthorize("@el.check('del:tsFiles')") - public ResponseResult deleteTsFilesByIds(@RequestParam String id, @RequestParam String type) { - if (StrUtil.isBlank(id)) { + public ResponseResult deleteTsFilesByIds(@RequestParam String ids, @RequestParam String type) { + if (StrUtil.isBlank(ids)) { return ResponseResult.error("参数为空"); } - String[] splitIds = id.split(","); + String[] splitIds = ids.split(","); // 数组转集合 List dataset = Arrays.asList(splitIds); return ResponseResult.success(tsFilesService.deleteTsFilesByIds(dataset, type)); @@ -417,4 +417,48 @@ public class TsFilesController { } + + /** + * 查询文件内容接口 + * + * @param id 文件的id + * @return 文件内容的纯文本(UTF-8 编码) + */ + @Log(module = "实验数据管理", value = "查询文件内容!") + @GetMapping("/api/files/content") + @ApiOperation("查询文件内容") + public ResponseResult getFileContent(@RequestParam String id) { + try { + if (StrUtil.isBlank(id)) { + return ResponseResult.error("参数为空"); + } + String content = tsFilesService.readFileContent(id); + return ResponseResult.successData(content); + } catch (SecurityException | IOException e) { + return ResponseResult.error("非法操作: " + e.getMessage()); + } + } + + /** + * 保存文件内容接口 + * + * @param id 文件的id + * @param content 新的文件内容(HTML/文本) + * @return 操作结果 + */ + @Log(module = "实验数据管理", value = "保存文件内容!") + @PostMapping("/save/files/content") + @ApiOperation("保存文件内容") + public ResponseResult saveFileContent( String id, String content) { + try { + if (StrUtil.isBlank(id) && StrUtil.isBlank(content)) { + return ResponseResult.error("参数为空"); + } + tsFilesService.saveFileContent(id, content); + return ResponseResult.success("文件保存成功"); + } catch (SecurityException | IOException e) { + return ResponseResult.error("非法操作: " + e.getMessage()); + } + } + } diff --git a/java/src/main/java/com/yfd/platform/modules/experimentalData/service/ITsFilesService.java b/java/src/main/java/com/yfd/platform/modules/experimentalData/service/ITsFilesService.java index de57932..7704b3b 100644 --- a/java/src/main/java/com/yfd/platform/modules/experimentalData/service/ITsFilesService.java +++ b/java/src/main/java/com/yfd/platform/modules/experimentalData/service/ITsFilesService.java @@ -161,4 +161,21 @@ public interface ITsFilesService extends IService { TsFiles compareDirectories(List dataset,String nodeId , String taskId); void batchSendNaviOutDataJob(String id, int samTimes,String token); + + /** + * 查询文件内容接口 + * + * @param id 文件的id + * @return 文件内容的纯文本(UTF-8 编码) + */ + String readFileContent(String id) throws IOException; + + /** + * 保存文件内容接口 + * + * @param id 文件的id + * @param content 新的文件内容(HTML/文本) + * @return 操作结果 + */ + void saveFileContent(String id, String content) throws IOException; } diff --git a/java/src/main/java/com/yfd/platform/modules/experimentalData/service/impl/TsFilesServiceImpl.java b/java/src/main/java/com/yfd/platform/modules/experimentalData/service/impl/TsFilesServiceImpl.java index 1752f65..dbb6896 100644 --- a/java/src/main/java/com/yfd/platform/modules/experimentalData/service/impl/TsFilesServiceImpl.java +++ b/java/src/main/java/com/yfd/platform/modules/experimentalData/service/impl/TsFilesServiceImpl.java @@ -49,11 +49,13 @@ import io.netty.handler.codec.compression.CompressionException; import org.apache.commons.compress.archivers.ArchiveEntry; import org.apache.commons.compress.archivers.ArchiveOutputStream; import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry; +import org.apache.commons.compress.archivers.sevenz.SevenZFile; import org.apache.commons.compress.archivers.sevenz.SevenZOutputFile; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream; @@ -307,9 +309,9 @@ public class TsFilesServiceImpl extends ServiceImpl impl if (StringUtils.isNotBlank(id)) { queryWrapper.eq("parent_id", id); } -// if (StringUtils.isNotBlank(path)) { -// queryWrapper.eq("work_path", path); -// } + if (StringUtils.isNotBlank(path)) { + queryWrapper.eq("work_path", path); + } if (StringUtils.isNotBlank(nodeId)) { queryWrapper.eq("node_id", nodeId); } @@ -372,7 +374,7 @@ public class TsFilesServiceImpl extends ServiceImpl impl // //判断文件夹是否创建 // AbstractBaseFileService fileService = storageSourceContext.getByStorageKey("local"); // boolean flag = fileService.isFolderCreated(File.separator + tsFiles.getNodeId()); -// //如果是false 说明没有创建 那就新建一个文件夹 + //如果是false 说明没有创建 那就新建一个文件夹 // if(!flag){ // //本地创建文件夹 // NewFolderRequest newFolderRequest = new NewFolderRequest(); @@ -1512,244 +1514,6 @@ public class TsFilesServiceImpl extends ServiceImpl impl } - /** - * 检查压缩文件是否包含文件夹(主入口方法) - * - * @param filePath 压缩文件路径 - * @return 是否包含文件夹 - */ - public boolean hasFolder(Path filePath) throws IOException { - String fileName = filePath.getFileName().toString().toLowerCase(); - QueryWrapper queryWrapperSysDictionary = new QueryWrapper<>(); - queryWrapperSysDictionary.eq("parentcode", "compressType"); - queryWrapperSysDictionary.orderByAsc("orderno"); - List sysDictionaryItems = sysDictionaryItemsMapper.selectList(queryWrapperSysDictionary); - // 遍历所有支持的压缩后缀进行匹配 - for (SysDictionaryItems sys : sysDictionaryItems) { - if (fileName.endsWith("." + sys.getDictName())) { - return checkBySuffix(filePath, sys.getDictName()); - } - } - throw new IOException("不支持的压缩格式: " + fileName); - } - - /** - * 初始化压缩后缀配置 - * - * @param sysDictionaryItems 数据库查询结果集 - */ - public void initCompressTypes(List sysDictionaryItems) { - // 按后缀长度降序排序(确保优先匹配长后缀如.tar.gz) - this.compressSuffixes = sysDictionaryItems.stream() - .map(item -> item.getDictName().toLowerCase()) - .sorted((s1, s2) -> Integer.compare(s2.length(), s1.length())) - .collect(Collectors.toList()); - } - - /** - * 根据后缀分派检查方法 - * - * @param path 文件路径 - * @param suffix 压缩后缀(不带点) - */ - private boolean checkBySuffix(Path path, String suffix) throws IOException { - switch (suffix) { - case "zip": - return checkZip(path); - case "tar": - return checkTar(path); - case "gz": - return checkGzip(path); - case "bz2": - return checkBzip2(path); - case "rar": - return checkRar(path); - case "tar.gz": - return checkTarGz(path); - case "tar.bz2": - return checkTarBz2(path); - default: - throw new IOException("未实现的格式检查: " + suffix); - } - } - - - /******************** ZIP格式检查 ********************/ - /** - * 检查ZIP文件是否包含目录 - * - * @param zipFilePath ZIP文件路径 - * @return 是否包含目录 - */ - private boolean checkZip(Path zipFilePath) throws IOException { - try (ZipFile zipFile = new ZipFile(zipFilePath.toFile())) { - Enumeration entries = zipFile.entries(); - while (entries.hasMoreElements()) { - ZipEntry entry = entries.nextElement(); - if (entry.isDirectory()) { - return true; - } - } - return false; - } - } - - /******************** TAR格式检查 ********************/ - /** - * 检查TAR文件是否包含目录 - * - * @param tarFilePath TAR文件路径 - * @return 是否包含目录 - */ - private boolean checkTar(Path tarFilePath) throws IOException { - try (TarArchiveInputStream tis = new TarArchiveInputStream(Files.newInputStream(tarFilePath))) { - TarArchiveEntry entry; - while ((entry = tis.getNextTarEntry()) != null) { - if (entry.isDirectory()) { - return true; - } - } - return false; - } - } - - /******************** GZIP格式检查 ********************/ - /** - * 检查GZ文件是否包含目录(可能包含嵌套TAR) - * - * @param gzFilePath GZ文件路径 - * @return 是否包含目录 - */ - private boolean checkGzip(Path gzFilePath) throws IOException { - // 检查是否为TAR.GZ格式 - try (InputStream is = Files.newInputStream(gzFilePath); - GzipCompressorInputStream gzis = new GzipCompressorInputStream(is)) { - - // 读取头部判断是否嵌套TAR - byte[] header = new byte[512]; - if (gzis.read(header) >= 512 && isTarHeader(header)) { - return checkTar(new ByteArrayInputStream(header, 0, 512), gzis); - } - return false; // 纯GZ文件不含目录 - } - } - - /******************** TAR.GZ复合格式检查 ********************/ - /** - * 检查TAR.GZ文件是否包含目录 - * - * @param tgzFilePath TAR.GZ文件路径 - * @return 是否包含目录 - */ - private boolean checkTarGz(Path tgzFilePath) throws IOException { - try (InputStream is = Files.newInputStream(tgzFilePath); - GzipCompressorInputStream gzis = new GzipCompressorInputStream(is); - TarArchiveInputStream tis = new TarArchiveInputStream(gzis)) { - - return checkTarEntries(tis); - } - } - - /******************** BZIP2格式检查 ********************/ - /** - * 检查BZ2文件是否包含目录(可能包含嵌套TAR) - * - * @param bz2FilePath BZ2文件路径 - * @return 是否包含目录 - */ - private boolean checkBzip2(Path bz2FilePath) throws IOException { - try (InputStream is = Files.newInputStream(bz2FilePath); - BZip2CompressorInputStream bz2is = new BZip2CompressorInputStream(is)) { - - // 读取头部判断是否嵌套TAR - byte[] header = new byte[512]; - if (bz2is.read(header) >= 512 && isTarHeader(header)) { - return checkTar(new ByteArrayInputStream(header, 0, 512), bz2is); - } - return false; - } - } - - /******************** TAR.BZ2复合格式检查 ********************/ - /** - * 检查TAR.BZ2文件是否包含目录 - * - * @param tbz2FilePath TAR.BZ2文件路径 - * @return 是否包含目录 - */ - private boolean checkTarBz2(Path tbz2FilePath) throws IOException { - try (InputStream is = Files.newInputStream(tbz2FilePath); - BZip2CompressorInputStream bz2is = new BZip2CompressorInputStream(is); - TarArchiveInputStream tis = new TarArchiveInputStream(bz2is)) { - - return checkTarEntries(tis); - } - } - - /******************** RAR格式检查 ********************/ - /** - * 检查RAR文件是否包含目录 - * - * @param rarFilePath RAR文件路径 - * @return 是否包含目录 - */ - private boolean checkRar(Path rarFilePath) throws IOException { - try (Archive archive = new Archive(rarFilePath.toFile())) { - FileHeader fileHeader; - while ((fileHeader = archive.nextFileHeader()) != null) { - if (fileHeader.isDirectory()) { - return true; - } - } - return false; - } catch (Exception e) { - throw new IOException("解析RAR文件失败", e); - } - } - - /******************** 通用工具方法 ********************/ - /** - * 检查TAR流是否包含目录 - * - * @param tis TAR输入流 - * @return 是否包含目录 - */ - private boolean checkTarEntries(TarArchiveInputStream tis) throws IOException { - TarArchiveEntry entry; - while ((entry = tis.getNextTarEntry()) != null) { - if (entry.isDirectory()) { - return true; - } - } - return false; - } - - /** - * 判断字节流是否为TAR头部 - * - * @param header 512字节头部数据 - * @return 是否TAR格式 - */ - private boolean isTarHeader(byte[] header) { - if (header.length < 263) return false; - String magic = new String(header, 257, 6, StandardCharsets.US_ASCII); - return magic.startsWith("ustar"); - } - - /** - * 合并头部和剩余流检查TAR - * - * @param headerStream 头部字节流 - * @param remainingStream 剩余数据流 - */ - private boolean checkTar(InputStream headerStream, InputStream remainingStream) throws IOException { - try (TarArchiveInputStream tis = new TarArchiveInputStream( - new SequenceInputStream(headerStream, remainingStream))) { - return checkTarEntries(tis); - } - } - - /** * 创建文件记录 */ @@ -1891,217 +1655,316 @@ public class TsFilesServiceImpl extends ServiceImpl impl } - // 从字典表获取支持的压缩文件后缀列表 - private List getSupportedCompressionSuffixes() { - QueryWrapper queryWrapperSysDictionary = new QueryWrapper<>(); - queryWrapperSysDictionary.eq("parentcode", "compressType"); - queryWrapperSysDictionary.orderByAsc("orderno"); - List sysDictionaryItems = sysDictionaryItemsMapper.selectList(queryWrapperSysDictionary); - // 提取后缀列表(例如 ["gz", "xz", "rar", "tar", "zip", "bz2", "tar.gz"]) - return sysDictionaryItems.stream() - .map(SysDictionaryItems::getDictName) - .collect(Collectors.toList()); - } - /** - * 解压 ZIP 文件到临时目录 - * - * @param zipFilePath ZIP 文件路径 - * @param baseDir 基础解压目录(如 /var/storage) - * @return 解压后的根目录 File 对象 - */ - //这个方法会直接覆盖 下面那个不会 那个会报错 TODO - // 1. 检测压缩包是否包含文件夹结构(目录或路径分隔符) + + + + + + + + + + // 新增:统一解压入口方法(保持原有逻辑结构) private File unzipToTemp(Path zipFilePath, String baseDir) throws IOException { - Path destRoot = Paths.get(baseDir); - Files.createDirectories(destRoot); + String fileName = zipFilePath.getFileName().toString(); + String ext = getFileExtension(fileName).toLowerCase(); - // 获取支持的压缩后缀列表 - List supportedSuffixes = getSupportedCompressionSuffixes(); - - // 根据文件扩展名匹配支持的压缩格式(优先匹配长后缀,如 .tar.gz) - String fileName = zipFilePath.getFileName().toString().toLowerCase(); - String matchedSuffix = supportedSuffixes.stream() - .filter(suffix -> fileName.endsWith("." + suffix)) - .max(Comparator.comparingInt(String::length)) // 优先匹配最长后缀(如 .tar.gz) - .orElseThrow(() -> new IOException("Unsupported compression format: " + fileName)); - - // 根据后缀选择解压方式 - switch (matchedSuffix) { - case "zip": - return unzipToTemp(zipFilePath, destRoot); - case "gz": - return decompressGzip(zipFilePath, destRoot); -// case "bz2": -// return decompressBzip2(zipFilePath, destRoot); -// case "xz": -// return decompressXz(zipFilePath, destRoot); - case "tar": - return decompressTar(zipFilePath, destRoot); - case "tar.gz": - return decompressTarGz(zipFilePath, destRoot); -// case "tar.bz2": -// return decompressTarBz2(zipFilePath, destRoot); - case "rar": - return decompressRar(zipFilePath, destRoot); - default: - throw new IOException("Unsupported compression format: " + matchedSuffix); + // 根据文件类型调用不同解压实现 + if (ext.equals(".zip")) { + return unzipFile(zipFilePath, baseDir); + } else if (fileName.endsWith(".tar.gz")) { + return unTarGzFile(zipFilePath, baseDir); + } else if (ext.equals(".7z")) { + return un7zFile(zipFilePath, baseDir); + } else { + throw new IllegalArgumentException("不支持的压缩格式: " + ext); } } - /** - * 解压 ZIP 文件 - */ - private File unzipToTemp(Path zipFilePath, Path destRoot) throws IOException { - String zipName = getFileNameWithoutExtension(zipFilePath.getFileName().toString()); + // 修改后的判断是否包含文件夹方法(支持多格式) + private boolean hasFolder(Path zipFilePath) throws IOException { + String fileName = zipFilePath.getFileName().toString(); + String ext = getFileExtension(fileName).toLowerCase(); + + if (ext.equals(".zip")) { + return hasFolderInZip(zipFilePath); + } else if (fileName.endsWith(".tar.gz")) { + return hasFolderInTarGz(zipFilePath); + } else if (ext.equals(".7z")) { + return hasFolderIn7z(zipFilePath); + } else { + throw new IllegalArgumentException("不支持的压缩格式: " + ext); + } + } + + + + // 保持原有ZIP判断方法不变 + private boolean hasFolderInZip(Path zipFilePath) throws IOException { + try (ZipFile zipFile = new ZipFile(zipFilePath.toFile())) { + return zipFile.stream() + .anyMatch(entry -> entry.isDirectory()); + } + } + + // 新增:TAR.GZ格式文件夹判断 + private boolean hasFolderInTarGz(Path filePath) throws IOException { + try (InputStream fi = Files.newInputStream(filePath); + InputStream gi = new GzipCompressorInputStream(fi); + TarArchiveInputStream ti = new TarArchiveInputStream(gi)) { + + TarArchiveEntry entry; + while ((entry = ti.getNextTarEntry()) != null) { + if (entry.isDirectory()) { + return true; + } + } + return false; + } + } + + // 新增:7Z格式文件夹判断 + private boolean hasFolderIn7z(Path filePath) throws IOException { + try (SevenZFile szFile = new SevenZFile(filePath.toFile())) { + SevenZArchiveEntry entry; + while ((entry = szFile.getNextEntry()) != null) { + if (entry.isDirectory()) { + return true; + } + } + return false; + } + } + + // 保持原有解压逻辑结构的ZIP实现 + // 修改后的ZIP解压方法(修复解压问题) + private File unzipFile(Path sourcePath, String baseDir) throws IOException { + Path destRoot = Paths.get(baseDir); + Files.createDirectories(destRoot); + + // 关键修复1:使用与压缩时相同的字符集 + try (ZipInputStream zis = new ZipInputStream( + Files.newInputStream(sourcePath), + StandardCharsets.UTF_8)) { + + // 关键修复2:记录已处理路径防止重复 + Set processedEntries = new HashSet<>(); - try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(zipFilePath))) { ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { - // 过滤 MacOS 元数据目录 - if (entry.getName().startsWith("__MACOSX/")) { - zis.closeEntry(); - continue; - } - // 剥离 ZIP 根目录(例如 ZIP 内有一个根目录 "example/") String entryName = entry.getName(); - if (entryName.startsWith(zipName + "/")) { - entryName = entryName.substring(zipName.length() + 1); - } - // 跳过空路径 - if (entryName.isEmpty()) { + + // 过滤系统文件(与压缩时逻辑对应) + if (entryName.startsWith("__MACOSX/")) { zis.closeEntry(); continue; } - // 构建安全路径并写入文件 + + // 关键修复3:路径标准化处理 + entryName = entryName.replace("\\", "/"); // 统一分隔符 + if (processedEntries.contains(entryName)) { + continue; + } + processedEntries.add(entryName); + + // 构建目标路径 Path destPath = destRoot.resolve(entryName).normalize(); - validatePathSafety(destPath, destRoot); + validatePathSafetya(destPath, destRoot); + + // 日志跟踪 + LOGGER.debug("[解压] 处理条目:{} → {}", entryName, destPath); if (entry.isDirectory()) { Files.createDirectories(destPath); + LOGGER.debug("创建目录:{}", destPath); } else { - Files.createDirectories(destPath.getParent()); - writeFile(zis, destPath); + // 关键修复4:确保父目录存在 + Path parentDir = destPath.getParent(); + if (!Files.exists(parentDir)) { + Files.createDirectories(parentDir); + LOGGER.debug("创建父目录:{}", parentDir); + } + + // 关键修复5:使用与压缩时相同的写入方式 + try (BufferedOutputStream bos = new BufferedOutputStream( + Files.newOutputStream(destPath, + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING))) { + + byte[] buffer = new byte[8192]; + int len; + while ((len = zis.read(buffer)) > 0) { + bos.write(buffer, 0, len); + } + // 立即刷新到磁盘 + bos.flush(); + LOGGER.info("已解压文件:{} (大小:{} bytes)", + destPath, Files.size(destPath)); + } } zis.closeEntry(); } } + + // 最终验证 + LOGGER.info("解压完成,验证文件结构:"); + Files.walk(destRoot) + .forEach(path -> LOGGER.info("-> {}", path)); + return destRoot.toFile(); } /** - * 解压 GZIP 文件(假设是单文件压缩) + * 路径安全验证(增强版) */ - private File decompressGzip(Path gzipFilePath, Path destRoot) throws IOException { - String fileName = getFileNameWithoutExtension(gzipFilePath.getFileName().toString()); - Path outputFile = destRoot.resolve(fileName); + private void validatePathSafetya(Path targetPath, Path destRoot) throws IOException { + Path normalizedDest = destRoot.toAbsolutePath().normalize(); + Path normalizedTarget = targetPath.toAbsolutePath().normalize(); - try (InputStream is = Files.newInputStream(gzipFilePath); - GZIPInputStream gzis = new GZIPInputStream(is); - OutputStream os = Files.newOutputStream(outputFile)) { - byte[] buffer = new byte[1024]; - int len; - while ((len = gzis.read(buffer)) > 0) { - os.write(buffer, 0, len); - } + if (!normalizedTarget.startsWith(normalizedDest)) { + throw new IOException("路径安全验证失败:" + normalizedTarget + + " 超出根目录 " + normalizedDest); } - return outputFile.toFile(); } - - /** - * 解压 TAR 文件 - */ - private File decompressTar(Path tarFilePath, Path destRoot) throws IOException { - try (TarArchiveInputStream tais = new TarArchiveInputStream(Files.newInputStream(tarFilePath))) { - TarArchiveEntry entry; - while ((entry = tais.getNextTarEntry()) != null) { - // 过滤无效文件 - if (entry.getName().startsWith("__MACOSX/")) continue; - - Path destPath = destRoot.resolve(entry.getName()).normalize(); - validatePathSafety(destPath, destRoot); - - if (entry.isDirectory()) { - Files.createDirectories(destPath); - } else { - Files.createDirectories(destPath.getParent()); - writeFile(tais, destPath); + /** + * 安全写入文件(带缓冲) + */ + private void writeFile(ZipInputStream zis, Path destPath) throws IOException { + try (BufferedOutputStream bos = new BufferedOutputStream( + Files.newOutputStream(destPath, + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING + ) + )) { + byte[] buffer = new byte[8192]; + int len; + while ((len = zis.read(buffer)) > 0) { + bos.write(buffer, 0, len); } } } - return destRoot.toFile(); - } - /** - * 解压 TAR.GZ 文件 - */ - private File decompressTarGz(Path tarGzFilePath, Path destRoot) throws IOException { - try (InputStream is = Files.newInputStream(tarGzFilePath); - GZIPInputStream gzis = new GZIPInputStream(is); - TarArchiveInputStream tais = new TarArchiveInputStream(gzis)) { + /** + * 内部类:存储 ZIP 条目信息 + */ + private static class ZipEntryInfo { + private final String name; + private final boolean isDirectory; - TarArchiveEntry entry; - while ((entry = tais.getNextTarEntry()) != null) { - // 过滤无效文件 - if (entry.getName().startsWith("__MACOSX/")) continue; + public ZipEntryInfo(String name, boolean isDirectory) { + this.name = name; + this.isDirectory = isDirectory; + } - Path destPath = destRoot.resolve(entry.getName()).normalize(); - validatePathSafety(destPath, destRoot); + public String getName() { + return name; + } - if (entry.isDirectory()) { - Files.createDirectories(destPath); - } else { - Files.createDirectories(destPath.getParent()); - writeFile(tais, destPath); - } + public boolean isDirectory() { + return isDirectory; } } - return destRoot.toFile(); - } - /** - * 解压 RAR 文件(需要 junrar 依赖) - */ - private File decompressRar(Path rarFilePath, Path destRoot) throws IOException { - try (Archive archive = new Archive(rarFilePath.toFile())) { - FileHeader fileHeader; - while ((fileHeader = archive.nextFileHeader()) != null) { - // 过滤无效文件 - if (fileHeader.getFileName().startsWith("__MACOSX/")) continue; - Path destPath = destRoot.resolve(fileHeader.getFileName()).normalize(); - validatePathSafety(destPath, destRoot); - if (fileHeader.isDirectory()) { - Files.createDirectories(destPath); + // 新增:TAR.GZ解压实现(保持相同路径逻辑) + private File unTarGzFile(Path sourcePath, String baseDir) throws IOException { + Path destRoot = Paths.get(baseDir); + Files.createDirectories(destRoot); + + try (InputStream fi = Files.newInputStream(sourcePath); + InputStream gi = new GzipCompressorInputStream(fi); + TarArchiveInputStream ti = new TarArchiveInputStream(gi)) { + + TarArchiveEntry entry; + while ((entry = ti.getNextTarEntry()) != null) { + // 保持相同过滤逻辑 + if (entry.getName().startsWith("__MACOSX/")) { + continue; + } + + Path targetPath = destRoot.resolve(entry.getName()).normalize(); + validatePathSafety(targetPath, destRoot); + + if (entry.isDirectory()) { + Files.createDirectories(targetPath); } else { - Files.createDirectories(destPath.getParent()); - try (OutputStream os = Files.newOutputStream(destPath)) { - archive.extractFile(fileHeader, os); + Files.createDirectories(targetPath.getParent()); + try (OutputStream os = Files.newOutputStream(targetPath)) { + IOUtils.copy(ti, os); } } } - } catch (RarException e) { - throw new IOException("Failed to decompress RAR file", e); } return destRoot.toFile(); } - /** - * 将输入流写入目标文件 - */ - private void writeFile(InputStream is, Path destPath) throws IOException { - try (OutputStream os = Files.newOutputStream(destPath)) { - byte[] buffer = new byte[1024]; - int len; - while ((len = is.read(buffer)) > 0) { - os.write(buffer, 0, len); + // 新增:7Z解压实现(保持相同路径逻辑) + private File un7zFile(Path sourcePath, String baseDir) throws IOException { + Path destRoot = Paths.get(baseDir); + Files.createDirectories(destRoot); + + try (SevenZFile szFile = new SevenZFile(sourcePath.toFile())) { + SevenZArchiveEntry entry; + while ((entry = szFile.getNextEntry()) != null) { + // 保持相同过滤逻辑 + if (entry.getName().startsWith("__MACOSX/")) { + continue; + } + + Path targetPath = destRoot.resolve(entry.getName()).normalize(); + validatePathSafety(targetPath, destRoot); + + if (entry.isDirectory()) { + Files.createDirectories(targetPath); + } else { + Files.createDirectories(targetPath.getParent()); + try (OutputStream os = Files.newOutputStream(targetPath)) { + byte[] buffer = new byte[8192]; + int len; + while ((len = szFile.read(buffer)) > 0) { + os.write(buffer, 0, len); + } + } + } } } + return destRoot.toFile(); } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + // /** // * 解析压缩包条目(支持多种格式) // */ @@ -2151,73 +2014,7 @@ public class TsFilesServiceImpl extends ServiceImpl impl // return entries; // } - /** - * 解析压缩包条目,过滤无效文件(如 __MACOSX) - */ - private List parseZipEntries(Path zipFilePath) throws IOException { - List entries = new ArrayList<>(); - try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFilePath.toFile()))) { - ZipEntry entry; - while ((entry = zis.getNextEntry()) != null) { - if (!entry.getName().startsWith("__MACOSX/")) { - entries.add(new ZipEntryInfo(entry.getName(), entry.isDirectory())); - } - zis.closeEntry(); - } - } - return entries; - } - /** - * 判断是否为单文件结构 - */ - private boolean isSingleFileStructure(List entries) { - // 有效条目数量为1,且是文件(非目录),且不包含路径分隔符 - if (entries.size() == 1) { - ZipEntryInfo entry = entries.get(0); - return !entry.isDirectory() && !entry.getName().contains("/"); - } - return false; - } - - /** - * 安全写入文件(带缓冲) - */ - private void writeFile(ZipInputStream zis, Path destPath) throws IOException { - try (BufferedOutputStream bos = new BufferedOutputStream( - Files.newOutputStream(destPath, - StandardOpenOption.CREATE, - StandardOpenOption.TRUNCATE_EXISTING - ) - )) { - byte[] buffer = new byte[8192]; - int len; - while ((len = zis.read(buffer)) > 0) { - bos.write(buffer, 0, len); - } - } - } - - /** - * 内部类:存储 ZIP 条目信息 - */ - private static class ZipEntryInfo { - private final String name; - private final boolean isDirectory; - - public ZipEntryInfo(String name, boolean isDirectory) { - this.name = name; - this.isDirectory = isDirectory; - } - - public String getName() { - return name; - } - - public boolean isDirectory() { - return isDirectory; - } - } //上面这个方法会覆盖 TODO @@ -4034,7 +3831,11 @@ public class TsFilesServiceImpl extends ServiceImpl impl LOGGER.info("查询minio树的时候" + path); AbstractBaseFileService fileService = storageSourceContext.getByStorageKey("minio"); FileItemResult fileItemResult = fileService.getFileItem(path); - if (fileItemResult != null || fileItemResult.getName() != null) { + if (fileItemResult == null) { + dto.setUrl(null); + dto.setType(null); + + }else{ dto.setUrl(fileItemResult.getUrl()); //如果是压缩文件 类型就给zip boolean isValid = hasValidExtension(fileItemResult.getName(), sysDictionaryItems); @@ -4130,6 +3931,8 @@ public class TsFilesServiceImpl extends ServiceImpl impl } + + // 封装发送数据的逻辑 private void sendData(String token, String[] values, int lineCount) { if (Thread.currentThread().isInterrupted()) { @@ -4164,6 +3967,81 @@ public class TsFilesServiceImpl extends ServiceImpl impl private String getValueSafely(String[] values, int index, String defaultValue) { return (index < values.length) ? values[index] : defaultValue; } + + + /** + * 查询文件内容接口 + * + * @param id 文件的id + * @return 文件内容的纯文本(UTF-8 编码) + */ + @Override + public String readFileContent(String id) throws IOException { + StorageSourceConfig config = storageSourceConfigMapper.selectOne(new QueryWrapper().eq("name", "filePath")); + TsFiles tsFiles = tsFilesMapper.selectById(id); + // 1. 路径标准化与安全校验 + Path targetPath = validateAndNormalizePath(config.getValue()+tsFiles.getWorkPath()+tsFiles.getFileName()); + + StringBuilder content = new StringBuilder(); + + // 使用缓冲流读取大文件(减少内存占用) + try (BufferedReader reader = Files.newBufferedReader(targetPath, StandardCharsets.UTF_8)) { + String line; + while ((line = reader.readLine()) != null) { + content.append(line).append("\n"); + } + } + return content.toString().trim(); // 移除末尾多余换行 + } + + + + /** + * 验证路径是否合法并转换为标准化路径 + */ + private Path validateAndNormalizePath(String filePath) throws SecurityException { + StorageSourceConfig config = storageSourceConfigMapper.selectOne(new QueryWrapper().eq("name", "filePath")); + + Path targetPath = Paths.get(filePath).normalize(); + Path baseDirPath = Paths.get(config.getValue()); + + if (!targetPath.startsWith(baseDirPath)) { + throw new SecurityException("路径越界: " + targetPath); + } + // 防止路径遍历攻击(如 workPath 包含 "..") + if (targetPath.toString().contains("..")) { + throw new SecurityException("非法路径: " + targetPath); + } + + return targetPath; + } + + + /** + * 保存文件内容 + * + * @param id 文件的id + * @param content 新的文件内容(HTML/文本) + * @throws SecurityException 路径非法时抛出 + * @throws IOException 文件写入失败时抛出 + */ + public void saveFileContent(String id, String content) throws SecurityException, IOException { + StorageSourceConfig config = storageSourceConfigMapper.selectOne(new QueryWrapper().eq("name", "filePath")); + TsFiles tsFiles = tsFilesMapper.selectById(id); + // 1. 路径标准化与安全校验 + Path targetPath = validateAndNormalizePath(config.getValue()+tsFiles.getWorkPath()+tsFiles.getFileName()); + // 2. 确保父目录存在 + Path parentDir = targetPath.getParent(); + if (parentDir != null && !Files.exists(parentDir)) { + Files.createDirectories(parentDir); + } + // 3. 写入文件内容(UTF-8 编码) + // 使用缓冲流写入大文件(支持流式上传) + try (BufferedWriter writer = Files.newBufferedWriter(targetPath, StandardCharsets.UTF_8)) { + writer.write(content); + } + } + } diff --git a/java/src/main/java/com/yfd/platform/modules/experimentalData/service/impl/TsNodesServiceImpl.java b/java/src/main/java/com/yfd/platform/modules/experimentalData/service/impl/TsNodesServiceImpl.java index ff396b3..8c7aa6a 100644 --- a/java/src/main/java/com/yfd/platform/modules/experimentalData/service/impl/TsNodesServiceImpl.java +++ b/java/src/main/java/com/yfd/platform/modules/experimentalData/service/impl/TsNodesServiceImpl.java @@ -273,7 +273,8 @@ public class TsNodesServiceImpl extends ServiceImpl impl //判断节点名称是否存在 QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("node_name", tsnodes.getNodeName());//名称 - queryWrapper.eq("parent_id", tsnodes.getParentId());//父节点 + queryWrapper.eq("task_id", tsnodes.getTaskId());//所属任务ID + queryWrapper.eq("parent_id", tsnodes.getParentId());//所属任务ID int count = tsNodesMapper.selectCount(queryWrapper); // 大于0说明 区域名称重复 if (count > 0) { diff --git a/java/src/main/java/com/yfd/platform/modules/storage/service/base/AbstractS3BaseFileService.java b/java/src/main/java/com/yfd/platform/modules/storage/service/base/AbstractS3BaseFileService.java index 9835e7d..df32b8b 100644 --- a/java/src/main/java/com/yfd/platform/modules/storage/service/base/AbstractS3BaseFileService.java +++ b/java/src/main/java/com/yfd/platform/modules/storage/service/base/AbstractS3BaseFileService.java @@ -395,20 +395,30 @@ public abstract class AbstractS3BaseFileService

extends A @Override public FileItemResult getFileItem(String pathAndName) { - String fileName = FileUtil.getName(pathAndName); - String parentPath = StringUtils.getParentPath(pathAndName); + try { + String fileName = FileUtil.getName(pathAndName); + String parentPath = StringUtils.getParentPath(pathAndName); + String trimStartPath = StringUtils.concatTrimStartSlashes(param.getBasePath(), pathAndName); - String trimStartPath = StringUtils.concatTrimStartSlashes(param.getBasePath(), pathAndName); - ObjectMetadata objectMetadata = s3Client.getObjectMetadata(param.getBucketName(), trimStartPath); + // 可能抛出异常的代码包裹在try块中 + ObjectMetadata objectMetadata; + try { + objectMetadata = s3Client.getObjectMetadata(param.getBucketName(), trimStartPath); + } catch (Exception e) { // 捕获所有类型的异常 + return null; // 任何异常都返回null + } - FileItemResult fileItemResult = new FileItemResult(); - fileItemResult.setName(fileName); - fileItemResult.setSize(objectMetadata.getInstanceLength()); - fileItemResult.setTime(objectMetadata.getLastModified()); - fileItemResult.setType(FileTypeEnum.FILE); - fileItemResult.setPath(parentPath); - fileItemResult.setUrl(getDownloadUrl(pathAndName)); - return fileItemResult; + FileItemResult fileItemResult = new FileItemResult(); + fileItemResult.setName(fileName); + fileItemResult.setSize(objectMetadata.getInstanceLength()); + fileItemResult.setTime(objectMetadata.getLastModified()); + fileItemResult.setType(FileTypeEnum.FILE); + fileItemResult.setPath(parentPath); + fileItemResult.setUrl(getDownloadUrl(pathAndName)); + return fileItemResult; + } catch (Exception e) { + return null; // 其他代码异常也返回null + } } @Override