diff --git a/data/src/main/java/com/sdm/data/service/IMinioService.java b/data/src/main/java/com/sdm/data/service/IMinioService.java index 972a75e3..3e97fb7f 100644 --- a/data/src/main/java/com/sdm/data/service/IMinioService.java +++ b/data/src/main/java/com/sdm/data/service/IMinioService.java @@ -78,6 +78,15 @@ public interface IMinioService { void copyFile(String oldObjectName, String newObjectName, String bucketName); + /** + * 递归重命名目录(将目录下所有对象从旧前缀迁移到新前缀) + * + * @param oldDirPath 旧目录路径(objectKey前缀) + * @param newDirPath 新目录路径(objectKey前缀) + * @param bucketName 桶名称 + */ + void renameDirectoryRecursively(String oldDirPath, String newDirPath, String bucketName); + /** * 从MinIO下载文件 * @param objectName 文件名(objectKey) diff --git a/data/src/main/java/com/sdm/data/service/impl/MinioFileIDataFileServiceImpl.java b/data/src/main/java/com/sdm/data/service/impl/MinioFileIDataFileServiceImpl.java index 3b3150bd..fe7e06ad 100644 --- a/data/src/main/java/com/sdm/data/service/impl/MinioFileIDataFileServiceImpl.java +++ b/data/src/main/java/com/sdm/data/service/impl/MinioFileIDataFileServiceImpl.java @@ -835,29 +835,55 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService { Long fileId = req.getFileId(); try { FileMetadataInfo oldFile = fileMetadataInfoService.lambdaQuery().eq(FileMetadataInfo::getId, fileId).one(); - bucketName = oldFile.getBucketName(); if (ObjectUtils.isEmpty(oldFile)) { return SdmResponse.failed("文件不存在"); } - /*boolean hasDeletePermission = fileUserPermissionService.hasFilePermission(fileId, ThreadLocalContext.getUserId(), FilePermissionEnum.WRITE); - if (!hasDeletePermission){ - return SdmResponse.failed("没有修改权限"); - }*/ + bucketName = oldFile.getBucketName(); + String newName = req.getNewName(); + // 1. 校验名称是否发生变化,如果未变化则直接返回成功,避免 MinIO 原地拷贝报错 + if (Objects.equals(newName, oldFile.getOriginalName())) { + return SdmResponse.success("名称未改变"); + } + + // 2. 校验同级目录下是否存在同名文件,防止覆盖或数据库记录冲突 + boolean exists = fileMetadataInfoService.lambdaQuery() + .eq(FileMetadataInfo::getParentId, oldFile.getParentId()) + .eq(FileMetadataInfo::getOriginalName, newName) + .eq(FileMetadataInfo::getTenantId, ThreadLocalContext.getTenantId()) + .exists(); + if (exists) { + return SdmResponse.failed("当前目录下已存在同名文件"); + } + oldObjectKey = oldFile.getObjectKey(); newObjectKey = oldObjectKey.substring(0, oldObjectKey.lastIndexOf("/") + 1) + newName; - minioService.renameFile(oldObjectKey, newObjectKey,bucketName); + minioService.renameFile(oldObjectKey, newObjectKey, bucketName); fileMetadataInfoService.lambdaUpdate() .set(FileMetadataInfo::getObjectKey, newObjectKey) .set(FileMetadataInfo::getOriginalName, newName) - .set(FileMetadataInfo::getUpdateTime, new Date()) + .set(FileMetadataInfo::getUpdateTime, LocalDateTime.now()) .set(FileMetadataInfo::getUpdaterId, ThreadLocalContext.getUserId()) .eq(FileMetadataInfo::getId, fileId).update(); + + // 更新关联表中的名称或路径 + fileStorageService.lambdaUpdate() + .set(FileStorage::getFileName, newName) + .eq(FileStorage::getFileId, fileId) + .update(); + return SdmResponse.success("重命名成功"); } catch (Exception e) { log.error("重命名文件失败", e); - minioService.renameFile(newObjectKey, oldObjectKey,bucketName); + // 只有当 MinIO 操作成功但后续数据库更新失败时,才尝试回滚 MinIO 路径 + if (oldObjectKey != null && newObjectKey != null) { + try { + minioService.renameFile(newObjectKey, oldObjectKey, bucketName); + } catch (Exception re) { + log.error("重命名失败回滚 MinIO 路径失败", re); + } + } throw new RuntimeException("重命名文件失败: " + e.getMessage(), e); } } @@ -992,43 +1018,120 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService { } @Override + @Transactional(rollbackFor = Exception.class) public SdmResponse renameDirNew(RenameDirReq req) { - FileMetadataInfo dirMetadataInfo = null; - if (ObjectUtils.isNotEmpty(req.getUuid()) ) { - dirMetadataInfo = fileMetadataInfoService.lambdaQuery().eq(FileMetadataInfo::getRelatedResourceUuid, req.getUuid()).one(); - } else { - dirMetadataInfo = fileMetadataInfoService.lambdaQuery().eq(FileMetadataInfo::getId, req.getDirId()).one(); - } - + // 1. 获取目录元数据 + FileMetadataInfo dirMetadataInfo = queryDirMetadataInfo(req); if (ObjectUtils.isEmpty(dirMetadataInfo)) { return SdmResponse.failed("文件夹不存在"); } - String oldDirMinioObjectKey = dirMetadataInfo.getObjectKey(); - - boolean hasWritePermission = fileUserPermissionService.hasFilePermission(dirMetadataInfo.getId(), ThreadLocalContext.getUserId(), FilePermissionEnum.WRITE); + // 2. 权限校验 + boolean hasWritePermission = fileUserPermissionService.hasFilePermission( + dirMetadataInfo.getId(), ThreadLocalContext.getUserId(), FilePermissionEnum.WRITE); if (!hasWritePermission) { return SdmResponse.failed("没有写入权限"); } - // 修正路径处理逻辑 - String newDirMinioObjectKey = getDirMinioObjectKey(oldDirMinioObjectKey.substring(0, oldDirMinioObjectKey.lastIndexOf(dirMetadataInfo.getOriginalName())) + req.getNewName()); + + String newName = req.getNewName(); + String oldName = dirMetadataInfo.getOriginalName(); + + // 3. 名称变化校验:若名称未改变,直接返回,避免 MinIO 原地拷贝报错 + if (Objects.equals(newName, oldName)) { + return SdmResponse.success("名称未改变"); + } + + // 4. 同名冲突校验:检查同级目录下是否存在同名文件夹 + boolean exists = fileMetadataInfoService.lambdaQuery() + .eq(FileMetadataInfo::getParentId, dirMetadataInfo.getParentId()) + .eq(FileMetadataInfo::getOriginalName, newName) + .eq(FileMetadataInfo::getTenantId, ThreadLocalContext.getTenantId()) + .exists(); + if (exists) { + return SdmResponse.failed("当前目录下已存在同名文件夹"); + } + + String oldDirMinioObjectKey = dirMetadataInfo.getObjectKey(); + String bucketName = dirMetadataInfo.getBucketName(); + // 计算新的目录 objectKey + String parentPath = oldDirMinioObjectKey.substring(0, oldDirMinioObjectKey.lastIndexOf(oldName)); + String newDirMinioObjectKey = getDirMinioObjectKey(parentPath + newName); try { - minioService.renameFile(oldDirMinioObjectKey, newDirMinioObjectKey,dirMetadataInfo.getBucketName()); + // 5. MinIO 递归重命名:将目录下所有对象从旧前缀迁移到新前缀 + minioService.renameDirectoryRecursively(oldDirMinioObjectKey, newDirMinioObjectKey, bucketName); + + // 6. 数据库递归更新:更新所有子文件/子目录的 objectKey + updateChildrenObjectKeys(oldDirMinioObjectKey, newDirMinioObjectKey, bucketName); + + // 7. 更新当前目录记录 fileMetadataInfoService.lambdaUpdate() .set(FileMetadataInfo::getObjectKey, newDirMinioObjectKey) - .set(FileMetadataInfo::getOriginalName, req.getNewName()) - .set(FileMetadataInfo::getUpdateTime, new Date()) + .set(FileMetadataInfo::getOriginalName, newName) + .set(FileMetadataInfo::getUpdateTime, LocalDateTime.now()) .set(FileMetadataInfo::getUpdaterId, ThreadLocalContext.getUserId()) - .eq(FileMetadataInfo::getId, dirMetadataInfo.getId()).update(); + .eq(FileMetadataInfo::getId, dirMetadataInfo.getId()) + .update(); + + // 8. 更新 file_storage 表中当前目录的 fileName + fileStorageService.lambdaUpdate() + .set(FileStorage::getFileName, newName) + .eq(FileStorage::getDirId, dirMetadataInfo.getId()) + .update(); + return SdmResponse.success("重命名目录成功"); } catch (Exception e) { - log.error("重命名目录失败", e); - minioService.renameFile(newDirMinioObjectKey, oldDirMinioObjectKey,dirMetadataInfo.getBucketName()); + log.error("重命名目录失败, oldKey={}, newKey={}", oldDirMinioObjectKey, newDirMinioObjectKey, e); + // 尝试回滚 MinIO(注:若部分成功部分失败,回滚可能不完整,需人工介入) + try { + minioService.renameDirectoryRecursively(newDirMinioObjectKey, oldDirMinioObjectKey, bucketName); + } catch (Exception re) { + log.error("重命名失败后回滚 MinIO 路径失败", re); + } throw new RuntimeException("重命名目录失败: " + e.getMessage(), e); } } + /** + * 查询目录元数据信息 + */ + private FileMetadataInfo queryDirMetadataInfo(RenameDirReq req) { + if (ObjectUtils.isNotEmpty(req.getUuid())) { + return fileMetadataInfoService.lambdaQuery() + .eq(FileMetadataInfo::getRelatedResourceUuid, req.getUuid()) + .one(); + } + return fileMetadataInfoService.lambdaQuery() + .eq(FileMetadataInfo::getId, req.getDirId()) + .one(); + } + + /** + * 递归更新数据库中所有子项的 objectKey + * + * @param oldPrefix 旧的 objectKey 前缀 + * @param newPrefix 新的 objectKey 前缀 + * @param bucketName 桶名称 + */ + private void updateChildrenObjectKeys(String oldPrefix, String newPrefix, String bucketName) { + // 使用 SQL 的 REPLACE 函数批量更新所有以 oldPrefix 开头的 objectKey + fileMetadataInfoService.lambdaQuery() + .likeRight(FileMetadataInfo::getObjectKey, oldPrefix) + .eq(FileMetadataInfo::getBucketName, bucketName) + .eq(FileMetadataInfo::getTenantId, ThreadLocalContext.getTenantId()) + .list() + .forEach(child -> { + String oldKey = child.getObjectKey(); + String newKey = newPrefix + oldKey.substring(oldPrefix.length()); + fileMetadataInfoService.lambdaUpdate() + .set(FileMetadataInfo::getObjectKey, newKey) + .set(FileMetadataInfo::getUpdateTime, LocalDateTime.now()) + .set(FileMetadataInfo::getUpdaterId, ThreadLocalContext.getUserId()) + .eq(FileMetadataInfo::getId, child.getId()) + .update(); + }); + } + @Override public SdmResponse copyFileToTask(CopyFileToTaskReq req) { // 源文件 diff --git a/data/src/main/java/com/sdm/data/service/minio/MinioService.java b/data/src/main/java/com/sdm/data/service/minio/MinioService.java index 70d86788..23ba4faa 100644 --- a/data/src/main/java/com/sdm/data/service/minio/MinioService.java +++ b/data/src/main/java/com/sdm/data/service/minio/MinioService.java @@ -416,6 +416,72 @@ public class MinioService implements IMinioService { } } + /** + * 递归重命名目录(将目录下所有对象从旧前缀迁移到新前缀) + * + * @param oldDirPath 旧目录路径(objectKey前缀) + * @param newDirPath 新目录路径(objectKey前缀) + * @param bucketName 桶名称 + */ + @Override + public void renameDirectoryRecursively(String oldDirPath, String newDirPath, String bucketName) { + try { + bucketName = getBucketName(bucketName); + // 确保目录路径以 "/" 结尾 + oldDirPath = dealDirPath(oldDirPath); + newDirPath = dealDirPath(newDirPath); + + // 列出旧目录下的所有对象 + Iterable> results = minioClient.listObjects( + ListObjectsArgs.builder() + .bucket(bucketName) + .prefix(oldDirPath) + .recursive(true) + .build()); + + List oldObjectKeys = new ArrayList<>(); + for (Result result : results) { + Item item = result.get(); + oldObjectKeys.add(item.objectName()); + } + + // 逐个拷贝到新路径 + for (String oldKey : oldObjectKeys) { + String newKey = newDirPath + oldKey.substring(oldDirPath.length()); + minioClient.copyObject( + CopyObjectArgs.builder() + .bucket(bucketName) + .object(newKey) + .source(CopySource.builder() + .bucket(bucketName) + .object(oldKey) + .build()) + .build()); + } + + // 批量删除旧对象 + if (!oldObjectKeys.isEmpty()) { + List deleteObjects = oldObjectKeys.stream() + .map(DeleteObject::new) + .collect(Collectors.toList()); + + Iterable> deleteErrors = minioClient.removeObjects( + RemoveObjectsArgs.builder() + .bucket(bucketName) + .objects(deleteObjects) + .build()); + + for (Result deleteErrorResult : deleteErrors) { + DeleteError deleteError = deleteErrorResult.get(); + log.error("删除旧对象失败: {}, 错误: {}", deleteError.objectName(), deleteError.message()); + } + } + } catch (Exception e) { + log.error("递归重命名目录失败: 从 {} 到 {}", oldDirPath, newDirPath, e); + throw new RuntimeException("递归重命名目录失败: " + e.getMessage(), e); + } + } + /** * 从MinIO下载文件