3636import lombok .extern .slf4j .Slf4j ;
3737import org .apache .commons .compress .archivers .zip .ZipArchiveEntry ;
3838import org .apache .commons .compress .archivers .zip .ZipArchiveOutputStream ;
39+ import org .apache .commons .io .FileUtils ;
3940import org .apache .commons .io .IOUtils ;
4041import org .apache .commons .lang3 .StringUtils ;
4142import org .springframework .beans .factory .annotation .Autowired ;
5354import java .nio .file .Files ;
5455import java .nio .file .Path ;
5556import java .nio .file .Paths ;
57+ import java .nio .file .StandardCopyOption ;
5658import java .nio .file .attribute .BasicFileAttributes ;
5759import java .time .LocalDateTime ;
5860import java .time .ZoneId ;
@@ -364,18 +366,18 @@ public void downloadDatasetFileAsZip(String datasetId, HttpServletResponse respo
364366 }
365367 String datasetPath = dataset .getPath ();
366368 Path downloadPath = Paths .get (datasetPath ).normalize ();
367-
369+
368370 // 检查路径是否存在
369371 if (!Files .exists (downloadPath ) || !Files .isDirectory (downloadPath )) {
370372 throw BusinessException .of (DataManagementErrorCode .DATASET_NOT_FOUND );
371373 }
372-
374+
373375 response .setContentType ("application/zip" );
374376 String zipName = String .format ("dataset_%s_%s.zip" ,
375377 dataset .getName () != null ? dataset .getName ().replaceAll ("[^a-zA-Z0-9_-]" , "_" ) : "dataset" ,
376378 LocalDateTime .now ().format (DateTimeFormatter .ofPattern ("yyyyMMddHHmmss" )));
377379 response .setHeader (HttpHeaders .CONTENT_DISPOSITION , "attachment; filename=\" " + zipName + "\" " );
378-
380+
379381 try (ZipArchiveOutputStream zos = new ZipArchiveOutputStream (response .getOutputStream ())) {
380382 try (Stream <Path > pathStream = Files .walk (downloadPath )) {
381383 pathStream
@@ -442,19 +444,25 @@ public String preUpload(UploadFilesPreRequest chunkUploadRequest, String dataset
442444 if (Objects .isNull (datasetRepository .getById (datasetId ))) {
443445 throw BusinessException .of (DataManagementErrorCode .DATASET_NOT_FOUND );
444446 }
445-
447+
446448 // 构建上传路径,如果有 prefix 则追加到路径中
447449 String prefix = Optional .ofNullable (chunkUploadRequest .getPrefix ()).orElse ("" ).trim ();
448450 prefix = prefix .replace ("\\ " , "/" );
449451 while (prefix .startsWith ("/" )) {
450452 prefix = prefix .substring (1 );
451453 }
452-
453- String uploadPath = datasetBasePath + File .separator + datasetId ;
454- if (!prefix .isEmpty ()) {
455- uploadPath = uploadPath + File .separator + prefix .replace ("/" , File .separator );
454+
455+ String uploadPath ;
456+ // 如果需要解压,上传到全局临时目录以避免覆盖数据集中的同名文件
457+ if (chunkUploadRequest .isHasArchive ()) {
458+ uploadPath = datasetBasePath + File .separator + ".temp_upload_" + System .currentTimeMillis ();
459+ } else {
460+ uploadPath = datasetBasePath + File .separator + datasetId ;
461+ if (!prefix .isEmpty ()) {
462+ uploadPath = uploadPath + File .separator + prefix .replace ("/" , File .separator );
463+ }
456464 }
457-
465+
458466 ChunkUploadPreRequest request = ChunkUploadPreRequest .builder ().build ();
459467 request .setUploadPath (uploadPath );
460468 request .setTotalFileNum (chunkUploadRequest .getTotalFileNum ());
@@ -512,8 +520,81 @@ private void saveFileInfoToDb(FileUploadResult fileUploadResult, String datasetI
512520 private void addFileToDataset (String datasetId , List <FileUploadResult > unpacked ) {
513521 Dataset dataset = datasetRepository .getById (datasetId );
514522 dataset .setFiles (datasetFileRepository .findAllByDatasetId (datasetId ));
523+
524+ // 收集所有临时目录,在文件处理完后统一删除
525+ Set <String > tempDirsToDelete = new HashSet <>();
526+
515527 for (FileUploadResult file : unpacked ) {
516528 File savedFile = file .getSavedFile ();
529+ String filePath = savedFile .getPath ();
530+
531+ // 如果文件在临时目录(从解压的上传来),移动到数据集目录
532+ if (filePath .contains (".temp_upload_" )) {
533+ try {
534+ // 提取临时目录之后的部分(保持相对路径结构)
535+ int tempIndex = filePath .indexOf (".temp_upload_" );
536+
537+ // 找到 .temp_upload_{timestamp}/ 之后的内容
538+ // 临时目录格式: /dataset/.temp_upload_{timestamp}/
539+ int afterTimestamp = filePath .indexOf (File .separator , tempIndex + ".temp_upload_" .length ());
540+ if (afterTimestamp == -1 ) {
541+ // 没有分隔符,说明文件就在临时目录根下
542+ afterTimestamp = filePath .length ();
543+ }
544+
545+ String tempDir = filePath .substring (0 , afterTimestamp );
546+ String relativePath = filePath .substring (afterTimestamp );
547+
548+ // 去掉相对路径开头的分隔符
549+ if (relativePath .startsWith (File .separator )) {
550+ relativePath = relativePath .substring (1 );
551+ }
552+
553+ // 构建安全的目标路径:防止目录遍历攻击
554+ // 校验 datasetId,防止目录遍历或非法路径片段
555+ if (datasetId .contains (".." ) || datasetId .contains ("/" ) || datasetId .contains ("\\ " )) {
556+ throw BusinessException .of (CommonErrorCode .PARAM_ERROR , "Invalid datasetId: " + datasetId );
557+ }
558+
559+ // 校验相对路径,防止目录遍历
560+ if (relativePath .contains (".." )) {
561+ throw BusinessException .of (CommonErrorCode .PARAM_ERROR , "Invalid relative path: " + relativePath );
562+ }
563+
564+ // 使用 Path API 安全地构建路径
565+ Path datasetBaseDirPath = Paths .get (datasetBasePath ).resolve (datasetId ).normalize ();
566+ Path targetPath ;
567+ if (!relativePath .isEmpty ()) {
568+ targetPath = datasetBaseDirPath .resolve (relativePath ).normalize ();
569+ } else {
570+ targetPath = datasetBaseDirPath ;
571+ }
572+
573+ // 确保目标路径仍然位于数据集根目录之下
574+ if (!targetPath .startsWith (datasetBaseDirPath )) {
575+ throw BusinessException .of (CommonErrorCode .PARAM_ERROR ,
576+ "Path traversal detected: " + relativePath );
577+ }
578+
579+ File targetFile = targetPath .toFile ();
580+ // 创建父目录
581+ FileUtils .createParentDirectories (targetFile );
582+
583+ // 移动文件(覆盖已存在的文件)
584+ Files .move (savedFile .toPath (), targetFile .toPath (), StandardCopyOption .REPLACE_EXISTING );
585+ log .info ("Moved file from temp dir: {} -> {}" , savedFile .getPath (), targetPath );
586+
587+ // 收集临时目录,稍后删除
588+ tempDirsToDelete .add (tempDir );
589+
590+ // 更新文件引用
591+ savedFile = targetFile ;
592+ } catch (IOException e ) {
593+ log .error ("Failed to move file from temp directory: {}" , filePath , e );
594+ continue ; // 跳过此文件
595+ }
596+ }
597+
517598 LocalDateTime currentTime = LocalDateTime .now ();
518599 // 统一 fileName:无论是否通过文件夹/压缩包上传,都只保留纯文件名
519600 String originalFileName = file .getFileName ();
@@ -539,6 +620,20 @@ private void addFileToDataset(String datasetId, List<FileUploadResult> unpacked)
539620 datasetFileRepository .saveOrUpdate (datasetFile );
540621 dataset .addFile (datasetFile );
541622 }
623+
624+ // 递归删除所有临时目录
625+ for (String tempDir : tempDirsToDelete ) {
626+ try {
627+ File tempDirFile = new File (tempDir );
628+ if (tempDirFile .exists () && tempDirFile .isDirectory ()) {
629+ org .apache .commons .io .FileUtils .deleteDirectory (tempDirFile );
630+ log .info ("Deleted temp directory: {}" , tempDir );
631+ }
632+ } catch (IOException e ) {
633+ log .warn ("Failed to delete temp directory: {}" , tempDir , e );
634+ }
635+ }
636+
542637 dataset .active ();
543638 datasetRepository .updateById (dataset );
544639 }
@@ -623,7 +718,7 @@ public void downloadDirectory(String datasetId, String prefix, HttpServletRespon
623718 try {
624719 response .setContentType ("application/zip" );
625720 response .setHeader (HttpHeaders .CONTENT_DISPOSITION , "attachment; filename=\" " + zipFileName + "\" " );
626-
721+
627722 try (ZipArchiveOutputStream zipOut = new ZipArchiveOutputStream (response .getOutputStream ())) {
628723 zipDirectory (normalized , normalized , zipOut );
629724 zipOut .finish ();
0 commit comments