|
24 | 24 | import com.datamate.datamanagement.infrastructure.persistence.repository.DatasetRepository; |
25 | 25 | import com.datamate.datamanagement.interfaces.converter.DatasetConverter; |
26 | 26 | import com.datamate.datamanagement.interfaces.dto.AddFilesRequest; |
| 27 | +import com.datamate.datamanagement.interfaces.dto.BatchDeleteFilesRequest; |
27 | 28 | import com.datamate.datamanagement.interfaces.dto.CreateDirectoryRequest; |
28 | 29 | import com.datamate.datamanagement.interfaces.dto.UploadFileRequest; |
29 | 30 | import com.datamate.datamanagement.interfaces.dto.UploadFilesPreRequest; |
@@ -239,21 +240,89 @@ public void deleteDatasetFile(String datasetId, String fileId, String prefix) { |
239 | 240 | // 删除文件时,上传到数据集中的文件会同时删除数据库中的记录和文件系统中的文件,归集过来的文件仅删除数据库中的记录 |
240 | 241 | if (file.getFilePath().startsWith(dataset.getPath())) { |
241 | 242 | try { |
242 | | - Path filePath = Paths.get(file.getFilePath()); |
| 243 | + Path filePath = validateAndResolvePath(file.getFilePath(), dataset.getPath()); |
243 | 244 | Files.deleteIfExists(filePath); |
244 | 245 | } catch (IOException ex) { |
245 | 246 | throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR); |
246 | 247 | } |
247 | 248 | } |
248 | 249 | } |
249 | 250 |
|
| 251 | + /** |
| 252 | + * 批量删除文件 |
| 253 | + */ |
| 254 | + @Transactional |
| 255 | + public void batchDeleteFiles(String datasetId, BatchDeleteFilesRequest request) { |
| 256 | + Dataset dataset = datasetRepository.getById(datasetId); |
| 257 | + if (dataset == null) { |
| 258 | + throw BusinessException.of(DataManagementErrorCode.DATASET_NOT_FOUND); |
| 259 | + } |
| 260 | + |
| 261 | + List<String> fileIds = request.getFileIds(); |
| 262 | + if (fileIds == null || fileIds.isEmpty()) { |
| 263 | + throw BusinessException.of(CommonErrorCode.PARAM_ERROR); |
| 264 | + } |
| 265 | + |
| 266 | + List<DatasetFile> filesToDelete = new ArrayList<>(); |
| 267 | + List<String> failedFileIds = new ArrayList<>(); |
| 268 | + |
| 269 | + for (String fileId : fileIds) { |
| 270 | + try { |
| 271 | + DatasetFile file = getDatasetFile(dataset, fileId, request.getPrefix()); |
| 272 | + filesToDelete.add(file); |
| 273 | + datasetFileRepository.removeById(fileId); |
| 274 | + } catch (Exception e) { |
| 275 | + log.error("Failed to delete file with id: {}", fileId, e); |
| 276 | + failedFileIds.add(fileId); |
| 277 | + } |
| 278 | + } |
| 279 | + |
| 280 | + // 更新数据集(避免 ConcurrentModificationException) |
| 281 | + List<DatasetFile> datasetFiles = dataset.getFiles(); |
| 282 | + if (datasetFiles != null) { |
| 283 | + // 创建一个新的列表来存储要保留的文件 |
| 284 | + List<DatasetFile> remainingFiles = new ArrayList<>(datasetFiles); |
| 285 | + // 移除要删除的文件 |
| 286 | + remainingFiles.removeAll(filesToDelete); |
| 287 | + dataset.setFiles(remainingFiles); |
| 288 | + } |
| 289 | + datasetRepository.updateById(dataset); |
| 290 | + |
| 291 | + // 删除文件系统中的文件 |
| 292 | + for (DatasetFile file : filesToDelete) { |
| 293 | + // 上传到数据集中的文件会同时删除数据库中的记录和文件系统中的文件,归集过来的文件仅删除数据库中的记录 |
| 294 | + if (file.getFilePath().startsWith(dataset.getPath())) { |
| 295 | + try { |
| 296 | + Path filePath = validateAndResolvePath(file.getFilePath(), dataset.getPath()); |
| 297 | + Files.deleteIfExists(filePath); |
| 298 | + } catch (IllegalArgumentException ex) { |
| 299 | + log.warn("Invalid file path detected, skipping deletion: {}", file.getFilePath()); |
| 300 | + } catch (IOException ex) { |
| 301 | + log.error("Failed to delete file from filesystem: {}", file.getFilePath(), ex); |
| 302 | + } |
| 303 | + } |
| 304 | + } |
| 305 | + |
| 306 | + // 如果有失败的文件,记录日志但不抛出异常 |
| 307 | + if (!failedFileIds.isEmpty()) { |
| 308 | + log.warn("Failed to delete {} files out of {}", failedFileIds.size(), fileIds.size()); |
| 309 | + } |
| 310 | + } |
| 311 | + |
250 | 312 | /** |
251 | 313 | * 下载文件 |
252 | 314 | */ |
253 | 315 | @Transactional(readOnly = true) |
254 | 316 | public Resource downloadFile(DatasetFile file) { |
255 | 317 | try { |
256 | | - Path filePath = Paths.get(file.getFilePath()).normalize(); |
| 318 | + // 获取对应的数据集以验证路径安全性 |
| 319 | + Dataset dataset = datasetRepository.getById(file.getDatasetId()); |
| 320 | + if (dataset == null) { |
| 321 | + throw new RuntimeException("Dataset not found for file: " + file.getFileName()); |
| 322 | + } |
| 323 | + |
| 324 | + // 验证路径安全性,防止路径遍历攻击 |
| 325 | + Path filePath = validateAndResolvePath(file.getFilePath(), dataset.getPath()); |
257 | 326 | log.info("start download file {}", file.getFilePath()); |
258 | 327 | Resource resource = new UrlResource(filePath.toUri()); |
259 | 328 | if (resource.exists()) { |
@@ -637,10 +706,14 @@ public void deleteDirectory(String datasetId, String prefix) { |
637 | 706 | throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR); |
638 | 707 | } |
639 | 708 |
|
640 | | - // 更新数据集 |
641 | | - dataset.setFiles(filesToDelete); |
642 | | - for (DatasetFile file : filesToDelete) { |
643 | | - dataset.removeFile(file); |
| 709 | + // 更新数据集(避免 ConcurrentModificationException,先获取文件列表再删除) |
| 710 | + List<DatasetFile> datasetFiles = dataset.getFiles(); |
| 711 | + if (datasetFiles != null) { |
| 712 | + // 创建一个新的列表来存储要保留的文件 |
| 713 | + List<DatasetFile> remainingFiles = new ArrayList<>(datasetFiles); |
| 714 | + // 移除要删除的文件 |
| 715 | + remainingFiles.removeAll(filesToDelete); |
| 716 | + dataset.setFiles(remainingFiles); |
644 | 717 | } |
645 | 718 | datasetRepository.updateById(dataset); |
646 | 719 | } |
@@ -867,8 +940,24 @@ private void addFile(String sourPath, String targetPath, boolean softAdd) { |
867 | 940 | if (StringUtils.isBlank(sourPath) || StringUtils.isBlank(targetPath)) { |
868 | 941 | return; |
869 | 942 | } |
870 | | - Path source = Paths.get(sourPath).normalize(); |
871 | | - Path target = Paths.get(targetPath).normalize(); |
| 943 | + |
| 944 | + // 规范化并验证源文件路径 |
| 945 | + Path source; |
| 946 | + try { |
| 947 | + source = Paths.get(sourPath).normalize(); |
| 948 | + } catch (Exception e) { |
| 949 | + log.warn("Invalid source file path: {}", sourPath); |
| 950 | + throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR); |
| 951 | + } |
| 952 | + |
| 953 | + // 规范化并验证目标文件路径 |
| 954 | + Path target; |
| 955 | + try { |
| 956 | + target = Paths.get(targetPath).normalize(); |
| 957 | + } catch (Exception e) { |
| 958 | + log.warn("Invalid target file path: {}", targetPath); |
| 959 | + throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR); |
| 960 | + } |
872 | 961 |
|
873 | 962 | // 检查源文件是否存在且为普通文件 |
874 | 963 | if (!Files.exists(source) || !Files.isRegularFile(source)) { |
@@ -926,4 +1015,30 @@ private static DatasetFile getDatasetFileForAdd(AddFilesRequest req, AddFilesReq |
926 | 1015 | .metadata(objectMapper.writeValueAsString(file.getMetadata())) |
927 | 1016 | .build(); |
928 | 1017 | } |
| 1018 | + |
| 1019 | + /** |
| 1020 | + * 安全地验证并获取文件路径,防止路径遍历攻击 |
| 1021 | + * |
| 1022 | + * @param filePath 用户提供的文件路径 |
| 1023 | + * @param basePath 允许的基础路径(数据集路径) |
| 1024 | + * @return 规范化后的绝对路径 |
| 1025 | + * @throws IllegalArgumentException 如果路径不在基础路径内 |
| 1026 | + */ |
| 1027 | + private Path validateAndResolvePath(String filePath, String basePath) { |
| 1028 | + if (StringUtils.isEmpty(filePath)) { |
| 1029 | + throw new IllegalArgumentException("File path cannot be empty"); |
| 1030 | + } |
| 1031 | + |
| 1032 | + Path normalizedPath = Paths.get(filePath).normalize(); |
| 1033 | + Path normalizedBasePath = Paths.get(basePath).normalize(); |
| 1034 | + |
| 1035 | + // 验证规范化后的路径是否在基础路径内 |
| 1036 | + if (!normalizedPath.startsWith(normalizedBasePath)) { |
| 1037 | + throw new IllegalArgumentException( |
| 1038 | + "File path is outside the allowed directory: " + filePath |
| 1039 | + ); |
| 1040 | + } |
| 1041 | + |
| 1042 | + return normalizedPath; |
| 1043 | + } |
929 | 1044 | } |
0 commit comments