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 2a6037c..194a854 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 @@ -64,7 +64,10 @@ public class TsFilesController { /********************************** * 用途说明: 查询实验数据管理文件夹 - * 参数说明 + * 参数说明 id 父级ID + * 参数说明 path 路径 + * 参数说明nodeId 节点ID + * 参数说明taskId 任务ID * pageNum 当前页 * 返回值说明: com.yfd.platform.config.ResponseResult 返回分页查询结果 ***********************************/ @@ -72,9 +75,9 @@ public class TsFilesController { @GetMapping("/listTsFiles") @ApiOperation("查询实验数据管理文件夹") @PreAuthorize("@el.check('select:tsfiles')") - public ResponseResult getsListTsFiles(String id, String path) throws Exception { + public ResponseResult getsListTsFiles(String id, String path,String nodeId,String taskId) throws Exception { //分页查询 - List tsfiles = tsFilesService.getsListTsFiles(id, path); + List tsfiles = tsFilesService.getsListTsFiles(id, path,nodeId,taskId); return ResponseResult.successData(tsfiles); } @@ -94,12 +97,9 @@ public class TsFilesController { if (ObjUtil.isEmpty(tsFiles)) { return ResponseResult.error("参数为空"); } - Boolean isOk = tsFilesService.addTsFiles(tsFiles); - if (isOk) { - return ResponseResult.success(); - } else { - return ResponseResult.error(); - } + + return tsFilesService.addTsFiles(tsFiles); + } /*********************************** @@ -303,19 +303,19 @@ public class TsFilesController { /** * 对比两个目录的文件差异 * - * @param ids 勾选的所有数据ID集合 + * @param id 勾选的所有数据ID集合 * @return 文件差异列表 */ @Log(module = "实验数据管理", value = "对比本地和minio的文件差异!") @PostMapping("/compare") @ApiOperation("对比两个目录的文件差异") - public ResponseResult compareDirectories( String ids, String nodeId, String taskId) { + public ResponseResult compareDirectories( String id, String nodeId, String taskId) { try { List dataset = new ArrayList<>(); - if (StrUtil.isNotEmpty(ids)) { - String[] splitIds = ids.split(","); + if (StrUtil.isNotEmpty(id)) { + String[] splitIds = id.split(","); // 数组转集合 dataset = Arrays.asList(splitIds); } 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 b3c7a0e..9436b6c 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 @@ -44,7 +44,7 @@ public interface ITsFilesService extends IService { * TsFiles 文档内容 * 返回值说明: com.yfd.platform.config.ResponseResult 返回新增成功或者失败 ***********************************/ - Boolean addTsFiles(TsFiles tsFiles); + ResponseResult addTsFiles(TsFiles tsFiles); /********************************** * 用途说明: 修改试验数据管理-文档内容 @@ -117,7 +117,7 @@ public interface ITsFilesService extends IService { * pageNum 当前页 * 返回值说明: com.yfd.platform.config.ResponseResult 返回分页查询结果 ***********************************/ - List getsListTsFiles(String id,String path); + List getsListTsFiles(String id,String path,String nodeId,String taskId); 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 23a2503..7d6dffc 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 @@ -4,7 +4,6 @@ import cn.hutool.core.collection.CollUtil; import java.nio.file.*; -import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.*; @@ -18,48 +17,46 @@ import com.amazonaws.services.s3.model.S3ObjectInputStream; import com.amazonaws.util.IOUtils; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.qiniu.storage.model.FileInfo; import com.yfd.platform.config.ResponseResult; import com.yfd.platform.modules.experimentalData.domain.*; import com.yfd.platform.modules.experimentalData.mapper.TsFilesMapper; -import com.yfd.platform.modules.experimentalData.mapper.TsNodesMapper; import com.yfd.platform.modules.experimentalData.service.ITsFilesService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.yfd.platform.modules.storage.chain.FileChain; import com.yfd.platform.modules.storage.context.StorageSourceContext; import com.yfd.platform.modules.storage.mapper.StorageSourceConfigMapper; -import com.yfd.platform.modules.storage.mapper.StorageSourceMapper; import com.yfd.platform.modules.storage.model.entity.StorageSourceConfig; import com.yfd.platform.modules.storage.model.enums.FileTypeEnum; import com.yfd.platform.modules.storage.model.request.BatchDeleteRequest; import com.yfd.platform.modules.storage.model.request.NewFolderRequest; import com.yfd.platform.modules.storage.model.request.RenameFileRequest; import com.yfd.platform.modules.storage.model.result.FileItemResult; -import com.yfd.platform.modules.storage.service.StorageSourceService; import com.yfd.platform.modules.storage.service.base.AbstractBaseFileService; import com.yfd.platform.system.domain.LoginUser; import com.yfd.platform.utils.StringUtils; import io.netty.channel.ChannelInboundHandlerAdapter; -import net.sf.jsqlparser.expression.LongValue; -import net.sf.jsqlparser.expression.StringValue; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; -import org.apache.poi.ss.formula.functions.T; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.bind.annotation.RequestParam; import javax.annotation.Resource; import java.io.*; import java.sql.Timestamp; import java.time.LocalDateTime; import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; +import org.apache.commons.codec.binary.Hex; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + + /** *

* 试验任务文档表 服务实现类 @@ -77,25 +74,9 @@ public class TsFilesServiceImpl extends ServiceImpl impl @Resource private TsFilesMapper tsFilesMapper; - //试验任务节点表 Mapper - @Resource - private TsNodesMapper tsNodesMapper; - - - //数据源Mapper - @Resource - private StorageSourceMapper storageSourceMapper; - - @Resource private StorageSourceContext storageSourceContext; - @Resource - private StorageSourceService storageSourceService; - - @Resource - private FileChain fileChain; - @Resource private StorageSourceConfigMapper storageSourceConfigMapper; @@ -218,7 +199,7 @@ public class TsFilesServiceImpl extends ServiceImpl impl return false; // 如果传入的文件名为空,返回 false } // 判断文件名后缀是否是 .zip 或 .tar - return name.endsWith(".zip") || name.endsWith(".tar"); + return name.endsWith(".zip") || name.endsWith(".tar") || name.endsWith(".rar")|| name.endsWith(".xz")|| name.endsWith(".tar.gz")|| name.endsWith(".gz")|| name.endsWith(".bz2"); } // 递归获取所有子节点ID(包含隔代子节点) @@ -241,7 +222,7 @@ public class TsFilesServiceImpl extends ServiceImpl impl * 返回值说明: com.yfd.platform.config.ResponseResult 返回分页查询结果 ***********************************/ @Override - public List getsListTsFiles(String id, String path) { + public List getsListTsFiles(String id, String path, String nodeId, String taskId) { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("is_file", "FOLDER"); if (StringUtils.isNotBlank(id)) { @@ -250,6 +231,12 @@ public class TsFilesServiceImpl extends ServiceImpl impl if (StringUtils.isNotBlank(path)) { queryWrapper.eq("work_path", path); } + if (StringUtils.isNotBlank(nodeId)) { + queryWrapper.eq("node_id", nodeId); + } + if (StringUtils.isNotBlank(taskId)) { + queryWrapper.eq("task_id", taskId); + } queryWrapper.isNotNull("work_path"); List tsFiles = tsFilesMapper.selectList(queryWrapper); return tsFiles; @@ -263,9 +250,9 @@ public class TsFilesServiceImpl extends ServiceImpl impl * 返回值说明: com.yfd.platform.config.ResponseResult 返回新增成功或者失败 ***********************************/ @Override - public Boolean addTsFiles(TsFiles tsFiles) { + public ResponseResult addTsFiles(TsFiles tsFiles) { - Boolean value = false; + Boolean value = true; //文件名称和大小 因为支持多个上传所以用,分隔 List names = Arrays.asList(tsFiles.getFileName().split(",")); List sizes = Arrays.asList(tsFiles.getFileSize().split(",")); @@ -277,7 +264,7 @@ public class TsFilesServiceImpl extends ServiceImpl impl // 数据校验 if (names.size() != sizes.size()) { LOGGER.error("文件名称和文件大小的列表长度不一致"); - return false; + return ResponseResult.error("文件名称和文件大小的列表长度不一致!"); } List filesToSave = new ArrayList<>(); @@ -290,6 +277,19 @@ public class TsFilesServiceImpl extends ServiceImpl impl for (int i = 0; i < names.size(); i++) { String name = names.get(i).trim(); String sizeStr = sizes.get(i).trim(); + + //通过节点ID 任务ID 路径 上级ID 文件名称 查询是否重复 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("node_id", tsFiles.getNodeId()); + queryWrapper.eq("task_id", tsFiles.getTaskId()); + queryWrapper.eq("work_path", tsFiles.getWorkPath()); + queryWrapper.eq("parent_id", tsFiles.getParentId()); + queryWrapper.eq("file_name", name); + int count = tsFilesMapper.selectCount(queryWrapper); + // 大于0说明 区域名称重复 + if (count > 0) { + return ResponseResult.error("文件已存在!"); + } // 校验文件大小是否可以转换成数值 try { TsFiles files1 = new TsFiles(); @@ -315,13 +315,20 @@ public class TsFilesServiceImpl extends ServiceImpl impl for (TsFiles filess : filesToSave) { int valueAdded = tsFilesMapper.insert(filess); if (valueAdded == 1) { + LOGGER.info("表结构和本地都创建文件夹成功"); value = true; } else { + LOGGER.error("本地创建文件夹成功,表结构新增失败"); value = false; } } } - return value; + if (value) { + return ResponseResult.success("新增文件成功!"); + } else { + return ResponseResult.error("新增文件失败!"); + } + } @@ -402,6 +409,7 @@ public class TsFilesServiceImpl extends ServiceImpl impl QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("node_id", tsFiles.getNodeId()); queryWrapper.eq("task_id", tsFiles.getTaskId()); + queryWrapper.eq("work_path", tsFiles.getWorkPath()); queryWrapper.eq("parent_id", tsFiles.getParentId()); queryWrapper.eq("file_name", tsFiles.getFileName()); int count = tsFilesMapper.selectCount(queryWrapper); @@ -448,6 +456,7 @@ public class TsFilesServiceImpl extends ServiceImpl impl QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("node_id", tsFiles.getNodeId()); queryWrapper.eq("task_id", tsFiles.getTaskId()); + queryWrapper.eq("work_path", tsFiles.getWorkPath()); queryWrapper.eq("parent_id", tsFiles.getParentId()); queryWrapper.eq("file_name", tsFiles.getFileName()); int count = tsFilesMapper.selectCount(queryWrapper); @@ -631,7 +640,7 @@ public class TsFilesServiceImpl extends ServiceImpl impl //首先通过ID集合查询所有的内容 然后放到List deleteItems里面 放好以后删除数据库 然后删除minio BatchDeleteRequest batchDeleteRequest = new BatchDeleteRequest(); batchDeleteRequest.setDeleteItems(deleteItemList); - batchDeleteRequest.setStorageKey("local"); + batchDeleteRequest.setStorageKey(type); AbstractBaseFileService fileService = storageSourceContext.getByStorageKey(batchDeleteRequest.getStorageKey()); List deleteItems = batchDeleteRequest.getDeleteItems(); @@ -932,60 +941,52 @@ public class TsFilesServiceImpl extends ServiceImpl impl /** * 递归压缩目录到ZIP文件 * - * @param sourceDir 当前要压缩的目录/文件路径 - * @param zipOut ZIP输出流 - * @param parentDir 当前路径在ZIP中的父目录(用于构建层级) - * @param processedPaths 已处理的规范路径集合(用于检测循环引用) + * @param sourcePath 当前要压缩的目录/文件路径 + * @param zipOut ZIP输出流 + * @param parentEntryName 当前路径在ZIP中的父目录(用于构建层级) + * @param processedPaths 已处理的规范路径集合(用于检测循环引用) * @return 压缩成功返回true,否则false */ - private boolean compressDirectoryToZip(Path sourceDir, ZipArchiveOutputStream zipOut, - String parentDir, Set processedPaths) { + private boolean compressDirectoryToZip(Path sourcePath, + ZipArchiveOutputStream zipOut, + String parentEntryName, + Set processedPaths) { try { - // --- 防循环处理 Start --- - // 获取规范路径(解析符号链接),用于检测重复目录 - Path canonicalPath = sourceDir.toRealPath(); + // === 路径规范化处理 === + Path canonicalPath = sourcePath.toRealPath(); - // 如果是目录且已处理过,直接跳过 - if (Files.isDirectory(sourceDir) && processedPaths.contains(canonicalPath)) { - return true; // 已处理,避免循环 - } - // 将当前目录加入已处理集合(先添加再处理子项) - processedPaths.add(canonicalPath); - // --- 防循环处理 End --- - - File currentFile = sourceDir.toFile(); - - // 处理文件(当sourceDirs包含文件时) - if (!currentFile.isDirectory()) { - // 直接压缩单个文件 - addFileToZip(currentFile, zipOut, parentDir); - return true; - } - - // 获取目录下的文件列表(处理空目录和权限问题) - File[] files = currentFile.listFiles(); - if (files == null) { - System.err.println("无法访问目录: " + sourceDir); - return false; // 目录不可访问 - } - - // 遍历目录内容 - for (File file : files) { - String entryName = parentDir + file.getName(); - - if (file.isDirectory()) { - // 创建目录条目(以/结尾) - zipOut.putArchiveEntry(new ZipArchiveEntry(entryName + "/")); - zipOut.closeArchiveEntry(); - - // 递归处理子目录,传入更新后的父路径和已处理集合 - if (!compressDirectoryToZip(file.toPath(), zipOut, entryName + "/", processedPaths)) { - return false; - } - } else { - // 压缩文件 - addFileToZip(file, zipOut, entryName); + // === 防循环检测 === + if (Files.isDirectory(sourcePath)) { + if (processedPaths.contains(canonicalPath)) { + return true; } + processedPaths.add(canonicalPath); + } + + // === 统一路径处理 === + String entryName = buildEntryName(sourcePath, parentEntryName); + + if (Files.isDirectory(sourcePath)) { + // === 处理目录 === + // 确保目录条目以/结尾 + if (!entryName.endsWith("/")) { + entryName += "/"; + } + + zipOut.putArchiveEntry(new ZipArchiveEntry(entryName)); + zipOut.closeArchiveEntry(); + + // 递归处理子项 + try (DirectoryStream dirStream = Files.newDirectoryStream(sourcePath)) { + for (Path child : dirStream) { + if (!compressDirectoryToZip(child, zipOut, entryName, processedPaths)) { + return false; + } + } + } + } else { + // === 处理文件 === + addFileToZip(sourcePath.toFile(), zipOut, entryName); } return true; } catch (IOException e) { @@ -995,18 +996,39 @@ public class TsFilesServiceImpl extends ServiceImpl impl } /** - * 将单个文件添加到ZIP流 - * - * @param file 要添加的文件 - * @param zipOut ZIP输出流 - * @param entryName 文件在ZIP中的路径 + * 构建ZIP条目名称(关键修改点) */ - private void addFileToZip(File file, ZipArchiveOutputStream zipOut, String entryName) throws IOException { - zipOut.putArchiveEntry(new ZipArchiveEntry(entryName)); - try (FileInputStream fis = new FileInputStream(file)) { - byte[] buffer = new byte[1024]; + private String buildEntryName(Path sourcePath, String parentEntryName) { + // 如果是根文件(无父目录),直接返回文件名 + if (parentEntryName.isEmpty()) { + return sourcePath.getFileName().toString(); + } + + // 合并父路径和当前文件名 + return parentEntryName + sourcePath.getFileName().toString(); + } + + /** + * 增强版文件添加方法 + */ + private void addFileToZip(File file, ZipArchiveOutputStream zipOut, String entryName) + throws IOException { + // === 重要修改:移除路径前缀 === + // 确保条目名称是纯文件名(当处理单个文件时) + if (!entryName.contains("/") && !entryName.isEmpty()) { + entryName = file.getName(); + } + + ZipArchiveEntry entry = new ZipArchiveEntry(entryName); + entry.setSize(file.length()); + entry.setTime(file.lastModified()); + + zipOut.putArchiveEntry(entry); + try (FileInputStream fis = new FileInputStream(file); + BufferedInputStream bis = new BufferedInputStream(fis)) { + byte[] buffer = new byte[8192]; int len; - while ((len = fis.read(buffer)) > 0) { + while ((len = bis.read(buffer)) > 0) { zipOut.write(buffer, 0, len); } } @@ -1030,47 +1052,129 @@ public class TsFilesServiceImpl extends ServiceImpl impl queryWrapper.eq("name", "filePath"); StorageSourceConfig storageSourceConfig = storageSourceConfigMapper.selectOne(queryWrapper); - - // 2. 获取压缩包记录 + // 1. 获取压缩包记录 TsFiles zipFileRecord = tsFilesMapper.selectById(id); String zipName = getFileNameWithoutExtension(zipFileRecord.getFileName()); // 示例:222 try { - // 3. 构建解压目标路径(示例:E:/yun/333/222/) - Path destRoot = Paths.get(storageSourceConfig.getValue(), decompressionPath); // 关键修改:强制创建222子目录 + // 2. 判断压缩包类型(单文件还是文件夹) + Path zipFilePath = Paths.get(storageSourceConfig.getValue(), zipFileRecord.getWorkPath(), zipFileRecord.getFileName()); + // 3. 判断压缩包内容类型 + boolean hasFolder = hasFolderInZip(zipFilePath); +// // 4. 构建解压目标路径 + Path destRoot; + if (hasFolder) { + // 如果有文件夹,创建子目录(如 E:/yun/333/222/) + destRoot = Paths.get(storageSourceConfig.getValue(), decompressionPath, zipName); + } else { + // 如果只有文件,直接解压到目标目录(如 E:/yun/333/) + destRoot = Paths.get(storageSourceConfig.getValue(), decompressionPath); + } Files.createDirectories(destRoot); - // 4. 执行解压(ZIP内容解压到222子目录) - File unzippedRoot = unzipToTemp(Paths.get(storageSourceConfig.getValue(), decompressionPath, zipFileRecord.getFileName()), destRoot.toString()); - // 5. 创建根目录记录(示例:222的work_path为/333/222/) - TsFiles rootFolder = createFolderRecord( - zipName, // 文件夹名称222 - parentId, // 父ID是333的ID - buildFolderPath(decompressionPath), // /333/222/ - zipFileRecord.getTaskId(), - zipFileRecord.getNodeId(), - zipFileRecord.getUploader() - ); - String rootFolderId = rootFolder.getId(); - - // 6. 处理子内容(路径从/333/222/开始) - processFolderContents( - unzippedRoot, - rootFolderId, - buildFolderPathq(decompressionPath + "/" + zipName + "/"), // /333/222/ - zipFileRecord.getTaskId(), - zipFileRecord.getNodeId(), - zipFileRecord.getUploader() + // 5.执行解压(到子目录) + File unzippedRoot = unzipToTemp( + Paths.get(storageSourceConfig.getValue(), zipFileRecord.getWorkPath(), zipFileRecord.getFileName()), + destRoot.toString() ); - // 递归保存结构到数据库 - // saveFolderStructure(unzippedRoot, parentId, decompressionPath, zipFileRecord.getTaskId(), zipFileRecord.getNodeId(), zipFileRecord.getUploader()); + // 6. 处理解压内容 + if (hasFolder) { + // 如果有文件夹,创建文件夹记录并处理子内容 + TsFiles rootFolder = createFolderRecord( + zipName, // 文件夹名称222 + parentId, // 父ID是333的ID + buildFolderPath(decompressionPath), // 修正为 /333/222/ + zipFileRecord.getTaskId(), + zipFileRecord.getNodeId(), + zipFileRecord.getUploader() + ); + String rootFolderId = rootFolder.getId(); + // 7. 处理子内容(路径从/333/222/开始) + processFolderContents( + unzippedRoot, + rootFolderId, + buildFolderPath(decompressionPath + "/" + zipName + "/"), // 修正为 /333/222/ + zipFileRecord.getTaskId(), + zipFileRecord.getNodeId(), + zipFileRecord.getUploader() + ); + } else { + // 如果只有文件,直接创建文件记录 + File[] files = unzippedRoot.listFiles(); + if (files != null && files.length > 0) { + for (File file : files) { + if (file.isDirectory()) { + continue; + } else { + TsFiles fileRecord = createFileRecord( + file.getName(), + parentId, + buildFolderPath(decompressionPath), + zipFileRecord.getTaskId(), + zipFileRecord.getNodeId(), + zipFileRecord.getUploader(), + file.length() + ); + } + } + } + + } return "解压存储成功"; } catch (Exception e) { throw new RuntimeException("操作失败: " + e.getMessage()); } } + /** + * 判断压缩包中是否包含文件夹 + */ + private boolean hasFolderInZip(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; // 如果发现文件夹,返回 true + } + } + return false; // 如果没有文件夹,返回 false + } + } + + + /** + * 创建文件记录 + */ + private TsFiles createFileRecord(String fileName, String parentId, String workPath, + String taskId, String nodeId, String uploader, long fileSize) { + + QueryWrapper query = new QueryWrapper() + .eq("node_id", nodeId) + .eq("task_id", taskId) + .eq("work_path", workPath) + .eq("file_name", fileName) + .eq("is_file", "FILE") + .eq("parent_id", parentId); + TsFiles existing = tsFilesMapper.selectOne(query); + if (existing != null) { + return existing; + } else { + TsFiles fileRecord = new TsFiles(); + fileRecord.setNodeId(nodeId); + fileRecord.setTaskId(taskId); + fileRecord.setParentId(parentId); + fileRecord.setFileName(fileName); + fileRecord.setWorkPath(workPath); + fileRecord.setIsFile("FILE"); + fileRecord.setFileSize(String.valueOf(fileSize)); + fileRecord.setUploadTime(new Timestamp(System.currentTimeMillis())); + operateTsfeiles(fileRecord); // 插入或更新记录 + return fileRecord; + } + } + /** * 创建文件夹记录 */ @@ -1101,16 +1205,18 @@ public class TsFilesServiceImpl extends ServiceImpl impl if (children == null) return; for (File child : children) { + String childName = child.getName(); + String childWorkPath = parentWorkPath + childName + (child.isDirectory() ? "/" : ""); // 修正为 /333/222/childName/ if (child.isDirectory()) { // 处理子目录(示例:555) - String folderWorkPath = parentWorkPath + child.getName() + "/"; + String folderWorkPath = parentWorkPath; TsFiles folderRecord = new TsFiles(); folderRecord.setNodeId(nodeId); folderRecord.setTaskId(taskId); folderRecord.setParentId(parentId); // 父ID是222的ID folderRecord.setFileName(child.getName()); - folderRecord.setWorkPath(parentWorkPath); // /333/222/555/ + folderRecord.setWorkPath(childWorkPath); // /333/222/555/ folderRecord.setIsFile("FOLDER"); folderRecord.setFileSize("0"); folderRecord.setUploadTime(new Timestamp(System.currentTimeMillis())); @@ -1188,11 +1294,13 @@ public class TsFilesServiceImpl extends ServiceImpl impl * @return 解压后的根目录 File 对象 */ //这个方法会直接覆盖 下面那个不会 那个会报错 TODO + // 1. 检测压缩包是否包含文件夹结构(目录或路径分隔符) private File unzipToTemp(Path zipFilePath, String baseDir) throws IOException { - // 1. 直接构建目标路径(baseDir/zipName) + Path destRoot = Paths.get(baseDir); + Files.createDirectories(destRoot); + + // 获取压缩包名称(无扩展名) String zipName = getFileNameWithoutExtension(zipFilePath.getFileName().toString()); - Path destRoot = Paths.get(baseDir, zipName); - Files.createDirectories(destRoot); // 确保目标目录存在 try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFilePath.toFile()))) { ZipEntry entry; @@ -1202,38 +1310,103 @@ public class TsFilesServiceImpl extends ServiceImpl impl continue; } - // 2. 直接解压到目标路径(不再嵌套临时目录) - Path destPath = destRoot.resolve(entry.getName()).normalize(); - validatePathSafety(destPath, destRoot); // 确保路径安全 + // 关键修改:剥离压缩包根目录 + String entryName = entry.getName(); + if (entryName.startsWith(zipName + "/")) { + entryName = entryName.substring(zipName.length() + 1); // 跳过 "2/" + } + + // 跳过空路径(根目录本身) + if (entryName.isEmpty()) { + zis.closeEntry(); + continue; + } + + // 构建目标路径 + Path destPath = destRoot.resolve(entryName).normalize(); + validatePathSafety(destPath, destRoot); - // 3. 处理目录 if (entry.isDirectory()) { Files.createDirectories(destPath); - } - // 4. 处理文件 - else { - // 确保父目录存在 + } else { Files.createDirectories(destPath.getParent()); - - // 写入文件(使用缓冲提升性能,覆盖已存在的文件) - 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); - } - } + writeFile(zis, destPath); } zis.closeEntry(); } } return destRoot.toFile(); } + + /** + * 解析压缩包条目,过滤无效文件(如 __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 // private File unzipToTemp(Path zipFilePath, String baseDir) throws IOException { // // 1. 直接构建目标路径(baseDir/zipName) @@ -1297,296 +1470,540 @@ public class TsFilesServiceImpl extends ServiceImpl impl } /*******************************************************本地和minio对比开始****************************************************************/ - @Override public TsFiles compareDirectories(List dataset, String nodeId, String taskId) { - //查询本地文件路径根目录 如 E:\yun - QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.eq("name", "filePath"); - StorageSourceConfig storageSourceConfig = storageSourceConfigMapper.selectOne(queryWrapper); + // 获取本地文件路径根目录和存储空间名称 + StorageSourceConfig filePathConfig = getStorageConfig("filePath"); + StorageSourceConfig bucketConfig = getStorageConfig("bucketName"); - //查询存储空间名称根目录 如test-bucket - QueryWrapper queryWrapper1 = new QueryWrapper<>(); - queryWrapper1.eq("name", "bucketName"); - StorageSourceConfig storageSourceConfig1 = storageSourceConfigMapper.selectOne(queryWrapper1); - // 获取文件列表 TsFiles tsFiles = new TsFiles(); try { - List fileItemListMinio = new ArrayList<>(); - List fileItemListLocal = new ArrayList<>(); - if (StringUtils.isNotEmpty(nodeId) && StringUtils.isNotEmpty(taskId)) { - QueryWrapper queryWrapper2 = new QueryWrapper<>(); - queryWrapper2.eq("task_id", taskId) - .eq("node_id", nodeId).and(i -> i.eq("work_path", "/").or() - .eq("backup_path", "/")); - List allNodes = tsFilesMapper.selectList(queryWrapper2); - for (TsFiles tsFiles1 : allNodes) { - if (tsFiles1 != null) { - // 获取minio文件列表 - if (StringUtils.isNotEmpty(tsFiles1.getBackupPath())) { - AbstractBaseFileService fileServiceMinio = storageSourceContext.getByStorageKey("minio"); - List minioFiles = fileServiceMinio.fileListData(tsFiles1.getBackupPath(), tsFiles1.getFileName()); - // 将当前文件列表添加到 fileItemListMinio - if (minioFiles != null) { - fileItemListMinio.addAll(minioFiles); + // 获取 MinIO 和本地文件列表(并行处理) + List fileItemListMinio = Collections.synchronizedList(new ArrayList<>()); + List fileItemListLocal = Collections.synchronizedList(new ArrayList<>()); + + if (StringUtils.isNoneEmpty(nodeId, taskId)) { + // 并行查询数据库记录 + tsFilesMapper.selectList(buildTaskQuery(nodeId, taskId)).parallelStream() + .forEach(tsFile -> { + try { + processFileLists(tsFile, fileItemListMinio, fileItemListLocal); + } catch (Exception e) { + e.printStackTrace(); } - } - - // 获取本地文件列表 - if (StringUtils.isNotEmpty(tsFiles1.getWorkPath())) { - AbstractBaseFileService fileServiceLocal = storageSourceContext.getByStorageKey("local"); - List localFiles = fileServiceLocal.fileListData(tsFiles1.getWorkPath(), tsFiles1.getFileName()); - - // 将当前文件列表添加到 fileItemListLocal - if (localFiles != null) { - fileItemListLocal.addAll(localFiles); - } - } - } - } - + }); } else { - for (String id : dataset) { - TsFiles tsFiles1 = tsFilesMapper.selectById(id); - if (tsFiles1 != null) { - // 获取minio文件列表 - if (StringUtils.isNotEmpty(tsFiles1.getBackupPath())) { - AbstractBaseFileService fileServiceMinio = storageSourceContext.getByStorageKey("minio"); - List minioFiles = fileServiceMinio.fileListData(tsFiles1.getBackupPath(), tsFiles1.getFileName()); - // 将当前文件列表添加到 fileItemListMinio - if (minioFiles != null) { - fileItemListMinio.addAll(minioFiles); + dataset.parallelStream() + .map(tsFilesMapper::selectById) + .filter(Objects::nonNull) + .forEach(tsFile -> { + try { + processFileLists(tsFile, fileItemListMinio, fileItemListLocal); + } catch (Exception e) { + e.printStackTrace(); } - } - - // 获取本地文件列表 - if (StringUtils.isNotEmpty(tsFiles1.getWorkPath())) { - AbstractBaseFileService fileServiceLocal = storageSourceContext.getByStorageKey("local"); - List localFiles = fileServiceLocal.fileListData(tsFiles1.getWorkPath(), tsFiles1.getFileName()); - - // 将当前文件列表添加到 fileItemListLocal - if (localFiles != null) { - fileItemListLocal.addAll(localFiles); - } - } - } - } + }); } - //去除重复的 - Set seenMinioPaths = new HashSet<>(); // 用于记录已经处理过的 path + name - Iterator iteratorMinio = fileItemListMinio.iterator(); - while (iteratorMinio.hasNext()) { - FileItemResult fileItemResult = iteratorMinio.next(); - String path = normalizePath(fileItemResult.getPath()); - String name = fileItemResult.getName(); - String uniqueKey = path + name; // 唯一标识 - if (seenMinioPaths.contains(uniqueKey)) { - // 如果已经存在,则移除当前项 - iteratorMinio.remove(); - } else { - // 如果不存在,则添加到已处理集合中,并更新 path - seenMinioPaths.add(uniqueKey); - fileItemResult.setPath(path); - } - } + // 去重逻辑(并行去重) + deduplicateFileList(fileItemListMinio); + deduplicateFileList(fileItemListLocal); - //去除重复的 - Set seenLocalPaths = new HashSet<>(); // 用于记录已经处理过的 path + name - Iterator iteratorLocal = fileItemListLocal.iterator(); - while (iteratorLocal.hasNext()) { - FileItemResult fileItemResult = iteratorLocal.next(); - String path = normalizePath(fileItemResult.getPath()); - String name = fileItemResult.getName(); - String uniqueKey = path + name; // 唯一标识 - - if (seenLocalPaths.contains(uniqueKey)) { - // 如果已经存在,则移除当前项 - iteratorLocal.remove(); - } else { - // 如果不存在,则添加到已处理集合中,并更新 path - seenLocalPaths.add(uniqueKey); - fileItemResult.setPath(path); - } - } - - // 对比文件 - tsFiles = compareFiles(fileItemListMinio, fileItemListLocal, storageSourceConfig.getValue(), storageSourceConfig1.getValue()); - LOGGER.info("minio文件 " + fileItemListMinio); - LOGGER.info("本地文件 " + fileItemListLocal); + // 对比文件(并行计算 MD5) + tsFiles = compareFiles(fileItemListMinio, fileItemListLocal, + filePathConfig.getValue(), bucketConfig.getValue()); + LOGGER.info("MinIO文件列表: {}", fileItemListMinio); + LOGGER.info("本地文件列表: {}", fileItemListLocal); } catch (Exception e) { - e.printStackTrace(); + LOGGER.error("目录对比失败: {}", e.getMessage(), e); } return tsFiles; } - - public String calculateMD5(InputStream inputStream) throws Exception { - MessageDigest md = MessageDigest.getInstance("MD5"); - byte[] buffer = new byte[8192]; - int bytesRead; - while ((bytesRead = inputStream.read(buffer)) != -1) { - md.update(buffer, 0, bytesRead); - } - byte[] digest = md.digest(); - StringBuilder sb = new StringBuilder(); - for (byte b : digest) { - sb.append(String.format("%02x", b)); - } - return sb.toString(); + // 辅助方法:获取存储配置 + private StorageSourceConfig getStorageConfig(String name) { + return storageSourceConfigMapper.selectOne( + new QueryWrapper().eq("name", name) + ); } - - public TsFiles compareFiles(List fileItemListMinio, List fileItemListLocal, String filePath, String bucketName) { - TsFiles tsFiles = new TsFiles(); - - // 将 MinIO 文件列表转换为 Map(文件名 -> 文件信息) - Map minioFileMap = new HashMap<>(); - for (FileItemResult file : fileItemListMinio) { - // 处理路径格式 - String path = normalizePath(file.getPath()); - QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.eq("file_name", file.getName()); - queryWrapper.eq("backup_path", path); - TsFiles tsFiles1 = tsFilesMapper.selectOne(queryWrapper); - if (tsFiles1 != null) { - file.setId(tsFiles1.getId()); - - } - - minioFileMap.put(path + file.getName(), file); - } - - // 将本地文件列表转换为 Map(文件名 -> 文件信息) - Map localFileMap = new HashMap<>(); - for (FileItemResult file : fileItemListLocal) { - // 处理路径格式 - String path = normalizePath(file.getPath()); - QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.eq("file_name", file.getName()); - queryWrapper.eq("work_path", path); - TsFiles tsFiles1 = tsFilesMapper.selectOne(queryWrapper); - if (tsFiles1 != null) { - file.setId(tsFiles1.getId()); - - } - localFileMap.put(path + file.getName(), file); - } - - // 检查本地有而 MinIO 没有的文件 - List localOnlyFiles = new ArrayList<>(); - for (FileItemResult localFile : fileItemListLocal) { - // 处理路径格式 - String path = normalizePath(localFile.getPath()); - if (!minioFileMap.containsKey(path + localFile.getName())) { - localOnlyFiles.add(localFile); - } - } - LOGGER.info("本地有而 MinIO 没有的文件:" + localOnlyFiles); - - // 检查 MinIO 有而本地没有的文件 - List minioOnlyFiles = new ArrayList<>(); - for (FileItemResult minioFile : fileItemListMinio) { - String path = normalizePath(minioFile.getPath()); - if (!localFileMap.containsKey(path + minioFile.getName())) { - minioOnlyFiles.add(minioFile); - } - } - LOGGER.info("MinIO 有而本地没有的文件:" + minioOnlyFiles); - - // 检查文件大小不一致的文件 - List sizeMismatchedFiles = new ArrayList<>(); - - // 检查 MD5 不一致的文件 - List md5MismatchedFiles = new ArrayList<>(); - - // 遍历 MinIO 文件 - for (FileItemResult minioFile : fileItemListMinio) { - String path = normalizePath(minioFile.getPath()); - // 检查 MinIO 文件是否在本地存在 - if (localFileMap.containsKey(path + minioFile.getName())) { - FileItemResult localFile = localFileMap.get(path + minioFile.getName()); - String localBasePath = filePath + minioFile.getPath(); - // 检查文件是否为文件夹,如果是文件夹,跳过 - File localFilePath = new File(localBasePath + localFile.getName()); - if (localFilePath.isDirectory()) { - LOGGER.error("路径指向的是一个文件夹,跳过该文件夹:" + localFilePath.getAbsolutePath()); - continue; // 跳过文件夹 - } - - // 检查文件大小是否匹配 - if (minioFile.getSize() != null && localFile.getSize() != null && minioFile.getSize().longValue() != localFile.getSize().longValue()) { - sizeMismatchedFiles.add(minioFile.getName()); - } - // 如果文件大小一致,进一步检查 MD5 - try { - // 计算本地文件的 MD5 - String localMD5 = calculateMD5(new FileInputStream(localFilePath)); - if (localMD5 == null || localMD5.isEmpty()) { - continue; - } - - // 获取 MinIO 文件的 MD5 - String minioMD5; - AbstractBaseFileService fileServiceMinio = storageSourceContext.getByStorageKey("minio"); - String key = StringUtils.concat(minioFile.getPath(), minioFile.getName()); // 构造文件的 key - if (minioFile.getSize() <= 5 * 1024 * 1024 * 1024L) { // 小于 5GB,使用 ETag - ObjectMetadata metadata = fileServiceMinio.getObjectMetadata(bucketName, key); - minioMD5 = metadata.getETag().replace("\"", ""); // 去除 ETag 的引号 - } else { // 大于 5GB,手动计算 MD5 - S3Object s3Object = fileServiceMinio.getObject(bucketName, key); - minioMD5 = calculateMD5(s3Object.getObjectContent()); - } - - // 比较 MD5 - if (!minioMD5.equals(localMD5)) { - minioFile.setLocatMd5(localMD5); - minioFile.setMinioMd5(minioMD5); - md5MismatchedFiles.add(minioFile); - } - } catch (Exception e) { - LOGGER.error("计算 MD5 失败:" + e.getMessage()); - } - - } else { - // 如果 MinIO 中没有该文件,本地有的情况下,跳过该文件 - LOGGER.info("MinIO 中没有该文件,本地有文件,跳过对比: " + minioFile.getName()); - } - } - tsFiles.setLocalOnlyFiles(localOnlyFiles); - tsFiles.setMinioOnlyFiles(minioOnlyFiles); - tsFiles.setMd5MismatchedFiles(md5MismatchedFiles); - LOGGER.info("文件大小不一致的文件:" + sizeMismatchedFiles); - LOGGER.info("文件大小一致但 MD5 不一致的文件:" + md5MismatchedFiles); - return tsFiles; - + // 辅助方法:构建查询条件 + private QueryWrapper buildTaskQuery(String nodeId, String taskId) { + return new QueryWrapper() + .eq("task_id", taskId) + .eq("node_id", nodeId) + .and(wrapper -> wrapper.eq("work_path", "/").or().eq("backup_path", "/")); } + // 辅助方法:处理文件列表(并行安全) + private void processFileLists(TsFiles tsFile, + List minioList, + List localList) throws Exception { + processFileList(tsFile.getBackupPath(), tsFile.getFileName(), "minio", minioList); + processFileList(tsFile.getWorkPath(), tsFile.getFileName(), "local", localList); + } + + // 辅助方法:获取文件列表并添加到集合 + private void processFileList(String path, String fileName, String storageKey, + List targetList) throws Exception { + if (StringUtils.isNotEmpty(path)) { + AbstractBaseFileService service = storageSourceContext.getByStorageKey(storageKey); + List files = service.fileListData(path, fileName); + if (files != null) { + // 对每个文件的路径进行规范化 + files.forEach(file -> { + String normalizedPath = ensurePathFormat(file.getPath()); + file.setPath(normalizedPath); + }); + // 同步添加(线程安全) + synchronized (targetList) { + targetList.addAll(files); + } + } + } + } + + /** - * 递归列出本地文件 - * - * @param file 当前文件/目录 - * @param basePath 基础路径(用于计算相对路径) - * @param results 结果Map + * 确保路径格式为以 "/" 开头和结尾(例如 "/data/test/") + * 若路径为空或非法,返回根路径 "/" */ - private void listLocalFilesRecursive(File file, String basePath, Map results) { - String relativePath = file.getAbsolutePath().substring(basePath.length()) - .replace("\\", "/"); - if (relativePath.startsWith("/")) relativePath = relativePath.substring(1); + private String ensurePathFormat(String path) { + if (StringUtils.isBlank(path)) { + return "/"; + } + // 统一替换反斜杠为斜杠(兼容 Windows 路径) + String normalized = path.replaceAll("\\\\", "/"); + // 确保以 "/" 开头 + if (!normalized.startsWith("/")) { + normalized = "/" + normalized; + } + // 确保以 "/" 结尾 + if (!normalized.endsWith("/")) { + normalized += "/"; + } + return normalized; + } - if (file.isDirectory()) { - results.put(relativePath + "/", new FileInfo()); - for (File f : file.listFiles()) { - listLocalFilesRecursive(f, basePath, results); + // 辅助方法:去重文件列表(并行安全) + private void deduplicateFileList(List fileList) { + ConcurrentMap seenKeys = new ConcurrentHashMap<>(); + fileList.removeIf(file -> { + String key = normalizePath(file.getPath()) + file.getName(); + return seenKeys.putIfAbsent(key, Boolean.TRUE) != null; + }); + } + + public TsFiles compareFiles(List minioFiles, + List localFiles, + String filePath, + String bucketName) { + TsFiles result = new TsFiles(); + + // 使用并行流构建 Map(路径 + 文件名 -> 文件) + ConcurrentMap minioMap = minioFiles.parallelStream() + .collect(Collectors.toConcurrentMap( + file -> generateMapKey(file), + file -> enrichFileMetadata(file, "backup_path") + )); + + ConcurrentMap localMap = localFiles.parallelStream() + .collect(Collectors.toConcurrentMap( + file -> generateMapKey(file), + file -> enrichFileMetadata(file, "work_path") + )); + + // 检查独有文件(并行过滤) + List localOnly = localFiles.parallelStream() + .filter(file -> !minioMap.containsKey(generateMapKey(file))) + .collect(Collectors.toList()); + + List minioOnly = minioFiles.parallelStream() + .filter(file -> !localMap.containsKey(generateMapKey(file))) + .collect(Collectors.toList()); + + // 检查不一致文件(并行处理) + List md5Mismatches = Collections.synchronizedList(new ArrayList<>()); + List sizeMismatches = Collections.synchronizedList(new ArrayList<>()); + + minioFiles.parallelStream().forEach(minioFile -> { + String key = generateMapKey(minioFile); + if (localMap.containsKey(key)) { + FileItemResult localFile = localMap.get(key); + checkFileConsistency(minioFile, localFile, filePath, bucketName, sizeMismatches, md5Mismatches); } - } else { - results.put(relativePath, new FileInfo()); - //results.put(relativePath, new FileInfo(calculateMD5(file), false)); + }); + + // 设置结果 + result.setLocalOnlyFiles(localOnly); + result.setMinioOnlyFiles(minioOnly); + result.setMd5MismatchedFiles(md5Mismatches); + + LOGGER.info("大小不一致文件: {}", sizeMismatches); + LOGGER.info("MD5不一致文件: {}", md5Mismatches); + + return result; + } + + // 辅助方法:生成 Map Key + private String generateMapKey(FileItemResult file) { + return normalizePath(file.getPath()) + file.getName(); + } + + // 辅助方法:丰富文件元数据 + private FileItemResult enrichFileMetadata(FileItemResult file, String pathType) { + TsFiles dbRecord = tsFilesMapper.selectOne( + new QueryWrapper() + .eq("file_name", file.getName()) + .eq(pathType, normalizePath(file.getPath())) + ); + if (dbRecord != null) { + file.setId(dbRecord.getId()); + } + return file; + } + + // 辅助方法:检查文件一致性 + private void checkFileConsistency(FileItemResult minioFile, + FileItemResult localFile, + String basePath, + String bucketName, + List sizeMismatches, + List md5Mismatches) { + File localFileObj = new File(basePath + minioFile.getPath(), localFile.getName()); + if (localFileObj.isDirectory()) { + LOGGER.warn("跳过文件夹: {}", localFileObj.getAbsolutePath()); + return; + } + + // 检查文件大小 + if (!isSizeMatch(minioFile, localFile)) { + sizeMismatches.add(minioFile.getName()); + return; // 大小不匹配时无需检查 MD5 + } + + // 检查 MD5 + try { + String localMD5 = calculateMD5(new FileInputStream(localFileObj)); + String minioMD5 = getMinioMD5(minioFile, bucketName); + + if (StringUtils.isNoneEmpty(localMD5, minioMD5) && !localMD5.equals(minioMD5)) { + minioFile.setLocatMd5(localMD5); + minioFile.setMinioMd5(minioMD5); + md5Mismatches.add(minioFile); + } + } catch (Exception e) { + LOGGER.error("MD5计算失败: {} - {}", minioFile.getName(), e.getMessage()); + } + } + + // 辅助方法:检查文件大小是否匹配 + private boolean isSizeMatch(FileItemResult minioFile, FileItemResult localFile) { + return minioFile.getSize() != null + && localFile.getSize() != null + && minioFile.getSize().equals(localFile.getSize()); + } + + // 辅助方法:获取 MinIO 文件 MD5 + private String getMinioMD5(FileItemResult file, String bucketName) { + AbstractBaseFileService minioService = storageSourceContext.getByStorageKey("minio"); + String key = StringUtils.concat(file.getPath(), file.getName()); + + try { + if (file.getSize() <= 5L * 1024 * 1024 * 1024) { + return minioService.getObjectMetadata(bucketName, key).getETag().replace("\"", ""); + } else { + try (S3Object s3Object = minioService.getObject(bucketName, key)) { + return calculateMD5(s3Object.getObjectContent()); + } + } + } catch (Exception e) { + LOGGER.error("MinIO MD5获取失败: {}", key, e); + return null; + } + } + + // 优化后的 MD5 计算方法(线程安全) + public String calculateMD5(InputStream inputStream) throws IOException, NoSuchAlgorithmException { + try (InputStream is = inputStream) { // 确保流关闭 + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = is.read(buffer)) != -1) { + md.update(buffer, 0, bytesRead); + } + return Hex.encodeHexString(md.digest()); } } +// @Override +// public TsFiles compareDirectories(List dataset, String nodeId, String taskId) { +// //查询本地文件路径根目录 如 E:\yun +// QueryWrapper queryWrapper = new QueryWrapper<>(); +// queryWrapper.eq("name", "filePath"); +// StorageSourceConfig storageSourceConfig = storageSourceConfigMapper.selectOne(queryWrapper); +// +// //查询存储空间名称根目录 如test-bucket +// QueryWrapper queryWrapper1 = new QueryWrapper<>(); +// queryWrapper1.eq("name", "bucketName"); +// StorageSourceConfig storageSourceConfig1 = storageSourceConfigMapper.selectOne(queryWrapper1); +// // 获取文件列表 +// TsFiles tsFiles = new TsFiles(); +// try { +// List fileItemListMinio = new ArrayList<>(); +// List fileItemListLocal = new ArrayList<>(); +// if (StringUtils.isNotEmpty(nodeId) && StringUtils.isNotEmpty(taskId)) { +// QueryWrapper queryWrapper2 = new QueryWrapper<>(); +// queryWrapper2.eq("task_id", taskId) +// .eq("node_id", nodeId).and(i -> i.eq("work_path", "/").or() +// .eq("backup_path", "/")); +// List allNodes = tsFilesMapper.selectList(queryWrapper2); +// for (TsFiles tsFiles1 : allNodes) { +// if (tsFiles1 != null) { +// // 获取minio文件列表 +// if (StringUtils.isNotEmpty(tsFiles1.getBackupPath())) { +// AbstractBaseFileService fileServiceMinio = storageSourceContext.getByStorageKey("minio"); +// List minioFiles = fileServiceMinio.fileListData(tsFiles1.getBackupPath(), tsFiles1.getFileName()); +// // 将当前文件列表添加到 fileItemListMinio +// if (minioFiles != null) { +// fileItemListMinio.addAll(minioFiles); +// } +// } +// +// // 获取本地文件列表 +// if (StringUtils.isNotEmpty(tsFiles1.getWorkPath())) { +// AbstractBaseFileService fileServiceLocal = storageSourceContext.getByStorageKey("local"); +// List localFiles = fileServiceLocal.fileListData(tsFiles1.getWorkPath(), tsFiles1.getFileName()); +// +// // 将当前文件列表添加到 fileItemListLocal +// if (localFiles != null) { +// fileItemListLocal.addAll(localFiles); +// } +// } +// } +// } +// +// } else { +// for (String id : dataset) { +// TsFiles tsFiles1 = tsFilesMapper.selectById(id); +// if (tsFiles1 != null) { +// // 获取minio文件列表 +// if (StringUtils.isNotEmpty(tsFiles1.getBackupPath())) { +// AbstractBaseFileService fileServiceMinio = storageSourceContext.getByStorageKey("minio"); +// List minioFiles = fileServiceMinio.fileListData(tsFiles1.getBackupPath(), tsFiles1.getFileName()); +// // 将当前文件列表添加到 fileItemListMinio +// if (minioFiles != null) { +// fileItemListMinio.addAll(minioFiles); +// } +// } +// +// // 获取本地文件列表 +// if (StringUtils.isNotEmpty(tsFiles1.getWorkPath())) { +// AbstractBaseFileService fileServiceLocal = storageSourceContext.getByStorageKey("local"); +// List localFiles = fileServiceLocal.fileListData(tsFiles1.getWorkPath(), tsFiles1.getFileName()); +// +// // 将当前文件列表添加到 fileItemListLocal +// if (localFiles != null) { +// fileItemListLocal.addAll(localFiles); +// } +// } +// } +// } +// } +// +// //去除重复的 +// Set seenMinioPaths = new HashSet<>(); // 用于记录已经处理过的 path + name +// Iterator iteratorMinio = fileItemListMinio.iterator(); +// while (iteratorMinio.hasNext()) { +// FileItemResult fileItemResult = iteratorMinio.next(); +// String path = normalizePath(fileItemResult.getPath()); +// String name = fileItemResult.getName(); +// String uniqueKey = path + name; // 唯一标识 +// if (seenMinioPaths.contains(uniqueKey)) { +// // 如果已经存在,则移除当前项 +// iteratorMinio.remove(); +// } else { +// // 如果不存在,则添加到已处理集合中,并更新 path +// seenMinioPaths.add(uniqueKey); +// fileItemResult.setPath(path); +// } +// } +// +// //去除重复的 +// Set seenLocalPaths = new HashSet<>(); // 用于记录已经处理过的 path + name +// Iterator iteratorLocal = fileItemListLocal.iterator(); +// while (iteratorLocal.hasNext()) { +// FileItemResult fileItemResult = iteratorLocal.next(); +// String path = normalizePath(fileItemResult.getPath()); +// String name = fileItemResult.getName(); +// String uniqueKey = path + name; // 唯一标识 +// +// if (seenLocalPaths.contains(uniqueKey)) { +// // 如果已经存在,则移除当前项 +// iteratorLocal.remove(); +// } else { +// // 如果不存在,则添加到已处理集合中,并更新 path +// seenLocalPaths.add(uniqueKey); +// fileItemResult.setPath(path); +// } +// } +// +// // 对比文件 +// tsFiles = compareFiles(fileItemListMinio, fileItemListLocal, storageSourceConfig.getValue(), storageSourceConfig1.getValue()); +// LOGGER.info("minio文件 " + fileItemListMinio); +// LOGGER.info("本地文件 " + fileItemListLocal); +// +// } catch (Exception e) { +// e.printStackTrace(); +// } +// return tsFiles; +// } +// +// +// +// +// public TsFiles compareFiles(List fileItemListMinio, List fileItemListLocal, String filePath, String bucketName) { +// TsFiles tsFiles = new TsFiles(); +// +// // 将 MinIO 文件列表转换为 Map(文件名 -> 文件信息) +// Map minioFileMap = new HashMap<>(); +// for (FileItemResult file : fileItemListMinio) { +// // 处理路径格式 +// String path = normalizePath(file.getPath()); +// QueryWrapper queryWrapper = new QueryWrapper<>(); +// queryWrapper.eq("file_name", file.getName()); +// queryWrapper.eq("backup_path", path); +// TsFiles tsFiles1 = tsFilesMapper.selectOne(queryWrapper); +// if (tsFiles1 != null) { +// file.setId(tsFiles1.getId()); +// +// } +// +// minioFileMap.put(path + file.getName(), file); +// } +// +// // 将本地文件列表转换为 Map(文件名 -> 文件信息) +// Map localFileMap = new HashMap<>(); +// for (FileItemResult file : fileItemListLocal) { +// // 处理路径格式 +// String path = normalizePath(file.getPath()); +// QueryWrapper queryWrapper = new QueryWrapper<>(); +// queryWrapper.eq("file_name", file.getName()); +// queryWrapper.eq("work_path", path); +// TsFiles tsFiles1 = tsFilesMapper.selectOne(queryWrapper); +// if (tsFiles1 != null) { +// file.setId(tsFiles1.getId()); +// +// } +// localFileMap.put(path + file.getName(), file); +// } +// +// // 检查本地有而 MinIO 没有的文件 +// List localOnlyFiles = new ArrayList<>(); +// for (FileItemResult localFile : fileItemListLocal) { +// // 处理路径格式 +// String path = normalizePath(localFile.getPath()); +// if (!minioFileMap.containsKey(path + localFile.getName())) { +// localOnlyFiles.add(localFile); +// } +// } +// LOGGER.info("本地有而 MinIO 没有的文件:" + localOnlyFiles); +// +// // 检查 MinIO 有而本地没有的文件 +// List minioOnlyFiles = new ArrayList<>(); +// for (FileItemResult minioFile : fileItemListMinio) { +// String path = normalizePath(minioFile.getPath()); +// if (!localFileMap.containsKey(path + minioFile.getName())) { +// minioOnlyFiles.add(minioFile); +// } +// } +// LOGGER.info("MinIO 有而本地没有的文件:" + minioOnlyFiles); +// +// // 检查文件大小不一致的文件 +// List sizeMismatchedFiles = new ArrayList<>(); +// +// // 检查 MD5 不一致的文件 +// List md5MismatchedFiles = new ArrayList<>(); +// +// // 遍历 MinIO 文件 +// for (FileItemResult minioFile : fileItemListMinio) { +// String path = normalizePath(minioFile.getPath()); +// // 检查 MinIO 文件是否在本地存在 +// if (localFileMap.containsKey(path + minioFile.getName())) { +// FileItemResult localFile = localFileMap.get(path + minioFile.getName()); +// String localBasePath = filePath + minioFile.getPath(); +// // 检查文件是否为文件夹,如果是文件夹,跳过 +// File localFilePath = new File(localBasePath + localFile.getName()); +// if (localFilePath.isDirectory()) { +// LOGGER.error("路径指向的是一个文件夹,跳过该文件夹:" + localFilePath.getAbsolutePath()); +// continue; // 跳过文件夹 +// } +// +// // 检查文件大小是否匹配 +// if (minioFile.getSize() != null && localFile.getSize() != null && minioFile.getSize().longValue() != localFile.getSize().longValue()) { +// sizeMismatchedFiles.add(minioFile.getName()); +// } +// // 如果文件大小一致,进一步检查 MD5 +// try { +// // 计算本地文件的 MD5 +// String localMD5 = calculateMD5(new FileInputStream(localFilePath)); +// if (localMD5 == null || localMD5.isEmpty()) { +// continue; +// } +// +// // 获取 MinIO 文件的 MD5 +// String minioMD5; +// AbstractBaseFileService fileServiceMinio = storageSourceContext.getByStorageKey("minio"); +// String key = StringUtils.concat(minioFile.getPath(), minioFile.getName()); // 构造文件的 key +// if (minioFile.getSize() <= 5 * 1024 * 1024 * 1024L) { // 小于 5GB,使用 ETag +// ObjectMetadata metadata = fileServiceMinio.getObjectMetadata(bucketName, key); +// minioMD5 = metadata.getETag().replace("\"", ""); // 去除 ETag 的引号 +// } else { // 大于 5GB,手动计算 MD5 +// S3Object s3Object = fileServiceMinio.getObject(bucketName, key); +// minioMD5 = calculateMD5(s3Object.getObjectContent()); +// } +// +// // 比较 MD5 +// if (!minioMD5.equals(localMD5)) { +// minioFile.setLocatMd5(localMD5); +// minioFile.setMinioMd5(minioMD5); +// md5MismatchedFiles.add(minioFile); +// } +// } catch (Exception e) { +// LOGGER.error("计算 MD5 失败:" + e.getMessage()); +// } +// +// } else { +// // 如果 MinIO 中没有该文件,本地有的情况下,跳过该文件 +// LOGGER.info("MinIO 中没有该文件,本地有文件,跳过对比: " + minioFile.getName()); +// } +// } +// tsFiles.setLocalOnlyFiles(localOnlyFiles); +// tsFiles.setMinioOnlyFiles(minioOnlyFiles); +// tsFiles.setMd5MismatchedFiles(md5MismatchedFiles); +// LOGGER.info("文件大小不一致的文件:" + sizeMismatchedFiles); +// LOGGER.info("文件大小一致但 MD5 不一致的文件:" + md5MismatchedFiles); +// return tsFiles; +// +// } +// +// +// public String calculateMD5(InputStream inputStream) throws Exception { +// MessageDigest md = MessageDigest.getInstance("MD5"); +// byte[] buffer = new byte[8192]; +// int bytesRead; +// while ((bytesRead = inputStream.read(buffer)) != -1) { +// md.update(buffer, 0, bytesRead); +// } +// byte[] digest = md.digest(); +// StringBuilder sb = new StringBuilder(); +// for (byte b : digest) { +// sb.append(String.format("%02x", b)); +// } +// return sb.toString(); +// } /*******************************************************本地和minio对比结束****************************************************************/ @@ -1647,7 +2064,7 @@ public class TsFilesServiceImpl extends ServiceImpl impl newFolderRequest.setStorageKey("minio");//存储源 key,示例值(local minio) AbstractBaseFileService fileService = storageSourceContext.getByStorageKey(newFolderRequest.getStorageKey()); boolean flag = fileService.newFolder(newFolderRequest.getPath(), newFolderRequest.getName()); - LOGGER.info("同步本地到minio文件夹路径加名称" + newFolderRequest.getPath()+newFolderRequest.getName()); + LOGGER.info("同步本地到minio文件夹路径加名称" + newFolderRequest.getPath() + newFolderRequest.getName()); //如果文件夹创建成功 if (flag) { @@ -1695,7 +2112,7 @@ public class TsFilesServiceImpl extends ServiceImpl impl //把本地文件上传到minio AbstractBaseFileService fileService = storageSourceContext.getByStorageKey("minio"); boolean flag1 = fileService.UploadFiles(storageSourceConfig1.getValue(), minioPath + name, zipFile); - LOGGER.info("同步本地到minio文件路径加名称" + storageSourceConfig1.getValue()+minioPath + name); + LOGGER.info("同步本地到minio文件路径加名称" + storageSourceConfig1.getValue() + minioPath + name); //如果上传成功 修改 表结构 if (flag1) { @@ -1847,7 +2264,7 @@ public class TsFilesServiceImpl extends ServiceImpl impl newFolderRequest.setStorageKey("local");//存储源 key,示例值(local minio) AbstractBaseFileService fileService = storageSourceContext.getByStorageKey(newFolderRequest.getStorageKey()); boolean flag = fileService.newFolder(newFolderRequest.getPath(), newFolderRequest.getName()); - LOGGER.info("同步minio到本地文件夹路径加名称" + newFolderRequest.getPath()+ newFolderRequest.getName()); + LOGGER.info("同步minio到本地文件夹路径加名称" + newFolderRequest.getPath() + newFolderRequest.getName()); //如果文件夹创建成功 if (flag) { 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 7c3e59a..3dc7591 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 @@ -8,6 +8,7 @@ import com.yfd.platform.modules.experimentalData.domain.TsFiles; import com.yfd.platform.modules.experimentalData.domain.TsNodes; import com.yfd.platform.modules.experimentalData.mapper.TsFilesMapper; import com.yfd.platform.modules.experimentalData.mapper.TsNodesMapper; +import com.yfd.platform.modules.experimentalData.service.ITsFilesService; import com.yfd.platform.modules.experimentalData.service.ITsNodesService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.yfd.platform.modules.specialDocument.domain.Nodes; @@ -34,6 +35,7 @@ import javax.annotation.Resource; import java.sql.Timestamp; import java.time.LocalDateTime; import java.util.*; +import java.util.stream.Collectors; /** *

@@ -56,6 +58,11 @@ public class TsNodesServiceImpl extends ServiceImpl impl @Resource private TsFilesMapper tsFilesMapper; + //实验任务文档表 + @Resource + private ITsFilesService tsFilesService; + + //数据源Mapper @Resource private StorageSourceMapper storageSourceMapper; @@ -218,7 +225,6 @@ public class TsNodesServiceImpl extends ServiceImpl impl result.add(nodeWithChildren); // 将当前节点加入结果列表 } } - // 如果当前节点符合条件且没有被添加到result,则将其添加 if (currentNode.get("nodeName") instanceof String && ((String) currentNode.get("nodeName")).contains(nodeName) && result.isEmpty()) { result.add(currentNode); // 将当前节点添加到结果列表 @@ -226,31 +232,6 @@ public class TsNodesServiceImpl extends ServiceImpl impl // 返回包含所有符合条件的树结构的列表 return result; - -// // 如果当前节点是目标节点,返回当前节点 -// if (currentNode.get("nodeName") instanceof String &&((String) currentNode.get("nodeName")).contains(nodeName)) { -// return Collections.singletonList(currentNode); -// } -//// if (nodeName.equals(currentNode.get("nodeName"))) { -//// return Collections.singletonList(currentNode); -//// } -// -// // 查找当前节点的所有子节点 -// List> children = findChildren(allNodes, currentNode.get("nodeId").toString()); -// -// // 递归查找目标节点 -// for (Map child : children) { -// List> childTree = buildTreeToTargetNode(child, allNodes, nodeName); -// if (!childTree.isEmpty()) { -// // 如果找到目标节点,将当前节点加入树中 -// Map nodeWithChildren = new HashMap<>(currentNode); -// nodeWithChildren.put("children", childTree); -// return Collections.singletonList(nodeWithChildren); -// } -// } -// -// // 如果未找到目标节点,返回空列表 -// return new ArrayList<>(); } @@ -291,56 +272,15 @@ public class TsNodesServiceImpl extends ServiceImpl impl } //序号 tsnodes.setNodeOrder(orderno); - //查询数据源 - List storageSources = storageSourceMapper.findAllOrderByOrderNum(); - - // 获取路径 - List pathNodes = new ArrayList<>(); - TsNodes nodesData = tsNodesMapper.selectById(tsnodes.getParentId()); - // 从当前节点向上遍历,直到根节点 - while (nodesData != null) { - pathNodes.add(nodesData.getNodeName()); - // 如果父节点是 "00",说明已经到了根节点,停止遍历 - if ("00".equals(nodesData.getParentId())) { - break; - } - // 获取父节点 - nodesData = tsNodesMapper.selectById(nodesData.getParentId()); // 修正:从 nodesData 中获取 parentId - } - // 反转路径,使其从根节点到当前节点 - Collections.reverse(pathNodes); - String path = "/" + String.join("/", pathNodes); - - //判断 local或者minio有没有成功 - List results = new ArrayList<>(); - for (StorageSource storageSource : storageSources) { - //新增节点的时候 创建文件夹 - NewFolderRequest newFolderRequest = new NewFolderRequest(); - newFolderRequest.setName(tsnodes.getNodeName());//新建的文件夹名称,示例值(/a/b/c) - newFolderRequest.setPassword("");//文件夹密码, 如果文件夹需要密码才能访问,则支持请求密码,示例值(123456) - newFolderRequest.setPath(path);//请求路径,示例值(/) - newFolderRequest.setStorageKey(storageSource.getType().toString());//存储源 key,示例值(local minio) - AbstractBaseFileService fileService = storageSourceContext.getByStorageKey(newFolderRequest.getStorageKey()); - boolean flag = fileService.newFolder(newFolderRequest.getPath(), newFolderRequest.getName()); - results.add(flag); - } - // 使用Java 8的Stream API检查列表中是否包含true - boolean hasTrue = results.stream().anyMatch(Boolean::booleanValue); - //如果是true 说明至少有一个生成了文件夹 下一步建立表数据 - if (hasTrue) { - int valueAdded = tsNodesMapper.insert(tsnodes); - if (valueAdded == 1) { - LOGGER.info("local和minio新增成功,表结构增加成功"); - return ResponseResult.success(); - } else { - LOGGER.error("local和minio新增成功,表结构增加失败"); - return ResponseResult.error(); - } - + int valueAdded = tsNodesMapper.insert(tsnodes); + if (valueAdded == 1) { + LOGGER.info("tsnodes表结构增加成功"); + return ResponseResult.success(); } else { - LOGGER.error("local和minio新增失败"); + LOGGER.error("tsnodes表结构增加失败"); return ResponseResult.error(); } + } /********************************** @@ -376,56 +316,12 @@ public class TsNodesServiceImpl extends ServiceImpl impl if (count > 0) { return ResponseResult.error("节点名称已存在!"); } - - //查询数据源 - List storageSources = storageSourceMapper.findAllOrderByOrderNum(); - - List pathNodes = new ArrayList<>(); - TsNodes nodesData = tsNodesMapper.selectById(tsnodes.getParentId()); - // 从当前节点向上遍历,直到根节点 - while (nodesData != null) { - pathNodes.add(nodesData.getNodeName()); - // 如果父节点是 "00",说明已经到了根节点,停止遍历 - if ("00".equals(nodesData.getParentId())) { - break; - } - // 获取父节点 - nodesData = tsNodesMapper.selectById(nodesData.getParentId()); // 修正:从 nodesData 中获取 parentId - } - // 反转路径,使其从根节点到当前节点 - Collections.reverse(pathNodes); - String path = String.join("/", pathNodes); - - //判断 local或者minio有没有成功 - List results = new ArrayList<>(); - for (StorageSource storageSource : storageSources) { - //修改文件夹名称 - RenameFolderRequest renameFolderRequest = new RenameFolderRequest(); - renameFolderRequest.setName(nodeNameOld);//重命名的原文件夹名称,示例值(movie) - renameFolderRequest.setNewName(nodeName);// 重命名后的文件名称,示例值(music) - renameFolderRequest.setPassword("");//文件夹密码, 如果文件夹需要密码才能访问,则支持请求密码,示例值(123456) - renameFolderRequest.setPath(path);//请求路径,示例值(/) - renameFolderRequest.setStorageKey(storageSource.getType().toString());//存储源 key,示例值(local minio) - AbstractBaseFileService fileService = storageSourceContext.getByStorageKey(renameFolderRequest.getStorageKey()); - boolean flag = fileService.renameFolder(renameFolderRequest.getPath(), renameFolderRequest.getName(), renameFolderRequest.getNewName()); - results.add(flag); - } - - // 使用Java 8的Stream API检查列表中是否包含true - boolean hasTrue = results.stream().anyMatch(Boolean::booleanValue); - //如果是true 说明至少有一个生成了文件夹 下一步建立表数据 - if (hasTrue) { - int valueAdded = tsNodesMapper.updateById(tsnodes); - if (valueAdded == 1) { - LOGGER.info("local和minio修改成功,表结构增加成功"); - return ResponseResult.success(); - } else { - LOGGER.error("local和minio修改成功,表结构增加失败"); - return ResponseResult.error(); - } - + int valueAdded = tsNodesMapper.updateById(tsnodes); + if (valueAdded == 1) { + LOGGER.info("tsnodes表结构修改成功"); + return ResponseResult.success(); } else { - LOGGER.error("local和minio修改失败"); + LOGGER.error("tsnodes表结构修改失败"); return ResponseResult.error(); } } @@ -437,105 +333,37 @@ public class TsNodesServiceImpl extends ServiceImpl impl ***********************************/ @Override public boolean deleteTsNodesById(String id) { - - -// try { - //根据ID 查询当前数据 TsNodes tsNodes = tsNodesMapper.selectById(id); //删除之前 先拼路径 然后删除本地和minio的文件夹 最后删除表结构 - // 删除当前节点 - int deleteCount = tsNodesMapper.deleteById(id); //删除当前节点的 文件 QueryWrapper queryWrapper1 = new QueryWrapper<>(); queryWrapper1.eq("node_id", tsNodes.getNodeId()); queryWrapper1.eq("task_id", tsNodes.getTaskId()); - tsFilesMapper.delete(queryWrapper1); + List tsFiles = tsFilesMapper.selectList(queryWrapper1); - - List pathNodes = new ArrayList<>(); - TsNodes nodesData = tsNodesMapper.selectById(tsNodes.getParentId()); - // 从当前节点向上遍历,直到根节点 - while (nodesData != null) { - pathNodes.add(nodesData.getNodeName()); - // 如果父节点是 "00",说明已经到了根节点,停止遍历 - if ("00".equals(nodesData.getParentId())) { - break; - } - // 获取父节点 - nodesData = tsNodesMapper.selectById(nodesData.getParentId()); // 修正:从 nodesData 中获取 parentId + List dataset = new ArrayList<>(); + if (tsFiles != null && !tsFiles.isEmpty()) { + dataset = tsFiles.stream() + .map(TsFiles::getId) // 假设 TsFiles 类中有 getId() 方法 + .collect(Collectors.toList()); } - // 反转路径,使其从根节点到当前节点 - Collections.reverse(pathNodes); - String path = String.join("/", pathNodes); - - List deleteItemList = new ArrayList<>(); - BatchDeleteRequest.DeleteItem deleteItemData = new BatchDeleteRequest.DeleteItem(); - deleteItemData.setName(tsNodes.getNodeName()); - deleteItemData.setPassword(""); - deleteItemData.setPath(path); - deleteItemData.setType(FileTypeEnum.FOLDER); - deleteItemList.add(deleteItemData); - - //查询数据源 - List storageSources = storageSourceMapper.findAllOrderByOrderNum(); - //判断 local或者minio有没有成功 - List results = new ArrayList<>(); - for (StorageSource storageSource : storageSources) { - - BatchDeleteRequest batchDeleteRequest = new BatchDeleteRequest(); - batchDeleteRequest.setDeleteItems(deleteItemList); - batchDeleteRequest.setStorageKey(storageSource.getKey()); - AbstractBaseFileService fileService = storageSourceContext.getByStorageKey(batchDeleteRequest.getStorageKey()); - List deleteItems = batchDeleteRequest.getDeleteItems(); - int deleteSuccessCount = 0, deleteFailCount = 0, totalCount = CollUtil.size(deleteItems); - for (BatchDeleteRequest.DeleteItem deleteItem : deleteItems) { - boolean flag = false; - try { - if (deleteItem.getType() == FileTypeEnum.FILE) { - flag = fileService.deleteFile(deleteItem.getPath(), deleteItem.getName()); - } else if (deleteItem.getType() == FileTypeEnum.FOLDER) { - flag = fileService.deleteFolder(deleteItem.getPath(), deleteItem.getName()); - } - - if (flag) { - deleteSuccessCount++; - } else { - deleteFailCount++; - } - } catch (Exception e) { - LOGGER.error("删除文件/文件夹失败, 文件路径: {}, 文件名称: {}", deleteItem.getPath(), deleteItem.getName(), e); - deleteFailCount++; - } - } - if (totalCount > 1) { - //return ResponseResult.success("批量删除 " + totalCount + " 个, 删除成功 " + deleteSuccessCount + " 个, 失败 " + deleteFailCount + " 个."); - LOGGER.error("批量删除 " + totalCount + " 个, 删除成功 " + deleteSuccessCount + " 个, 失败 " + deleteFailCount + " 个."); - } else { - //return totalCount == deleteSuccessCount ? ResponseResult.success("删除成功") : ResponseResult.error("删除失败"); - LOGGER.error("批量删除 " + totalCount + " 个, 删除成功 " + deleteSuccessCount + " 个, 失败 " + deleteFailCount + " 个."); - - } - //如果是1 说明成功删除 - if (deleteSuccessCount >= 1) { - results.add(true); - } else { - results.add(false); - } + //批量删除TsFiles表数据 + if (dataset.size() > 0) { + tsFilesService.deleteTsFilesByIds(dataset, "local"); } - // 使用Java 8的Stream API检查列表中是否包含true - boolean hasTrue = results.stream().anyMatch(Boolean::booleanValue); - if (hasTrue) { - // 递归删除子节点 - deleteChildren(tsNodes.getNodeId(), tsNodes.getTaskId()); + + // 递归删除子节点 + deleteChildren(tsNodes.getNodeId(), tsNodes.getTaskId()); + // 删除当前节点 + int deleteCount = tsNodesMapper.deleteById(id); + if (deleteCount == 1) { + LOGGER.info("tsnodes表结删除改成功"); + return true; + } else { + LOGGER.error("tsnodes表结构删除失败"); + return false; } - return hasTrue; - -// } catch (Exception e) { -// // 如果发生异常,返回 false -// return false; -// } - } /** @@ -556,9 +384,22 @@ public class TsNodesServiceImpl extends ServiceImpl impl tsNodesMapper.deleteById(child.getNodeId()); // 删除当前子节点 //批量文件的数据 QueryWrapper queryWrapper1 = new QueryWrapper<>(); - queryWrapper1.eq("node_id", parentId); + queryWrapper1.eq("node_id", child.getNodeId()); queryWrapper1.eq("task_id", taskId); - tsFilesMapper.delete(queryWrapper1); + List tsFiles = tsFilesMapper.selectList(queryWrapper1); + + List dataset = new ArrayList<>(); + if (tsFiles != null && !tsFiles.isEmpty()) { + dataset = tsFiles.stream() + .map(TsFiles::getId) // 假设 TsFiles 类中有 getId() 方法 + .collect(Collectors.toList()); + } + //批量删除TsFiles表数据 + if (dataset.size() > 0) { + tsFilesService.deleteTsFilesByIds(dataset, "local"); + } } } + + } 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 239d7b2..0399a7f 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 @@ -164,17 +164,19 @@ public abstract class AbstractS3BaseFileService

extends A public List s3FileListData(String path, String name) { List fileItemList = new ArrayList<>(); String bucketName = param.getBucketName(); - if(path == null || StringUtils.isEmpty(path)){ + if (path == null || StringUtils.isEmpty(path)) { return fileItemList; } - path = ensurePathWithSlash(path); // 确保路径以斜杠开头 - String fullPath = StringUtils.trimStartSlashes(StringUtils.concat(param.getBasePath(), path, ZFileConstant.PATH_SEPARATOR)); + // 确保路径格式正确,并拼接目标文件夹名称(name) + path = ensurePathWithSlash(path); // 输入 path="/",处理为 "/" + String targetPath = path + name + ZFileConstant.PATH_SEPARATOR; // 目标路径:/431/ + String fullPath = StringUtils.trimStartSlashes( + StringUtils.concat(param.getBasePath(), targetPath) + ); - - // 新增 includeAll 参数控制是否包含所有子项 - listFilesInDirectory(bucketName, fullPath, path, name, fileItemList, false); - + // 调用时传入 name=null,确保递归时不再匹配其他同名文件夹 + listFilesInDirectory(bucketName, fullPath, targetPath, null, fileItemList, false); return fileItemList; } @@ -184,11 +186,11 @@ public abstract class AbstractS3BaseFileService

extends A String path, String name, List fileItemList, - boolean includeAll // 新增参数:是否强制包含所有内容 + boolean includeAll ) { ListObjectsRequest listObjectsRequest = new ListObjectsRequest() .withBucketName(bucketName) - .withPrefix(fullPath) + .withPrefix(fullPath) // 关键修复:直接通过 Prefix 限定目标路径 .withMaxKeys(1000) .withDelimiter("/"); @@ -209,15 +211,15 @@ public abstract class AbstractS3BaseFileService

extends A fileName = fileName.substring(1); } - // 包含条件:强制包含 或 名称匹配 - if (includeAll || StrUtil.isEmpty(name) || fileName.equals(name)) { + // 包含条件:强制包含 或 名称匹配(仅在初始调用时检查 name) + if (includeAll || name == null || StrUtil.isEmpty(name) || fileName.equals(name)) { FileItemResult item = new FileItemResult(); item.setName(fileName); item.setSize(s.getSize()); item.setTime(s.getLastModified()); item.setType(FileTypeEnum.FILE); - item.setPath(path); // 路径保持不变 - item.setUrl(getDownloadUrl(ensurePathWithSlash(path) + fileName)); // 确保路径以斜杠开头 + item.setPath(path); + item.setUrl(getDownloadUrl(ensurePathWithSlash(path) + fileName)); fileItemList.add(item); } } @@ -227,27 +229,27 @@ public abstract class AbstractS3BaseFileService

extends A String folderName = commonPrefix.substring(fullPath.length(), commonPrefix.length() - 1); if (StrUtil.isEmpty(folderName) || folderName.equals(StringUtils.DELIMITER_STR)) continue; - // 判断是否匹配名称或需要强制包含 - boolean matchFolder = includeAll || StrUtil.isEmpty(name) || folderName.equals(name); + // 匹配条件:仅在初始调用时检查 name,递归时 name=null 直接包含 + boolean matchFolder = includeAll || name == null || StrUtil.isEmpty(name) || folderName.equals(name); if (matchFolder) { FileItemResult folderItem = new FileItemResult(); folderItem.setName(folderName); folderItem.setType(FileTypeEnum.FOLDER); - folderItem.setPath(path); // 路径保持不变 + folderItem.setPath(path); fileItemList.add(folderItem); } - // 递归处理子文件夹(如果匹配则强制包含子项) - String subPath = ensurePathWithSlash(path) + folderName + ZFileConstant.PATH_SEPARATOR; // 确保路径以斜杠开头并正确拼接 + // 递归处理子文件夹时,name=null,强制包含所有子项 + String subPath = ensurePathWithSlash(path) + folderName + ZFileConstant.PATH_SEPARATOR; String subFullPath = commonPrefix; listFilesInDirectory( bucketName, subFullPath, subPath, - name, + null, // 关键修复:递归时不再传递 name,避免深层匹配 fileItemList, - matchFolder // 关键修改:如果父文件夹匹配,则强制包含所有子项 + matchFolder // 如果父级匹配,则强制包含子项 ); } diff --git a/java/src/main/java/com/yfd/platform/modules/storage/service/impl/LocalServiceImpl.java b/java/src/main/java/com/yfd/platform/modules/storage/service/impl/LocalServiceImpl.java index 95a27d4..c134c23 100644 --- a/java/src/main/java/com/yfd/platform/modules/storage/service/impl/LocalServiceImpl.java +++ b/java/src/main/java/com/yfd/platform/modules/storage/service/impl/LocalServiceImpl.java @@ -191,40 +191,60 @@ public class LocalServiceImpl extends AbstractProxyTransferService { List resultList = new ArrayList<>(); String basePath = param.getFilePath(); - // 处理根目录特殊情况 - String fullPath = folderPath.equals("/") - ? Paths.get(basePath).toString() - : Paths.get(basePath, folderPath).toString(); + // 1. 构建目标路径(确保前后有 /) + String targetPath = formatCombinedPath(folderPath, name); // 关键修改点 + + // 2. 拼接完整物理路径 + String fullPath = Paths.get(basePath, targetPath).toString(); File targetDir = new File(fullPath); - if (!targetDir.exists()) { throw new FileNotFoundException("路径不存在: " + fullPath); } - // 不添加目标文件夹本身到结果列表(与示例数据结构一致) - // resultList.add(convertToFileItem(targetDir, folderPath)); - - // 列出目标目录内容 + // 3. 列出目录内容 if (targetDir.isDirectory()) { - String effectiveParentPath = folderPath.endsWith("/") - ? folderPath - : folderPath + "/"; - listFilesInDirectory(targetDir, effectiveParentPath, resultList); + listFilesInDirectory(targetDir, targetPath, resultList); } return resultList; } /** - * 递归列出文件夹下的所有内容 + * 构建规范化路径(根路径为 /,其他路径为 /path/) + */ + private String formatCombinedPath(String folderPath, String name) { + StringBuilder pathBuilder = new StringBuilder(); + + // 处理 folderPath + if (StringUtils.isBlank(folderPath) || folderPath.equals("/")) { + pathBuilder.append("/"); + } else { + pathBuilder.append(folderPath.trim()); + if (!folderPath.endsWith("/")) { + pathBuilder.append("/"); + } + } + + // 处理 name + if (StringUtils.isNotBlank(name)) { + String sanitizedName = name.trim().replaceAll("/+", ""); // 防止注入额外路径 + pathBuilder.append(sanitizedName).append("/"); + } + + // 最终格式化(替换多斜杠为单斜杠) + return pathBuilder.toString().replaceAll("/+", "/"); + } + + /** + * 递归列出目录内容 */ private void listFilesInDirectory(File directory, String parentPath, List resultList) { File[] files = directory.listFiles(); if (files == null) return; for (File file : files) { - // 创建文件项:父路径不带文件名 + // 创建文件项(确保路径格式) FileItemResult item = convertToFileItem(file, parentPath); resultList.add(item); @@ -237,12 +257,12 @@ public class LocalServiceImpl extends AbstractProxyTransferService { } /** - * 转换文件对象为结果对象 + * 转换文件对象为结果对象(路径前后带 /) */ private FileItemResult convertToFileItem(File file, String parentPath) { FileItemResult item = new FileItemResult(); item.setName(file.getName()); - item.setPath(formatPath(parentPath)); // 父路径不带当前文件名 + item.setPath(formatSinglePath(parentPath)); // 关键修改点 item.setType(file.isDirectory() ? FileTypeEnum.FOLDER : FileTypeEnum.FILE); item.setSize(file.isDirectory() ? 0 : file.length()); item.setTime(new Date(file.lastModified())); @@ -250,13 +270,29 @@ public class LocalServiceImpl extends AbstractProxyTransferService { } /** - * 统一格式化路径 + * 单一路径格式化规则: + * - 根路径返回 / + * - 其他路径返回 /path/ */ - private String formatPath(String path) { - // 保证路径以/开头且不以/结尾(示例数据结构风格) + private String formatSinglePath(String path) { + if (StringUtils.isBlank(path)) return "/"; + + // 替换多斜杠为单斜杠 path = path.replaceAll("/+", "/"); - if (!path.startsWith("/")) path = "/" + path; - if (path.endsWith("/") && path.length() > 1) path = path.substring(0, path.length()-1); + + // 确保以 / 开头 + if (!path.startsWith("/")) { + path = "/" + path; + } + + // 根路径直接返回 + if (path.equals("/")) return path; + + // 确保以 / 结尾 + if (!path.endsWith("/")) { + path += "/"; + } + return path; } diff --git a/java/src/main/resources/application-dev.yml b/java/src/main/resources/application-dev.yml index 3c4f058..7be4ef2 100644 --- a/java/src/main/resources/application-dev.yml +++ b/java/src/main/resources/application-dev.yml @@ -11,19 +11,19 @@ spring: druid: master: driverClassName: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://120.27.210.161:3306/testdb?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true - username: testdb - password: 27CTfsyJmZRESmsa -# url: jdbc:mysql://121.37.111.42:33306/filemanagedb?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true -# username: filemanagedb -# password: GAPchydbCKYFjjAa +# url: jdbc:mysql://120.27.210.161:3306/testdb?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true +# username: testdb +# password: 27CTfsyJmZRESmsa + url: jdbc:mysql://121.37.111.42:3306/filemanagedb?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true + username: filemanagedb + password: GAPchydbCKYFjjAa mvc: pathmatch: matching-strategy: ant_path_matcher servlet: multipart: - max-file-size: 30MB - max-request-size: 100MB + max-file-size: 50GB + max-request-size: 50GB logging: file: name: logs/projectname.log @@ -34,7 +34,7 @@ logging: # 在线文档: swagger-ui(生产环境建议关闭) swagger-ui: - enabled: true + enabled: flase mybatis-plus: configuration: default-enum-type-handler: com.yfd.platform.config.MybatisEnumTypeHandler