Files
u-desk/app_filesystem.go
T
lxy 56fe0d9b82 修复: 文件系统初始化守卫与传输收尾等七处
- filesystemSvc 守卫17绑定方法,异步初始化完成前返回明确错误
- MovePath 跨盘剪切建 move emitter,失败清理目标盘残留
- SFTP 空目录下载/上传补 MkdirAll 落地
- drawio 下载失败等待者立即返回,不再空轮询30分钟
- batch 高亮补 %1 与 %%i 规则
- 错误文案转换抽 errMessage 共享函数,11处归一
- log.Printf 统一 slog.Warn
2026-09-15 23:49:01 +08:00

375 lines
10 KiB
Go

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)
}