diff --git a/app.go b/app.go index e519367..7f9f213 100644 --- a/app.go +++ b/app.go @@ -270,17 +270,17 @@ func (a *App) ListDir(path string) ([]map[string]interface{}, error) { } // CreateDir 创建目录 -func (a *App) CreateDir(path string) error { +func (a *App) CreateDir(path string) (*filesystem.FileOperationResult, error) { return a.filesystem.CreateDir(path) } // CreateFile 创建文件 -func (a *App) CreateFile(path string) error { +func (a *App) CreateFile(path string) (*filesystem.FileOperationResult, error) { return a.filesystem.CreateFile(path) } // DeletePath 删除文件或目录 -func (a *App) DeletePath(path string) error { +func (a *App) DeletePath(path string) (*filesystem.FileOperationResult, error) { return a.filesystem.DeletePath(path) } @@ -291,7 +291,7 @@ type RenamePathRequest struct { } // RenamePath 重命名文件或目录 -func (a *App) RenamePath(req RenamePathRequest) error { +func (a *App) RenamePath(req RenamePathRequest) (*filesystem.FileOperationResult, error) { return a.filesystem.RenamePath(req.OldPath, req.NewPath) } diff --git a/internal/filesystem/fs.go b/internal/filesystem/fs.go index ba7e99d..26ab7c0 100644 --- a/internal/filesystem/fs.go +++ b/internal/filesystem/fs.go @@ -8,90 +8,9 @@ import ( "time" ) -// ========== 向后兼容的全局函数包装器 ========== -// 这些函数提供向后兼容性,内部委托给 FileSystemService -// 新代码应该使用 FileSystemService 而不是这些全局函数 - -// ReadFile 读取文件内容(向后兼容包装器) -func ReadFile(path string) (string, error) { - service, err := GetGlobalService() - if err != nil { - return "", fmt.Errorf("服务未初始化: %v", err) - } - return service.ReadFile(path) -} - -// WriteFile 写入文件(向后兼容包装器) -func WriteFile(path, content string) error { - service, err := GetGlobalService() - if err != nil { - return fmt.Errorf("服务未初始化: %v", err) - } - return service.WriteFile(path, content) -} - -// ListDir 列出目录内容(向后兼容包装器) -func ListDir(path string) ([]map[string]interface{}, error) { - service, err := GetGlobalService() - if err != nil { - return nil, fmt.Errorf("服务未初始化: %v", err) - } - return service.ListDir(path) -} - -// CreateDir 创建目录(向后兼容包装器) -func CreateDir(path string) error { - service, err := GetGlobalService() - if err != nil { - return fmt.Errorf("服务未初始化: %v", err) - } - return service.CreateDir(path) -} - -// CreateFile 创建空文件(向后兼容包装器) -func CreateFile(path string) error { - service, err := GetGlobalService() - if err != nil { - return fmt.Errorf("服务未初始化: %v", err) - } - return service.CreateFile(path) -} - -// DeletePath 删除文件或目录(向后兼容包装器) -func DeletePath(path string) error { - service, err := GetGlobalService() - if err != nil { - return fmt.Errorf("服务未初始化: %v", err) - } - return service.DeletePath(path) -} - -// DeletePathWithConfig 使用指定配置删除文件或目录(向后兼容包装器) -func DeletePathWithConfig(path string, config *Config) error { - service, err := GetGlobalService() - if err != nil { - return fmt.Errorf("服务未初始化: %v", err) - } - - // 临时替换服务的配置 - originalConfig := service.config - service.config = config - defer func() { service.config = originalConfig }() - - return service.DeletePath(path) -} - -// GetFileInfo 获取文件信息(向后兼容包装器) -func GetFileInfo(path string) (map[string]interface{}, error) { - service, err := GetGlobalService() - if err != nil { - return nil, fmt.Errorf("服务未初始化: %v", err) - } - return service.GetFileInfo(path) -} +// ========== 辅助函数 ========== // OpenPath 打开文件或目录(使用系统默认程序) -// 这是一个核心工具函数,保留为独立函数 func OpenPath(path string) error { // 使用 path.validator 进行验证 validator := NewPathValidator(DefaultConfig()) @@ -132,16 +51,7 @@ func OpenPath(path string) error { return nil } -// RenamePath 重命名文件或目录(向后兼容包装器) -func RenamePath(oldPath, newPath string) error { - service, err := GetGlobalService() - if err != nil { - return fmt.Errorf("服务未初始化: %v", err) - } - return service.RenamePath(oldPath, newPath) -} - -// ========== 辅助函数 ========== +// ========== 工具函数 ========== // formatBytes 格式化字节大小为人类可读格式 func formatBytes(bytes int64) string { diff --git a/internal/filesystem/service.go b/internal/filesystem/service.go index 4aae42b..7b2a50f 100644 --- a/internal/filesystem/service.go +++ b/internal/filesystem/service.go @@ -13,6 +13,19 @@ import ( "u-desk/internal/common" ) +// FileOperationResult 文件操作结果 +type FileOperationResult struct { + Path string `json:"path"` + Name string `json:"name"` + Size int64 `json:"size"` + SizeStr string `json:"size_str,omitempty"` + IsDir bool `json:"is_dir"` + ModTime string `json:"mod_time,omitempty"` + Mode string `json:"mode,omitempty"` + OldPath string `json:"old_path,omitempty"` // 仅重命名操作时有值 + Deleted bool `json:"deleted,omitempty"` // 仅删除操作时有值 +} + // FileSystemService 文件系统服务 // 统一管理所有文件系统相关的功能,使用依赖注入而非全局变量 type FileSystemService struct { @@ -173,52 +186,52 @@ func (s *FileSystemService) Open(path string) error { } // Delete 删除文件或目录(实现 FileService 接口) -func (s *FileSystemService) Delete(path string) error { +func (s *FileSystemService) Delete(path string) (*FileOperationResult, error) { return s.DeletePathWithContext(context.Background(), path) } // DeletePath 删除文件或目录 -func (s *FileSystemService) DeletePath(path string) error { +func (s *FileSystemService) DeletePath(path string) (*FileOperationResult, error) { return s.DeletePathWithContext(context.Background(), path) } -// DeletePathWithContext 带上下文的删除操作 -func (s *FileSystemService) DeletePathWithContext(ctx context.Context, path string) error { +// DeletePathWithContext 带上下文的删除操作,返回被删除文件的信息 +func (s *FileSystemService) DeletePathWithContext(ctx context.Context, path string) (*FileOperationResult, error) { // 路径验证 if err := s.validatePath(path); err != nil { - return err + return nil, err } - // 获取文件信息 + // 获取文件信息(在删除前保存) info, err := os.Stat(path) if err != nil { if os.IsNotExist(err) { - return fmt.Errorf("文件或目录不存在") + return nil, fmt.Errorf("文件或目录不存在") } - return fmt.Errorf("获取文件信息失败: %v", err) + return nil, fmt.Errorf("获取文件信息失败: %v", err) } // 检查删除限制 exceeds, details, checkErr := CheckDeleteRestrictions(path, info, s.config) if checkErr != nil { - return checkErr + return nil, checkErr } if exceeds { if s.config.Security.DeleteRestrictions.RequireConfirm { - return &DeleteRestrictionWarning{ + return nil, &DeleteRestrictionWarning{ Path: path, Details: details, Info: info, } } - return fmt.Errorf("删除限制: %s", details) + return nil, fmt.Errorf("删除限制: %s", details) } // 文件锁检查(可选) if s.lockChecker != nil { if err := s.lockChecker.SafeDeleteWithLockCheck(path); err != nil { - return err + return nil, err } } @@ -233,7 +246,7 @@ func (s *FileSystemService) DeletePathWithContext(ctx context.Context, path stri s.logDelete(path, info.IsDir(), info.Size(), deleteErr) if deleteErr != nil { - return fmt.Errorf("删除失败: %v", deleteErr) + return nil, fmt.Errorf("删除失败: %v", deleteErr) } // 如果启用回收站,移动到回收站而非永久删除 @@ -247,7 +260,17 @@ func (s *FileSystemService) DeletePathWithContext(ctx context.Context, path stri } } - return nil + // 返回被删除的文件信息,用于前端更新 + return &FileOperationResult{ + Path: path, + Name: info.Name(), + Size: info.Size(), + SizeStr: formatBytes(info.Size()), + IsDir: info.IsDir(), + ModTime: info.ModTime().Format("2006-01-02 15:04:05"), + Mode: info.Mode().String(), + Deleted: true, + }, nil } // ListDir 列出目录内容 @@ -292,14 +315,14 @@ func (s *FileSystemService) ListDir(path string) ([]map[string]interface{}, erro return result, nil } -// CreateDir 创建目录 -func (s *FileSystemService) CreateDir(path string) error { +// CreateDir 创建目录,返回创建的目录信息 +func (s *FileSystemService) CreateDir(path string) (*FileOperationResult, error) { if err := s.validatePath(path); err != nil { - return err + return nil, err } if err := os.MkdirAll(path, DefaultDirPermissions); err != nil { - return fmt.Errorf("创建目录失败: %v", err) + return nil, fmt.Errorf("创建目录失败: %v", err) } s.logAudit(AuditLogEntry{ @@ -310,23 +333,42 @@ func (s *FileSystemService) CreateDir(path string) error { Success: true, }) - return nil + // 获取创建的目录信息 + info, err := os.Stat(path) + if err != nil { + // 创建成功但获取信息失败,返回基本信息 + return &FileOperationResult{ + Path: path, + Name: filepath.Base(path), + IsDir: true, + }, nil + } + + return &FileOperationResult{ + Path: path, + Name: info.Name(), + Size: info.Size(), + SizeStr: formatBytes(info.Size()), + IsDir: true, + ModTime: info.ModTime().Format("2006-01-02 15:04:05"), + Mode: info.Mode().String(), + }, nil } -// CreateFile 创建空文件 -func (s *FileSystemService) CreateFile(path string) error { +// CreateFile 创建空文件,返回创建的文件信息 +func (s *FileSystemService) CreateFile(path string) (*FileOperationResult, error) { if err := s.validatePath(path); err != nil { - return err + return nil, err } // 检查文件是否已存在 if _, err := os.Stat(path); err == nil { - return fmt.Errorf("文件已存在") + return nil, fmt.Errorf("文件已存在") } file, err := os.Create(path) if err != nil { - return fmt.Errorf("创建文件失败: %v", err) + return nil, fmt.Errorf("创建文件失败: %v", err) } file.Close() @@ -338,7 +380,27 @@ func (s *FileSystemService) CreateFile(path string) error { Success: true, }) - return nil + // 获取创建的文件信息 + info, err := os.Stat(path) + if err != nil { + // 创建成功但获取信息失败,返回基本信息 + return &FileOperationResult{ + Path: path, + Name: filepath.Base(path), + IsDir: false, + Size: 0, + }, nil + } + + return &FileOperationResult{ + Path: path, + Name: info.Name(), + Size: info.Size(), + SizeStr: formatBytes(info.Size()), + IsDir: false, + ModTime: info.ModTime().Format("2006-01-02 15:04:05"), + Mode: info.Mode().String(), + }, nil } // GetInfo 获取文件信息(实现 FileService 接口) @@ -380,21 +442,21 @@ func (s *FileSystemService) OpenPath(path string) error { return OpenPath(path) } -// RenamePath 重命名文件或目录 -func (s *FileSystemService) RenamePath(oldPath, newPath string) error { +// RenamePath 重命名文件或目录,返回新文件信息 +func (s *FileSystemService) RenamePath(oldPath, newPath string) (*FileOperationResult, error) { // 验证旧路径 if err := s.validatePath(oldPath); err != nil { - return err + return nil, err } // 验证新路径 if err := s.validatePath(newPath); err != nil { - return err + return nil, err } // 执行重命名 if err := os.Rename(oldPath, newPath); err != nil { - return fmt.Errorf("重命名失败: %v", err) + return nil, fmt.Errorf("重命名失败: %v", err) } s.logAudit(AuditLogEntry{ @@ -405,7 +467,27 @@ func (s *FileSystemService) RenamePath(oldPath, newPath string) error { Success: true, }) - return nil + // 获取新文件信息 + info, err := os.Stat(newPath) + if err != nil { + // 重命名成功但获取信息失败,返回基本信息 + return &FileOperationResult{ + Path: newPath, + Name: filepath.Base(newPath), + OldPath: oldPath, + }, nil + } + + return &FileOperationResult{ + Path: newPath, + Name: info.Name(), + Size: info.Size(), + SizeStr: formatBytes(info.Size()), + IsDir: info.IsDir(), + ModTime: info.ModTime().Format("2006-01-02 15:04:05"), + Mode: info.Mode().String(), + OldPath: oldPath, + }, nil } // ========== ZIP操作接口 ========== diff --git a/internal/filesystem/service_interfaces.go b/internal/filesystem/service_interfaces.go deleted file mode 100644 index 86ca1ee..0000000 --- a/internal/filesystem/service_interfaces.go +++ /dev/null @@ -1,30 +0,0 @@ -package filesystem - -import ( - "context" -) - -// FileService 文件操作核心接口 -// 定义所有文件操作的基本功能,便于mock测试 -type FileService interface { - // 基本操作 - Read(path string) (string, error) - Write(path, content string) error - Delete(path string) error - List(path string) ([]map[string]interface{}, error) - CreateDir(path string) error - CreateFile(path string) error - GetInfo(path string) (map[string]interface{}, error) - Open(path string) error - - // 快捷方式 - ResolveShortcut(lnkPath string) (targetPath string, err error) - - // 配置 - GetConfig() *Config - Close(ctx context.Context) error -} - -// 确保实现接口 -var _ FileService = (*FileSystemService)(nil) - diff --git a/web/src/api/system.ts b/web/src/api/system.ts index 9b71654..27cd7b1 100644 --- a/web/src/api/system.ts +++ b/web/src/api/system.ts @@ -81,41 +81,41 @@ export async function writeFile(path: string, content: string): Promise { /** * 删除文件或目录 */ -export async function deletePath(path: string): Promise { +export async function deletePath(path: string): Promise { if (!window.go?.main?.App?.DeletePath) { throw new Error('DeletePath API 不可用') } - await window.go.main.App.DeletePath(path) + return await window.go.main.App.DeletePath(path) } /** * 创建目录 */ -export async function createDir(path: string): Promise { +export async function createDir(path: string): Promise { if (!window.go?.main?.App?.CreateDir) { throw new Error('CreateDir API 不可用') } - await window.go.main.App.CreateDir(path) + return await window.go.main.App.CreateDir(path) } /** * 创建文件 */ -export async function createFile(path: string): Promise { +export async function createFile(path: string): Promise { if (!window.go?.main?.App?.CreateFile) { throw new Error('CreateFile API 不可用') } - await window.go.main.App.CreateFile(path) + return await window.go.main.App.CreateFile(path) } /** * 重命名文件或目录 */ -export async function renamePath(oldPath: string, newPath: string): Promise { +export async function renamePath(oldPath: string, newPath: string): Promise { if (!window.go?.main?.App?.RenamePath) { throw new Error('RenamePath API 不可用') } - await window.go.main.App.RenamePath({ + return await window.go.main.App.RenamePath({ oldPath: String(oldPath), newPath: String(newPath) }) diff --git a/web/src/components/FileSystem/composables/useFileOperations.ts b/web/src/components/FileSystem/composables/useFileOperations.ts index a97d988..5baf92d 100644 --- a/web/src/components/FileSystem/composables/useFileOperations.ts +++ b/web/src/components/FileSystem/composables/useFileOperations.ts @@ -80,12 +80,13 @@ export function useFileOperations(options: UseFileOperationsOptions = {}) { } /** - * 删除路径(文件或目录) + * 删除路径(文件或目录),返回被删除的文件信息 */ - const deletePath = async (path: string): Promise => { + const deletePath = async (path: string): Promise => { try { - await deletePathApi(path) - onSuccess?.('deletePath', { path }) + const result = await deletePathApi(path) + onSuccess?.('deletePath', { path, result }) + return result as FileItem } catch (error) { const err = error as Error onError?.('deletePath', err) @@ -94,16 +95,17 @@ export function useFileOperations(options: UseFileOperationsOptions = {}) { } /** - * 创建新文件 + * 创建新文件,返回创建的文件信息 */ const createNewFile = async ( dirPath: string, filename: string, content: string = '' - ): Promise => { + ): Promise => { try { - await createFile(dirPath, filename, content) - onSuccess?.('createFile', { dirPath, filename }) + const result = await createFile(dirPath, filename, content) + onSuccess?.('createFile', { dirPath, filename, result }) + return result as FileItem } catch (error) { const err = error as Error onError?.('createFile', err) @@ -112,12 +114,13 @@ export function useFileOperations(options: UseFileOperationsOptions = {}) { } /** - * 创建新目录 + * 创建新目录,返回创建的目录信息 */ - const createNewDir = async (parentPath: string, dirname: string): Promise => { + const createNewDir = async (parentPath: string, dirname: string): Promise => { try { - await createDir(parentPath, dirname) - onSuccess?.('createDir', { parentPath, dirname }) + const result = await createDir(parentPath, dirname) + onSuccess?.('createDir', { parentPath, dirname, result }) + return result as FileItem } catch (error) { const err = error as Error onError?.('createDir', err) @@ -126,9 +129,9 @@ export function useFileOperations(options: UseFileOperationsOptions = {}) { } /** - * 重命名文件或目录 + * 重命名文件或目录,返回新文件信息 */ - const rename = async (oldPath: string, newName: string): Promise => { + const rename = async (oldPath: string, newName: string): Promise => { // 构造新路径 const separator = oldPath.includes('\\') ? '\\' : '/' const parentPath = oldPath.substring( @@ -138,8 +141,9 @@ export function useFileOperations(options: UseFileOperationsOptions = {}) { const newPath = parentPath + separator + newName try { - await renamePathApi(oldPath, newPath) - onSuccess?.('rename', { oldPath, newPath }) + const result = await renamePathApi(oldPath, newPath) + onSuccess?.('rename', { oldPath, newPath, result }) + return result as FileItem } catch (error) { const err = error as Error onError?.('rename', err) diff --git a/web/src/components/FileSystem/index.vue b/web/src/components/FileSystem/index.vue index 0538116..88a91e9 100644 --- a/web/src/components/FileSystem/index.vue +++ b/web/src/components/FileSystem/index.vue @@ -532,54 +532,67 @@ const handleSaveEditing = async (oldPath: string, newName: string) => { } try { - await fileOps.rename(oldPath, trimmedName) - - // 如果重命名的是当前打开的文件,更新其路径 + // 如果重命名的是当前打开的文件,先关闭编辑器和预览 if (selectedFileItem.value?.path === oldPath) { - selectedFileItem.value = { - ...selectedFileItem.value, - path: newPath, - name: trimmedName + // 如果是文件(不是文件夹),才需要关闭编辑器 + if (!selectedFileItem.value.is_dir) { + // 清空编辑器内容 + await clearContent() + + // 清空预览URL + if (previewUrl.value) { + previewUrl.value = '' + } } + + // 取消选中状态 + selectedFileItem.value = null + + // 等待文件句柄释放(文件需要更长时间) + await new Promise(resolve => setTimeout(resolve, 300)) } - // 刷新文件列表(重命名成功后必须刷新) - await loadDirectory(filePath.value) + const renamedFile = await fileOps.rename(oldPath, trimmedName) + + // 更新文件列表(保留收藏状态) + updateFileInList(oldPath, renamedFile) // 如果重命名的是收藏的文件,更新收藏夹中的路径 - // 注意:必须在刷新文件列表后才能找到新文件 if (isFavorite(oldPath)) { - // 移除旧路径 removeFav(oldPath) - // 添加新路径(保持收藏状态) - const newFile = fileList.value.find(f => f.path === newPath) - if (newFile) { - toggleFav(newFile) - } + toggleFav(renamedFile) } Message.success(`✓ 重命名成功: ${trimmedName}`) } catch (error: any) { - // 解析并清理错误消息 + // 提取错误信息 let errorMsg = error?.message || error?.toString() || '未知错误' - // 清理后端返回的错误消息(去除命令和路径部分) - // 格式:rename oldPath newPath: actual error message - if (errorMsg.includes(': ')) { - const parts = errorMsg.split(': ') - if (parts.length > 1) { - // 取最后一部分作为真正的错误信息 - errorMsg = parts.slice(1).join(': ') - } - } - - // 清理常见的错误前缀 + // 清理后端返回的错误前缀 errorMsg = errorMsg - .replace(/^rename\s+.*?:\s*/i, '') // 移除 "rename path: " 前缀 - .replace(/^create\s+.*?:\s*/i, '') // 移除 "create path: " 前缀 + .replace(/^rename\s+.*?:\s*/i, '') + .replace(/^create\s+.*?:\s*/i, '') + .replace(/^delete\s+.*?:\s*/i, '') .trim() + // 针对常见错误提供友好提示 + if (errorMsg.includes('being used by another process') || + errorMsg.includes('being used by another process') || + errorMsg.includes('被另一个进程占用')) { + errorMsg = '文件正在被其他程序使用,请先关闭该文件后重试' + if (selectedFileItem.value?.is_dir) { + errorMsg = '文件夹正在被其他程序使用(如文件管理器、终端等),请先关闭后重试' + } + } else if (errorMsg.includes('access is denied') || + errorMsg.includes('permission denied')) { + errorMsg = '权限不足,无法重命名该文件' + } else if (errorMsg.includes('no such file') || + errorMsg.includes('cannot find')) { + errorMsg = '文件不存在,可能已被删除或移动' + } + Message.error(`重命名失败: ${errorMsg}`) + // 失败时恢复编辑状态 editingFilePath.value = oldPath editingFileName.value = oldName @@ -730,9 +743,9 @@ const handleCreateFile = async () => { const fullPath = `${filePath.value}\\${fileName}` try { - await fileOps.createNewFile(fullPath) + const newFile = await fileOps.createNewFile(fullPath) Message.success(`✓ 文件 "${fileName}" 创建成功`) - await loadDirectory(filePath.value) + addFileToList(newFile) } catch (error: any) { Message.error(`创建文件失败: ${error.message || error}`) } @@ -786,9 +799,9 @@ const handleCreateDir = async () => { const fullPath = `${filePath.value}\\${folderName}` try { - await fileOps.createNewDir(fullPath) + const newDir = await fileOps.createNewDir(fullPath) Message.success(`✓ 文件夹 "${folderName}" 创建成功`) - await loadDirectory(filePath.value) + addFileToList(newDir) } catch (error: any) { Message.error(`创建文件夹失败: ${error.message || error}`) } @@ -827,6 +840,9 @@ const handleDeleteFile = async (file: FileItem) => { await fileOps.deletePath(targetPath) Message.success('删除成功') + // 从文件列表中移除 + removeFileFromList(targetPath) + // 如果删除的是收藏的文件,从收藏夹中移除 if (isFavorite(targetPath)) { removeFav(targetPath) @@ -836,9 +852,6 @@ const handleDeleteFile = async (file: FileItem) => { if (selectedFileItem.value?.path === targetPath) { selectedFileItem.value = null } - - // 刷新文件列表 - await loadDirectory(filePath.value) } catch (error: any) { Message.error(`删除失败: ${error.message || error}`) } @@ -988,6 +1001,35 @@ const loadDirectory = async (path: string) => { } } +/** + * 添加文件到列表(保持排序) + */ +const addFileToList = (item: FileItem) => { + fileList.value = sortFileList([...fileList.value, { ...item, is_favorite: false }]) +} + +/** + * 从列表中移除文件 + */ +const removeFileFromList = (path: string) => { + fileList.value = fileList.value.filter(f => f.path !== path) +} + +/** + * 更新列表中的文件信息(保留运行时状态如 is_favorite) + */ +const updateFileInList = (oldPath: string, newItem: FileItem) => { + const index = fileList.value.findIndex(f => f.path === oldPath) + if (index !== -1) { + // 保留原有属性(如 is_favorite),更新其他字段 + fileList.value[index] = { + ...fileList.value[index], // 保留原有所有属性 + ...newItem, // 覆盖新字段 + is_favorite: fileList.value[index].is_favorite // 确保保留收藏状态 + } + } +} + // 加载 ZIP 目录内容 const loadZipDirectoryContents = async (zipPath: string, currentDir: string): Promise => { fileLoading.value = true diff --git a/web/src/types/file-system.ts b/web/src/types/file-system.ts index 30e0d71..e30efeb 100644 --- a/web/src/types/file-system.ts +++ b/web/src/types/file-system.ts @@ -17,6 +17,10 @@ export interface FileItem { is_dir: boolean /** 修改时间 */ modified_time?: string + /** 是否被收藏(运行时属性) */ + is_favorite?: boolean + /** 旧路径(仅重命名操作时存在) */ + old_path?: string } /**