Skip to content

Commit 78ac871

Browse files
authored
fix(auto-annotation): resolve task bugs and add dataset and folder renaming support (#245)
Fix multiple issues in auto-annotation tasks Add support for renaming datasets and folders
1 parent 6470004 commit 78ac871

File tree

8 files changed

+356
-5
lines changed

8 files changed

+356
-5
lines changed

backend/services/data-management-service/src/main/java/com/datamate/datamanagement/application/DatasetFileApplicationService.java

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import com.datamate.datamanagement.interfaces.dto.CreateDirectoryRequest;
2828
import com.datamate.datamanagement.interfaces.dto.UploadFileRequest;
2929
import com.datamate.datamanagement.interfaces.dto.UploadFilesPreRequest;
30+
import com.datamate.datamanagement.interfaces.dto.RenameFileRequest;
31+
import com.datamate.datamanagement.interfaces.dto.RenameDirectoryRequest;
3032
import com.fasterxml.jackson.core.JsonProcessingException;
3133
import com.fasterxml.jackson.databind.ObjectMapper;
3234
import jakarta.servlet.http.HttpServletResponse;
@@ -607,6 +609,148 @@ public void deleteDirectory(String datasetId, String prefix) {
607609
datasetRepository.updateById(dataset);
608610
}
609611

612+
/**
613+
* 重命名数据集文件(仅允许修改主名称,文件后缀保持不变)
614+
*/
615+
@Transactional
616+
public void renameFile(String datasetId, String fileId, RenameFileRequest request) {
617+
DatasetFile file = getDatasetFile(datasetId, fileId);
618+
Dataset dataset = datasetRepository.getById(datasetId);
619+
if (dataset == null) {
620+
throw BusinessException.of(DataManagementErrorCode.DATASET_NOT_FOUND);
621+
}
622+
623+
String newName = Optional.ofNullable(request.getNewName()).orElse("").trim();
624+
if (newName.isEmpty()) {
625+
throw BusinessException.of(CommonErrorCode.PARAM_ERROR);
626+
}
627+
628+
String originalFileName = file.getFileName();
629+
String baseName = originalFileName;
630+
String extension = "";
631+
int dotIndex = originalFileName.lastIndexOf('.');
632+
if (dotIndex > 0 && dotIndex < originalFileName.length() - 1) {
633+
baseName = originalFileName.substring(0, dotIndex);
634+
extension = originalFileName.substring(dotIndex); // 包含点号,如 .jpg
635+
}
636+
637+
// 只接收主名称,后缀始终使用原始后缀
638+
String finalFileName = newName + extension;
639+
640+
Path oldPath = Paths.get(file.getFilePath()).normalize();
641+
Path basePath = Paths.get(dataset.getPath()).normalize();
642+
643+
// 仅允许重命名数据集自身目录下的文件
644+
if (!oldPath.startsWith(basePath)) {
645+
throw BusinessException.of(CommonErrorCode.PARAM_ERROR);
646+
}
647+
648+
Path parentDir = oldPath.getParent();
649+
if (parentDir == null) {
650+
throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR);
651+
}
652+
Path newPath = parentDir.resolve(finalFileName).normalize();
653+
654+
if (!newPath.startsWith(basePath)) {
655+
throw BusinessException.of(CommonErrorCode.PARAM_ERROR);
656+
}
657+
658+
if (Files.exists(newPath)) {
659+
throw BusinessException.of(DataManagementErrorCode.DATASET_FILE_ALREADY_EXISTS);
660+
}
661+
662+
try {
663+
Files.move(oldPath, newPath);
664+
} catch (IOException e) {
665+
log.error("Failed to rename file from {} to {}", oldPath, newPath, e);
666+
throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR);
667+
}
668+
669+
file.setFileName(finalFileName);
670+
file.setFilePath(newPath.toString());
671+
file.setFileType(AnalyzerUtils.getExtension(finalFileName));
672+
file.setLastAccessTime(LocalDateTime.now());
673+
datasetFileRepository.updateById(file);
674+
}
675+
676+
/**
677+
* 重命名目录
678+
*/
679+
@Transactional
680+
public void renameDirectory(String datasetId, RenameDirectoryRequest request) {
681+
Dataset dataset = datasetRepository.getById(datasetId);
682+
if (dataset == null) {
683+
throw BusinessException.of(DataManagementErrorCode.DATASET_NOT_FOUND);
684+
}
685+
686+
String prefix = Optional.ofNullable(request.getPrefix()).orElse("").trim();
687+
prefix = prefix.replace("\\", "/");
688+
while (prefix.startsWith("/")) {
689+
prefix = prefix.substring(1);
690+
}
691+
while (prefix.endsWith("/")) {
692+
prefix = prefix.substring(0, prefix.length() - 1);
693+
}
694+
695+
if (prefix.isEmpty()) {
696+
throw BusinessException.of(CommonErrorCode.PARAM_ERROR);
697+
}
698+
699+
String newName = Optional.ofNullable(request.getNewName()).orElse("").trim();
700+
if (newName.isEmpty() || newName.contains("..") || newName.contains("/") || newName.contains("\\")) {
701+
throw BusinessException.of(CommonErrorCode.PARAM_ERROR);
702+
}
703+
704+
String datasetPath = dataset.getPath();
705+
Path basePath = Paths.get(datasetPath).normalize();
706+
Path oldDir = basePath.resolve(prefix).normalize();
707+
708+
if (!oldDir.startsWith(basePath)) {
709+
throw BusinessException.of(CommonErrorCode.PARAM_ERROR);
710+
}
711+
712+
if (!Files.exists(oldDir) || !Files.isDirectory(oldDir)) {
713+
throw BusinessException.of(DataManagementErrorCode.DIRECTORY_NOT_FOUND);
714+
}
715+
716+
Path parentDir = oldDir.getParent();
717+
if (parentDir == null) {
718+
throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR);
719+
}
720+
Path newDir = parentDir.resolve(newName).normalize();
721+
722+
if (!newDir.startsWith(basePath)) {
723+
throw BusinessException.of(CommonErrorCode.PARAM_ERROR);
724+
}
725+
726+
if (Files.exists(newDir)) {
727+
throw BusinessException.of(DataManagementErrorCode.DIRECTORY_NOT_FOUND);
728+
}
729+
730+
try {
731+
Files.move(oldDir, newDir);
732+
} catch (IOException e) {
733+
log.error("Failed to rename directory from {} to {}", oldDir, newDir, e);
734+
throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR);
735+
}
736+
737+
// 同步更新数据库中该目录下所有文件的 filePath
738+
String oldDirPath = oldDir.toString().replace("\\", "/");
739+
String newDirPath = newDir.toString().replace("\\", "/");
740+
741+
List<DatasetFile> allFiles = datasetFileRepository.findAllByDatasetId(datasetId);
742+
for (DatasetFile file : allFiles) {
743+
String filePath = Optional.ofNullable(file.getFilePath()).orElse("").replace("\\", "/");
744+
if (filePath.startsWith(oldDirPath + "/")) {
745+
String relative = filePath.substring(oldDirPath.length() + 1);
746+
Path updatedPath = Paths.get(newDirPath).resolve(relative);
747+
file.setFilePath(updatedPath.toString());
748+
file.setLastAccessTime(LocalDateTime.now());
749+
datasetFileRepository.updateById(file);
750+
}
751+
}
752+
}
753+
610754
/**
611755
* 递归删除目录
612756
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.datamate.datamanagement.interfaces.dto;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import lombok.Getter;
5+
import lombok.Setter;
6+
7+
/**
8+
* 重命名数据集目录请求
9+
*/
10+
@Getter
11+
@Setter
12+
public class RenameDirectoryRequest {
13+
14+
/** 目录前缀,例如 "images/",与列表/删除目录接口保持一致 */
15+
@NotBlank
16+
private String prefix;
17+
18+
/** 新的目录名称 */
19+
@NotBlank
20+
private String newName;
21+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.datamate.datamanagement.interfaces.dto;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import lombok.Getter;
5+
import lombok.Setter;
6+
7+
/**
8+
* 重命名数据集文件请求
9+
*/
10+
@Getter
11+
@Setter
12+
public class RenameFileRequest {
13+
14+
/** 新的文件名称(不包含后缀) */
15+
@NotBlank
16+
private String newName;
17+
}

backend/services/data-management-service/src/main/java/com/datamate/datamanagement/interfaces/rest/DatasetFileController.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import com.datamate.datamanagement.interfaces.dto.DatasetFileResponse;
1515
import com.datamate.datamanagement.interfaces.dto.UploadFileRequest;
1616
import com.datamate.datamanagement.interfaces.dto.UploadFilesPreRequest;
17+
import com.datamate.datamanagement.interfaces.dto.RenameFileRequest;
18+
import com.datamate.datamanagement.interfaces.dto.RenameDirectoryRequest;
1719
import jakarta.servlet.http.HttpServletResponse;
1820
import jakarta.validation.Valid;
1921
import lombok.extern.slf4j.Slf4j;
@@ -194,4 +196,25 @@ public ResponseEntity<Void> deleteDirectory(@PathVariable("datasetId") String da
194196
datasetFileApplicationService.deleteDirectory(datasetId, prefix);
195197
return ResponseEntity.ok().build();
196198
}
199+
200+
/**
201+
* 重命名文件
202+
*/
203+
@PutMapping("/{fileId}/rename")
204+
public ResponseEntity<Void> renameFile(@PathVariable("datasetId") String datasetId,
205+
@PathVariable("fileId") String fileId,
206+
@RequestBody @Valid RenameFileRequest request) {
207+
datasetFileApplicationService.renameFile(datasetId, fileId, request);
208+
return ResponseEntity.ok().build();
209+
}
210+
211+
/**
212+
* 重命名目录
213+
*/
214+
@PutMapping("/directories/rename")
215+
public ResponseEntity<Void> renameDirectory(@PathVariable("datasetId") String datasetId,
216+
@RequestBody @Valid RenameDirectoryRequest request) {
217+
datasetFileApplicationService.renameDirectory(datasetId, request);
218+
return ResponseEntity.ok().build();
219+
}
197220
}

frontend/src/pages/DataManagement/Detail/components/Overview.tsx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export default function Overview({ dataset, filesOperation, fetchDataset }) {
2121
handleCreateDirectory,
2222
handleDownloadDirectory,
2323
handleDeleteDirectory,
24+
handleRenameFile,
25+
handleRenameDirectory,
2426
} = filesOperation;
2527

2628
// 文件列表多选配置
@@ -201,6 +203,37 @@ export default function Overview({ dataset, filesOperation, fetchDataset }) {
201203
>
202204
下载
203205
</Button>
206+
<Button
207+
size="small"
208+
type="link"
209+
onClick={() => {
210+
let newDirName = record.fileName;
211+
modal.confirm({
212+
title: '重命名文件夹',
213+
content: (
214+
<Input
215+
autoFocus
216+
defaultValue={record.fileName}
217+
onChange={(e) => {
218+
newDirName = e.target.value?.trim();
219+
}}
220+
/>
221+
),
222+
okText: '确定',
223+
cancelText: '取消',
224+
onOk: async () => {
225+
if (!newDirName) {
226+
message.warning('请输入文件夹名称');
227+
return Promise.reject();
228+
}
229+
await handleRenameDirectory(fullPath, record.fileName, newDirName);
230+
fetchDataset();
231+
},
232+
});
233+
}}
234+
>
235+
重命名
236+
</Button>
204237
<Button
205238
size="small"
206239
type="link"
@@ -233,6 +266,45 @@ export default function Overview({ dataset, filesOperation, fetchDataset }) {
233266
>
234267
下载
235268
</Button>
269+
<Button
270+
size="small"
271+
type="link"
272+
onClick={() => {
273+
const originalName = record.fileName || '';
274+
const dotIndex = originalName.lastIndexOf('.');
275+
const baseName = dotIndex > 0 ? originalName.slice(0, dotIndex) : originalName;
276+
const ext = dotIndex > 0 ? originalName.slice(dotIndex) : '';
277+
let newBaseName = baseName;
278+
279+
modal.confirm({
280+
title: '重命名文件',
281+
content: (
282+
<div className="space-y-2">
283+
<Input
284+
autoFocus
285+
defaultValue={baseName}
286+
addonAfter={ext}
287+
onChange={(e) => {
288+
newBaseName = e.target.value?.trim();
289+
}}
290+
/>
291+
</div>
292+
),
293+
okText: '确定',
294+
cancelText: '取消',
295+
onOk: async () => {
296+
if (!newBaseName) {
297+
message.warning('请输入文件名称');
298+
return Promise.reject();
299+
}
300+
await handleRenameFile(record, newBaseName);
301+
fetchDataset();
302+
},
303+
});
304+
}}
305+
>
306+
重命名
307+
</Button>
236308
<Button
237309
size="small"
238310
type="link"

frontend/src/pages/DataManagement/Detail/useFilesOperation.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
createDatasetDirectoryUsingPost,
1313
downloadDirectoryUsingGet,
1414
deleteDirectoryUsingDelete,
15+
renameDatasetFileUsingPut,
16+
renameDirectoryUsingPut,
1517
} from "../dataset.api";
1618
import { useParams } from "react-router";
1719

@@ -182,5 +184,37 @@ export function useFilesOperation(dataset: Dataset) {
182184
message.error({ content: `文件夹 ${directoryName} 删除失败` });
183185
}
184186
},
187+
handleRenameFile: async (file, newBaseName: string) => {
188+
try {
189+
const trimmed = (newBaseName || "").trim();
190+
if (!trimmed) {
191+
message.warning({ content: "请输入文件名称" });
192+
return;
193+
}
194+
await renameDatasetFileUsingPut(dataset.id, file.id, { newName: trimmed });
195+
const currentPrefix = pagination.prefix || "";
196+
await fetchFiles(currentPrefix, 1, pagination.pageSize);
197+
message.success({ content: `文件 ${file.fileName} 重命名成功` });
198+
} catch (error) {
199+
message.error({ content: `文件 ${file.fileName} 重命名失败` });
200+
throw error;
201+
}
202+
},
203+
handleRenameDirectory: async (directoryPath: string, oldName: string, newName: string) => {
204+
try {
205+
const trimmed = (newName || "").trim();
206+
if (!trimmed) {
207+
message.warning({ content: "请输入文件夹名称" });
208+
return;
209+
}
210+
await renameDirectoryUsingPut(dataset.id, { prefix: directoryPath, newName: trimmed });
211+
const currentPrefix = pagination.prefix || "";
212+
await fetchFiles(currentPrefix, 1, pagination.pageSize);
213+
message.success({ content: `文件夹 ${oldName} 重命名为 ${trimmed} 成功` });
214+
} catch (error) {
215+
message.error({ content: `文件夹 ${oldName} 重命名失败` });
216+
throw error;
217+
}
218+
},
185219
};
186220
}

0 commit comments

Comments
 (0)