From 40f9bdddbdf32def5880d904c65c899cb50ea3c2 Mon Sep 17 00:00:00 2001 From: zhangzheng <826193720@qq.com> Date: Wed, 5 Aug 2026 11:05:19 +0800 Subject: [PATCH] =?UTF-8?q?fix=20bugs=201=E6=96=87=E4=BB=B6=E6=A0=91?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E8=8A=82=E7=82=B9=E5=90=8E=EF=BC=8C=E7=82=B9?= =?UTF-8?q?=E5=87=BBtoolbar=E4=B8=8A=E7=9A=84=E5=88=B7=E6=96=B0=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E5=90=8E=EF=BC=8C=E5=9C=A8=E4=BD=BF=E7=94=A8=E6=96=B0?= =?UTF-8?q?=E5=BB=BA=E6=96=87=E4=BB=B6/=E6=96=87=E4=BB=B6=E5=A4=B9?= =?UTF-8?q?=EF=BC=8C=E5=88=B7=E6=96=B0=E7=AD=89=E5=BF=AB=E6=8D=B7=E9=94=AE?= =?UTF-8?q?=E6=97=A0=E6=95=88=EF=BC=9B2=E6=96=87=E4=BB=B6=E6=A0=91?= =?UTF-8?q?=E6=96=B0=E5=BB=BA=E9=87=8D=E5=90=8D=E6=96=87=E4=BB=B6=E6=97=B6?= =?UTF-8?q?=E6=B2=A1=E6=9C=89=E6=8A=A5=E9=94=99=EF=BC=8C=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E7=94=9F=E6=88=90backup=E6=96=87=E4=BB=B6=EF=BC=8C=E6=96=B0?= =?UTF-8?q?=E5=BB=BA=E9=87=8D=E5=90=8D=E6=96=87=E4=BB=B6=E5=A4=B9=E6=97=B6?= =?UTF-8?q?=E6=B2=A1=E6=9C=89=E6=8A=A5=E9=94=99=E3=80=82=E4=B8=AD=E6=96=87?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E4=B8=8B=EF=BC=8C=E5=88=9B=E5=BB=BA=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=A4=B9=E6=97=B6=EF=BC=8C=E5=AD=98=E5=9C=A8=E5=90=8C?= =?UTF-8?q?=E5=90=8D=E7=9A=84=E6=96=87=E4=BB=B6=EF=BC=8C=E6=8A=A5=E9=94=99?= =?UTF-8?q?=E5=BC=B9=E7=AA=97=E6=8F=90=E7=A4=BA=E9=83=A8=E5=88=86=E4=B8=AD?= =?UTF-8?q?=E6=96=87=EF=BC=8C=E9=83=A8=E5=88=86=E8=8B=B1=E6=96=87=EF=BC=88?= =?UTF-8?q?=E5=88=9B=E5=BB=BA=E6=96=87=E4=BB=B6=E5=A4=B9=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=EF=BC=9AFailed=20to=20create=20directory:=20Service=20error:?= =?UTF-8?q?=20Failed=20to=20create=20directory:=20File=20exists(os=20error?= =?UTF-8?q?=2017)=EF=BC=89=EF=BC=8C=E5=B8=AE=E6=88=91=E9=80=82=E9=85=8D?= =?UTF-8?q?=E4=B8=8B=E6=96=B0=E5=BB=BA=E6=96=87=E4=BB=B6=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=A4=B9=E6=97=B6=E7=9A=84=E5=9B=BD=E9=99=85=E5=8C=96=EF=BC=8C?= =?UTF-8?q?=E6=80=BB=E5=85=B1=E4=B8=89=E7=A7=8D=E8=AF=AD=E8=A8=80=EF=BC=8C?= =?UTF-8?q?=E4=B8=AD=E6=96=87=E8=8B=B1=E6=96=87=E7=B9=81=E4=BD=93=EF=BC=9B?= =?UTF-8?q?3=E6=8A=98=E5=8F=A0=E6=96=87=E4=BB=B6=E6=95=B0=E6=89=80?= =?UTF-8?q?=E6=9C=89=E7=9B=AE=E5=BD=95=E7=9A=84=E5=BF=AB=E6=8D=B7=E9=94=AE?= =?UTF-8?q?ctrl+shift+[=E5=A4=B1=E6=95=88=EF=BC=9B4=E6=96=B0=E5=BB=BA?= =?UTF-8?q?=E6=96=87=E4=BB=B6=EF=BC=8C=E5=BC=B9=E5=87=BA=E5=BC=B9=E7=AA=97?= =?UTF-8?q?=E5=85=B3=E9=97=AD=E5=90=8E=EF=BC=8C=E5=BC=B9=E7=AA=97=E5=85=B3?= =?UTF-8?q?=E9=97=AD=E4=B9=8B=E5=89=8D=E6=A0=87=E9=A2=98=E6=96=B0=E5=BB=BA?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=BC=9A=E5=8F=98=E6=88=90=E6=96=B0=E5=BB=BA?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=A4=B9=EF=BC=9B5=E9=87=8D=E5=91=BD?= =?UTF-8?q?=E5=90=8D=E8=BE=93=E5=85=A5=E6=A1=86=E6=8B=96=E6=8B=BD=E9=80=89?= =?UTF-8?q?=E4=B8=AD=E5=AD=97=E7=AC=A6=E6=97=B6=EF=BC=8C=E5=8F=98=E6=88=90?= =?UTF-8?q?=E4=BA=86=E6=8B=96=E6=8B=BD=E6=96=87=E4=BB=B6=E6=A0=91=E7=9A=84?= =?UTF-8?q?=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apps/desktop/src/api/path_target.rs | 22 ++++- .../src/filesystem/operations.rs | 2 +- .../src/app/components/panels/FilesPanel.tsx | 91 ++++++++++++++----- .../app/scenes/file-viewer/FileViewerNav.tsx | 2 +- .../components/InputDialog/InputDialog.tsx | 12 ++- .../services/ShortcutManager.test.ts | 41 +++++++++ .../services/ShortcutManager.ts | 13 ++- .../src/locales/en-US/panels/files.json | 6 +- .../src/locales/zh-CN/panels/files.json | 6 +- .../src/locales/zh-TW/panels/files.json | 12 ++- .../controller/ExplorerController.ts | 6 ++ .../file-explorer/model/ExplorerModel.test.ts | 10 ++ .../file-explorer/model/ExplorerModel.ts | 7 ++ .../file-system/components/FileTreeItem.tsx | 2 +- .../tools/file-system/hooks/useFileSystem.ts | 6 ++ 15 files changed, 201 insertions(+), 37 deletions(-) diff --git a/src/apps/desktop/src/api/path_target.rs b/src/apps/desktop/src/api/path_target.rs index 87298e3689..f71306df76 100644 --- a/src/apps/desktop/src/api/path_target.rs +++ b/src/apps/desktop/src/api/path_target.rs @@ -590,7 +590,13 @@ pub async fn create_empty_file( ) -> Result<(), String> { match resolve_desktop_path_target(app_state, raw_path, preferred_remote_connection_id).await? { DesktopPathTarget::Local { resolved_path, .. } => { - let options = FileOperationOptions::default(); + if resolved_path.exists() { + return Err("Path already exists".to_string()); + } + let options = FileOperationOptions { + backup_on_overwrite: false, + ..FileOperationOptions::default() + }; app_state .filesystem_service .write_file_with_options(&resolved_path.to_string_lossy(), "", options) @@ -606,6 +612,13 @@ pub async fn create_empty_file( .get_remote_file_service_async() .await .map_err(|e| format!("Remote file service not available: {}", e))?; + if remote_fs + .exists(&entry.connection_id, &requested_path) + .await + .map_err(|e| format!("Failed to check remote path: {}", e))? + { + return Err("Path already exists".to_string()); + } remote_fs .write_file(&entry.connection_id, &requested_path, b"") .await @@ -633,6 +646,13 @@ pub async fn create_directory( .get_remote_file_service_async() .await .map_err(|e| format!("Remote file service not available: {}", e))?; + if remote_fs + .exists(&entry.connection_id, &requested_path) + .await + .map_err(|e| format!("Failed to check remote path: {}", e))? + { + return Err("Path already exists".to_string()); + } remote_fs .create_dir_all(&entry.connection_id, &requested_path) .await diff --git a/src/crates/services/services-core/src/filesystem/operations.rs b/src/crates/services/services-core/src/filesystem/operations.rs index e019e0419e..6909641fb3 100644 --- a/src/crates/services/services-core/src/filesystem/operations.rs +++ b/src/crates/services/services-core/src/filesystem/operations.rs @@ -453,7 +453,7 @@ impl FileOperationService { self.validate_file_access(path, true).await?; - fs::create_dir_all(path) + fs::create_dir(path) .await .map_err(|e| FileSystemError::service(format!("Failed to create directory: {}", e)))?; diff --git a/src/web-ui/src/app/components/panels/FilesPanel.tsx b/src/web-ui/src/app/components/panels/FilesPanel.tsx index 5c61d97114..bd28fbebb7 100644 --- a/src/web-ui/src/app/components/panels/FilesPanel.tsx +++ b/src/web-ui/src/app/components/panels/FilesPanel.tsx @@ -59,6 +59,26 @@ const FOCUS_REFRESH_THROTTLE_MS = 1000; const REMOTE_REFRESH_POLL_MS = 15000; const LARGE_FILE_THRESHOLD_BYTES = 2 * 1024 * 1024; +function getChildNames(nodes: FileSystemNode[], parentPath: string): string[] { + for (const node of nodes) { + if (pathsEquivalentFs(node.path, parentPath)) { + return (node.children ?? []).map((child) => child.name); + } + if (node.children) { + const childNames = getChildNames(node.children, parentPath); + if (childNames.length > 0) { + return childNames; + } + } + } + return []; +} + +function isAlreadyExistsError(error: unknown): boolean { + const message = error instanceof Error ? error.message : String(error); + return /already exists|file exists|os error 17|os error 183|EEXIST/i.test(message); +} + /** Format a byte-per-second speed value for display, e.g. "1.4 MB/s". */ function formatSpeed(bytesPerSec: number): string { return `${formatBytes(bytesPerSec)}/s`; @@ -262,6 +282,7 @@ const FilesPanel: React.FC = ({ expandFolder, expandFolderLazy, expandFolderEnsure, + collapseAll, removePath, } = useFileSystem({ rootPath: workspacePath, @@ -358,15 +379,23 @@ const FilesPanel: React.FC = ({ }); }, []); - const handleInputDialogClose = useCallback(() => { - setInputDialog({ - isOpen: false, - type: null, - parentPath: '', + const focusFileTree = useCallback(() => { + window.requestAnimationFrame(() => { + panelRef.current + ?.querySelector('[data-shortcut-scope="filetree"]') + ?.focus(); }); }, []); - const handleConfirmNewFile = useCallback(async (fileName: string) => { + const handleInputDialogClose = useCallback(() => { + setInputDialog((current) => ({ + ...current, + isOpen: false, + })); + focusFileTree(); + }, [focusFileTree]); + + const handleConfirmNewFile = useCallback(async (fileName: string): Promise => { const filePath = joinWorkspaceTargetPath( inputDialog.parentPath, fileName, @@ -376,13 +405,17 @@ const FilesPanel: React.FC = ({ try { await workspaceAPI.createFile(filePath, currentWorkspace?.connectionId); log.info('File created', { path: filePath }); - handleInputDialogClose(); - loadFileTree(workspacePath || '', true); + void loadFileTree(workspacePath || '', true); + return true; } catch (error) { log.error('Failed to create file', error); - notification.error(t('notifications.createFileFailed', { error: String(error) })); + const messageKey = isAlreadyExistsError(error) + ? 'notifications.createFileAlreadyExists' + : 'notifications.createFileFailed'; + notification.error(t(messageKey)); + return false; } - }, [inputDialog.parentPath, workspacePath, loadFileTree, notification, t, handleInputDialogClose, currentWorkspace]); + }, [inputDialog.parentPath, workspacePath, loadFileTree, notification, t, currentWorkspace]); const handleNewFolder = useCallback((data: { parentPath: string }) => { setInputDialog({ @@ -392,7 +425,7 @@ const FilesPanel: React.FC = ({ }); }, []); - const handleConfirmNewFolder = useCallback(async (folderName: string) => { + const handleConfirmNewFolder = useCallback(async (folderName: string): Promise => { const folderPath = joinWorkspaceTargetPath( inputDialog.parentPath, folderName, @@ -402,20 +435,26 @@ const FilesPanel: React.FC = ({ try { await workspaceAPI.createDirectory(folderPath, currentWorkspace?.connectionId); log.info('Directory created', { path: folderPath }); - handleInputDialogClose(); - loadFileTree(workspacePath || '', true); + void loadFileTree(workspacePath || '', true); + return true; } catch (error) { log.error('Failed to create directory', error); - notification.error(t('notifications.createFolderFailed', { error: String(error) })); + const messageKey = isAlreadyExistsError(error) + ? 'notifications.createFolderAlreadyExists' + : 'notifications.createFolderFailed'; + notification.error(t(messageKey)); + return false; } - }, [inputDialog.parentPath, workspacePath, loadFileTree, notification, t, handleInputDialogClose, currentWorkspace]); + }, [inputDialog.parentPath, workspacePath, loadFileTree, notification, t, currentWorkspace]); - const handleInputDialogConfirm = useCallback((value: string) => { + const handleInputDialogConfirm = useCallback((value: string): Promise | boolean => { if (inputDialog.type === 'newFile') { - handleConfirmNewFile(value); - } else if (inputDialog.type === 'newFolder') { - handleConfirmNewFolder(value); + return handleConfirmNewFile(value); + } + if (inputDialog.type === 'newFolder') { + return handleConfirmNewFolder(value); } + return false; }, [inputDialog.type, handleConfirmNewFile, handleConfirmNewFolder]); const handleStartRename = useCallback((data: { path: string; name: string }) => { @@ -750,6 +789,12 @@ const FilesPanel: React.FC = ({ () => handlePaste(), { enabled: Boolean(workspacePath) } ); + useShortcut( + 'filetree.collapseAll', + { key: '[', ctrl: true, shift: true, scope: 'filetree' }, + collapseAll, + { enabled: Boolean(workspacePath) && viewMode === 'tree' } + ); // macOS bridge: the native menu bar intercepts Cmd+V before the DOM sees a // keydown event, so ShortcutManager never fires. In "System" edit-menu mode @@ -1321,8 +1366,12 @@ const FilesPanel: React.FC = ({ confirmText={inputDialog.type === 'newFile' ? t('dialog.newFile.confirm') : t('dialog.newFolder.confirm')} cancelText={inputDialog.type === 'newFile' ? t('dialog.newFile.cancel') : t('dialog.newFolder.cancel')} validator={(value) => { - const errorKey = validateFileName(value, { isRemote: isRemoteCurrentWorkspace }); - return errorKey ? t(errorKey) : null; + const siblingNames = getChildNames(fileTree, inputDialog.parentPath); + const errorKey = validateFileName(value, { + isRemote: isRemoteCurrentWorkspace, + siblings: siblingNames, + }); + return errorKey ? t(errorKey, { name: value.trim() }) : null; }} /> diff --git a/src/web-ui/src/app/scenes/file-viewer/FileViewerNav.tsx b/src/web-ui/src/app/scenes/file-viewer/FileViewerNav.tsx index ea5d7500f3..f52b2657a7 100644 --- a/src/web-ui/src/app/scenes/file-viewer/FileViewerNav.tsx +++ b/src/web-ui/src/app/scenes/file-viewer/FileViewerNav.tsx @@ -40,7 +40,7 @@ const FileViewerNav: React.FC = () => { }, []); return ( -
+