package main import ( "fmt" "log/slog" "path/filepath" "time" "u-desk/internal/filesystem" "github.com/wailsapp/wails/v3/pkg/application" ) // filesystemSvc 返回文件系统服务。初始化在启动 goroutine 异步进行, // 完成前前端触发文件操作时返回明确错误而非 nil panic func (a *App) filesystemSvc() (*filesystem.FileSystemService, error) { if a.filesystem == nil { return nil, fmt.Errorf("文件系统服务尚未初始化完成,请稍后重试") } return a.filesystem, nil } // EnsureDrawioViewer 确保本地已缓存 drawio viewer(首次预览时下载),前端打开 .drawio 前调用。 // 通过 drawio-download-progress 事件推送下载进度。 func (a *App) EnsureDrawioViewer() (map[string]interface{}, error) { err := filesystem.EnsureDrawioViewer(func(progress, speed float64, downloaded, total int64) { a.mainWindow.EmitEvent("drawio-download-progress", map[string]interface{}{ "progress": progress, "speed": speed, "downloaded": downloaded, "total": total, }) }) return map[string]interface{}{ "ready": err == nil, "version": filesystem.DrawioViewerVersion(), }, err } // ReadFile 读取文件 func (a *App) ReadFile(path string) (string, error) { fs, err := a.filesystemSvc() if err != nil { return "", err } return fs.ReadFile(path) } // WatchFile 开始监听指定文件的变化,变化时发送 file-changed 事件 func (a *App) WatchFile(path string) error { if a.fileWatcher == nil { return fmt.Errorf("文件监听器未初始化") } return a.fileWatcher.WatchFile(path) } // UnwatchFile 停止监听文件变化 func (a *App) UnwatchFile() { if a.fileWatcher != nil { a.fileWatcher.UnwatchFile() } } // WriteFile 写入文件 func (a *App) WriteFile(req WriteFileRequest) error { fs, err := a.filesystemSvc() if err != nil { return err } return fs.WriteFile(req.Path, req.Content) } // SaveBase64File 将 base64 内容解码后写入文件(用于图片等二进制数据) func (a *App) SaveBase64File(req SaveBase64FileRequest) error { fs, err := a.filesystemSvc() if err != nil { return err } return fs.SaveBase64File(req.Path, req.Content) } // ListDir 列出目录 func (a *App) ListDir(path string) ([]map[string]interface{}, error) { fs, err := a.filesystemSvc() if err != nil { return nil, err } return fs.ListDir(path) } // CreateDir 创建目录 func (a *App) CreateDir(path string) (*filesystem.FileOperationResult, error) { fs, err := a.filesystemSvc() if err != nil { return nil, err } return fs.CreateDir(path) } // CreateFile 创建文件 func (a *App) CreateFile(path string) (*filesystem.FileOperationResult, error) { fs, err := a.filesystemSvc() if err != nil { return nil, err } return fs.CreateFile(path) } // DeletePath 删除文件或目录 func (a *App) DeletePath(path string) (*filesystem.FileOperationResult, error) { fs, err := a.filesystemSvc() if err != nil { return nil, err } return fs.DeletePath(path) } // RenamePath 重命名文件或目录 func (a *App) RenamePath(req RenamePathRequest) (*filesystem.FileOperationResult, error) { fs, err := a.filesystemSvc() if err != nil { return nil, err } return fs.RenamePath(req.OldPath, req.NewPath) } // CopyPath 复制文件或目录(递归),通过 transfer-progress 事件推送进度 func (a *App) CopyPath(src, dst string) error { fs, err := a.filesystemSvc() if err != nil { return err } emitter := newTransferEmitter(a, "copy", filepath.Base(filepath.FromSlash(src))) err = fs.CopyPath(src, dst, func(copied, total int64) { emitter.progress(emitter.name, copied, total) }) emitter.finish(err) return err } // MovePath 移动文件或目录(同盘重命名,跨盘复制后删除),跨盘时经 transfer-progress 事件推送进度 func (a *App) MovePath(src, dst string) error { fs, err := a.filesystemSvc() if err != nil { return err } emitter := newTransferEmitter(a, "move", filepath.Base(filepath.FromSlash(src))) err = fs.MovePath(src, dst, func(copied, total int64) { emitter.progress(emitter.name, copied, total) }) emitter.finish(err) return err } // PickDirectory 打开系统目录选择器,返回选中目录(取消时返回空串) func (a *App) PickDirectory() (string, error) { dialog := application.Get().Dialog.OpenFileWithOptions(&application.OpenFileDialogOptions{ CanChooseDirectories: true, CanChooseFiles: false, CanCreateDirectories: true, Title: "选择目录", }) return dialog.PromptForSingleSelection() } // ========== 传输进度事件(下载/上传/本地复制共用) ========== // transferProgressInterval 进度事件最小推送间隔(节流) const transferProgressInterval = 100 * time.Millisecond // transferEmitter 封装 transfer-progress 事件的节流推送 type transferEmitter struct { app *App id string name string lastEmit time.Time } // newTransferID 生成传输任务标识 func newTransferID(prefix string) string { return fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano()) } // newTransferEmitter 创建进度推送器并发送首帧 running 事件 func newTransferEmitter(a *App, prefix, name string) *transferEmitter { te := &transferEmitter{app: a, id: newTransferID(prefix), name: name} te.emit(name, 0, 0, "") return te } // emit 推送一帧进度(errMsg 非空表示失败) func (t *transferEmitter) emit(name string, copied, total int64, errMsg string) { payload := map[string]interface{}{ "id": t.id, "name": name, "copied": copied, "total": total, "status": "running", } if errMsg != "" { payload["status"] = "error" payload["error"] = errMsg } t.app.emitEvent("transfer-progress", payload) } // progress 流式进度回调(100ms 节流;目录传输时 name 为当前文件名) func (t *transferEmitter) progress(name string, copied, total int64) { t.name = name now := time.Now() if now.Sub(t.lastEmit) < transferProgressInterval { return } t.lastEmit = now t.emit(name, copied, total, "") } // finish 结束传输:成功发 done,失败发 error(各一帧,不节流) func (t *transferEmitter) finish(err error) { if err != nil { t.emit(t.name, 0, 0, err.Error()) return } payload := map[string]interface{}{ "id": t.id, "name": t.name, "status": "done", } t.app.emitEvent("transfer-progress", payload) } // GetFileInfo 获取文件信息 func (a *App) GetFileInfo(path string) (map[string]interface{}, error) { fs, err := a.filesystemSvc() if err != nil { return nil, err } return fs.GetFileInfo(path) } // OpenPath 使用系统默认程序打开文件或目录 func (a *App) OpenPath(path string) error { fs, err := a.filesystemSvc() if err != nil { return err } return fs.OpenPath(path) } // ========== Zip 文件操作接口 ========== // ListZipContents 列出 zip 文件内容 func (a *App) ListZipContents(zipPath string) ([]map[string]interface{}, error) { fs, err := a.filesystemSvc() if err != nil { return nil, err } return fs.ListZipContents(zipPath) } // ExtractFileFromZip 从 zip 文件中提取单个文件内容 func (a *App) ExtractFileFromZip(zipPath, filePath string) (string, error) { fs, err := a.filesystemSvc() if err != nil { return "", err } return fs.ExtractFileFromZip(zipPath, filePath) } // ExtractFileFromZipToTemp 从 zip 文件中提取单个文件到临时目录 func (a *App) ExtractFileFromZipToTemp(zipPath, filePath string) (string, error) { fs, err := a.filesystemSvc() if err != nil { return "", err } return fs.ExtractFileFromZipToTemp(zipPath, filePath) } // GetZipFileInfo 获取 zip 文件中特定文件的信息 func (a *App) GetZipFileInfo(zipPath, filePath string) (map[string]interface{}, error) { fs, err := a.filesystemSvc() if err != nil { return nil, err } return fs.GetZipFileInfo(zipPath, filePath) } // ResolveShortcut 解析快捷方式文件,返回目标路径信息 func (a *App) ResolveShortcut(lnkPath string) (map[string]interface{}, error) { fs, err := a.filesystemSvc() if err != nil { return nil, err } targetPath, err := fs.ResolveShortcut(lnkPath) if err != nil { return map[string]interface{}{ "success": false, "message": err.Error(), }, err } fileInfo, err := fs.GetFileInfo(targetPath) if err != nil { return map[string]interface{}{ "success": true, "targetPath": targetPath, "targetExists": false, "targetAccessible": false, }, nil } return map[string]interface{}{ "success": true, "targetPath": targetPath, "targetExists": true, "targetAccessible": true, "targetInfo": fileInfo, }, nil } // startFileServer 启动文件服务器 func (a *App) startFileServer() { if _, err := filesystem.StartLocalFileServer(); err != nil { slog.Error("文件服务器启动失败", "error", err) return } slog.Info("文件服务器已启动", "addr", "http://"+filesystem.GetLocalFileServerAddr()) } // ========== 文件服务器接口 ========== // GetFileServerURL 获取本地文件服务器的URL func (a *App) GetFileServerURL() string { return fmt.Sprintf("http://%s", filesystem.GetLocalFileServerAddr()) } // DetectFileTypeByContent 通过文件内容检测文件类型 func (a *App) DetectFileTypeByContent(path string) (map[string]interface{}, error) { return filesystem.DetectFileTypeByContentSimple(path) } // initFilesystemModule 延迟初始化文件系统模块 func (a *App) initFilesystemModule() { if a.filesystem != nil { slog.Info("文件系统模块已初始化,跳过") return } slog.Info("延迟初始化文件系统模块") fsConfig := filesystem.DefaultConfig() var err error a.filesystem, err = filesystem.NewFileSystemService(fsConfig) if err != nil { slog.Error("文件系统模块初始化失败", "error", err) return } // startFileServer 内部 listen 同步(极快)、Serve 异步,无需再包 goroutine a.startFileServer() slog.Info("文件系统模块初始化完成") } // ========== 审计日志接口 ========== // GetAuditLogs 获取审计日志 func (a *App) GetAuditLogs(limit int) ([]map[string]interface{}, error) { fs, err := a.filesystemSvc() if err != nil { return nil, err } return fs.GetAuditLogs(limit) }