diff --git a/CHANGELOG.internal.md b/CHANGELOG.internal.md index a0bb2ea..2bd9f10 100644 --- a/CHANGELOG.internal.md +++ b/CHANGELOG.internal.md @@ -2,9 +2,62 @@ > 本文档记录所有技术细节,包括代码重构、构建优化等内部改动 -## [0.5.0] - 2026-05-01 (fs-only-v3) +## [0.4.1] - 2026-05-01 ~ 05-16 (fs-only-v3) -### Wails v3 迁移 🏗️ +--- + +### 2026-05-16 增量更新 + +#### App 结构体拆分 🏗️ + +app.go 从 1281 行拆为 10 个域文件: + +| 文件 | 方法数 | 职责 | +|------|--------|------| +| `app.go` | 5 | 结构体定义、生命周期、请求类型 | +| `app_config.go` | 12 | 配置管理、BGM、Profile CRUD | +| `app_filesystem.go` | 23 | 文件操作、回收站、文件监听 | +| `app_sftp.go` | 17 | SFTP 连接管理 | +| `app_oss.go` | 17 | OSS 对象存储 | +| `app_window.go` | 8 | 窗口控制 | +| `app_system.go` | 7 | 系统信息 | +| `app_update.go` | 11 | 版本更新管理 | +| `app_pdf.go` | 2 | PDF 导出 | +| `app_windows.go` | 4 | Windows 平台专用(`//go:build windows`) | + +#### 结构化日志 📝 + +`fmt.Print*` → `log/slog`(45 处):app.go、filesystem/service.go、storage/download_cache.go + +#### Bug 修复 🐛 + +- **预览状态级联失败**: `clearContent()` 未重置 `isBinaryFile`,模板 `v-if` 链被阻断 → 加 `isBinaryFile.value = false` +- **Tab 切换状态不同步**: `activateTab` 未更新 `filePath` → 加 `getParentPath` + `loadDirectory` +- **AsyncCodeEditor 永久死亡**: `defineAsyncComponent` 无 `onError` → 添加重试(限 2 次) +- **FileLock 误创建文件**: `O_RDWR|O_CREAT` → `O_RDWR`,`mode 0666` → `0` +- **OSS 目录重命名**: `c.Delete()` 返回值未检查,目录标记删除添加容错 + +#### 并发安全 🔒 + +- **RecycleBin**: 所有公开方法添加 `sync.Mutex`,`ListEntries()` 返回深拷贝副本 + +#### 缓存优化 📦 + +- **contentDetectCache**: 添加 200 条上限,超限全量清空 + +#### 依赖升级 ⬆️ + +Wails alpha.80→alpha.92, echo v4.15.1→v4.15.2, go-sqlite v1.22, modernc/sqlite v1.50, go-git v5.19 + +#### 代码审查 (15/17 修复) + +🔴 构建标签 · 死参数 · 重复逻辑 · 并发锁 | 🟡 错误处理 · 方法归属 · 日志 | ⚪ 注释 · 去重 + +**暂不处理**: ⚪⑬ fallback 路径 · ⚪⑯ ensure 风格 · ⚪⑰ ensure 模式 + +--- + +### 2026-05-01 Wails v3 迁移 #### 框架升级 - **Wails v2.12 → v3 alpha.80**: 全面迁移至 Wails v3 架构 diff --git a/CHANGELOG.md b/CHANGELOG.md index ac8981e..42d9ad3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,40 +1,42 @@ # 更新日志 -## [0.5.0] - 2026-05-05 (fs-only-v3) +## [0.4.1] - 2026-05-16 (fs-only-v3) ### 新增 ✨ -- **云 OSS 存储**: 七牛云/阿里云双厂商支持,AK/SK 认证连接 -- **多桶导航**: 根目录自动列出所有桶,点进桶浏览文件,桶级客户端懒创建+缓存 -- **OSS 全功能 CRUD**: 列目录/读文件/写文件/创建/删除/重命名/预签名URL -- **GBK 编码自动转换**: 文件预览智能检测编码(UTF-8/GBK),解决 LRC 等中文文件乱码 -- **桶图标 🪣**: OSS 桶与普通文件夹图标区分 -- **连接对话框 OSS 分类**: 「云OSS」父分类 + 厂商子选择(七牛云/阿里云) -- **Sidebar 折叠架构**: 收藏夹和帮助文档独立区块,各自支持折叠/展开 -- **帮助文档区块**: 静态快捷键参考面板,默认展示 -- **收藏夹内部滚动**: 收藏内容多时列表区域独立滚动,帮助区块固定底部不溢出 +- **云 OSS 存储**: 七牛云/阿里云双厂商,AK/SK 认证,桶级客户端懒创建+缓存 +- **OSS 全功能 CRUD**: 列目录/读文件/写文件/创建/删除/重命名/预签名 URL +- **OSS 区域自动嗅探**: 连接时自动探测桶区域,无需手动填写 +- **七牛云桶管理**: 查询桶域名、真实区域、设置访问权限(公开/私有) +- **HTML 网站预览**: OSS/SFTP 上的 HTML 文件自动下载关联资源(JS/CSS/图片),逐级嗅探网站根目录,完整预览静态站点 +- **BGM 音乐播放器**: 编辑器底部控制条,支持播放/暂停/上下首/进度拖拽/四种播放模式(单曲/顺序/随机/循环),播放列表持久化,失败自动跳过 +- **LRC 歌词同步显示**: 播放时自动查找同名 .lrc 文件,实时高亮滚动歌词,点击歌词行跳转 +- **全局热键**: Ctrl+Shift+B 系统级快捷键,应用最小化时快速唤起 +- **文件变更监听**: 文件外部被修改时自动刷新,避免编辑冲突 +- **下载缓存**: 远程文件预览缓存到本地 SQLite(24 小时过期),重启后仍有效 +- **远程服务器监控**: 侧边栏显示磁盘/CPU/内存使用率,超 90% 红色警告,15 秒自动刷新 +- **连接指示器快速导航**: 悬停弹出根目录子文件夹列表,可递归展开,快速跳转 +- **侧边栏自定义**: 设置面板支持服务器/收藏夹/帮助区块拖拽排序和显隐控制 -### 重构 🔧 -- **Wails v3 迁移**: 从 Wails v2 升级至 v3 alpha.80,全面重构项目架构 -- **前端目录规范化**: `web/` → `frontend/`,对齐 Wails v3 标准目录结构 -- **跨平台构建配置**: 新增 Android/iOS/Linux/macOS/Docker 构建模板(Taskfile.yml) -- **v3 Bindings**: 自动生成的 TypeScript 绑定替代手动维护的 wailsjs +### 优化 🚀 +- **侧边栏折叠架构**: 服务器、收藏夹、帮助独立可折叠区块,收藏夹内部独立滚动 +- **七牛云目录层级**: ListFiles 支持 delimiter,正确展示文件夹层级而非平铺 +- **七牛云签名 URL**: 修正私有空间下载/预览签名格式 +- **七牛云下载域名**: 智能解析策略:临时域名→自定义域名→CDN 兜底,失败自动重试 +- **GBK 编码自动转换**: 文件预览智能检测编码(UTF-8/GBK),中文文件不再乱码 +- **SFTP 功能对齐**: 补全粘贴图片、网站预览、系统监控、常用路径等能力 +- **预览缓存迁移**: 前端 LRU → 后端 SQLite,重启后缓存仍有效 +- **Wails v3 迁移**: 框架升级至 v3 alpha.92,核心依赖全面更新 ### 修复 🐛 -- **MP3 误报加载失败**: 音频 @canplay 清除错误状态 + previewUrl watcher 重置 -- **启动路径恢复错误**: 本地模式跳过 Linux/OSS 路径残留,避免 `open /bucket` 报错 -- **阿里云签名修复**: ListFiles 签名不含 list 查询参数(prefix/delimiter/marker/max-keys 非子资源) -- **阿里云 XML 解析**: `` 直接映射文件字段,修正 `Contents.Object` 嵌套错误 -- **阿里云 LastModified**: 宽容时间解析(4 种格式兼容) -- **临时文件白名单放行**: OSS/SFTP 预览文件绕过文件类型限制 -- **custom.js 404**: AssetOptions Middleware 拦截返回空响应,消除控制台报错 -- **longPressTimer TypeError**: `const` → `let` 修复重复赋值错误 -- **Arco Tabs padding**: 覆盖默认 16px padding-top -- **DevTools 可用性**: production 构建带 devtools tag + 延迟 OpenDevTools() 调用 - -### 变更说明 -- 分支: `feature/fs-only` → `fs-only-v3` -- 入口: main.go 新增 Middleware 中间件模式 -- build/config.yml executes 流水线对齐官方模板(once → background → blocking → primary) +- **二进制文件后预览失效**: 打开二进制文件后图片/视频无法预览 +- **跨服务器 Tab 切换**: 面包屑和文件列表未同步更新 +- **代码编辑器加载失败**: 异步组件失败后自动重试 +- **OSS 收藏打开失败**: provider 字段映射修正 +- **阿里云 OSS 签名/解析**: 文件列表获取失败、时间格式不兼容等问题 +- **启动路径错误**: 本地模式跳过无效的 OSS 路径残留 +- **OSS 目录重命名**: 单文件失败不中断整体操作 +- **文件锁定检查**: 不再误创建空文件 +- **长期运行内存增长**: 文件类型检测缓存添加上限 --- diff --git a/README.md b/README.md index 1150ecb..0379edf 100644 --- a/README.md +++ b/README.md @@ -4,52 +4,57 @@ ## 功能 -- 文件浏览 / 编辑 / 预览(文本、Markdown、图片、Office、PDF) -- 收藏夹管理(折叠/展开、拖拽排序、置顶) -- Markdown 编辑器(实时预览、语法高亮、Mermaid 图表) -- 远程文件服务器连接 -- 主题切换(亮色/暗色) -- 版本更新检查 +- **本地文件管理**: 浏览、编辑、重命名、删除、回收站 +- **SFTP 远程连接**: SSH 密钥/密码认证,文件上传下载 +- **云 OSS 存储**: 七牛云/阿里云,多桶浏览 +- **多格式预览**: 图片/视频/音频/PDF/Office/HTML +- **Markdown 编辑器**: 实时预览、Mermaid 图表、语法高亮 +- **收藏夹管理**: 折叠/展开、拖拽排序、置顶 +- **BGM 播放**: 音频播放列表、全局热键 Ctrl+Shift+B +- **PDF 导出**: Markdown/HTML 导出为 PDF +- **版本更新检查**: 自动检测新版本 +- **主题切换**: 亮色/暗色 ## 技术栈 | 层 | 技术 | |---|------| -| 桌面框架 | Wails v3 (alpha.80) | -| 后端 | Go 1.22+ | +| 桌面框架 | Wails v3 (alpha.82) | +| 后端 | Go 1.24 | | 前端 | Vue 3 + TypeScript | | UI 组件库 | Arco Design Vue | | 编辑器 | CodeMirror 6 | | 构建 | Vite 7 + Taskfile | +| 数据库 | SQLite (配置/历史) | +| 文件服务 | 内置 HTTP 文件服务器 (端口 2652) | ## 项目结构 ``` -├── main.go # 入口:窗口配置、中间件、DevTools -├── app.go # 应用逻辑:文件系统、更新检查等 -├── internal/ # 内部模块 -│ ├── filesystem/ # 文件操作、锁、预览服务 -│ └── api/ # API 处理器 -├── frontend/ # 前端代码 (Vue 3) -│ ├── src/ -│ │ ├── components/FileSystem/ # 文件管理主组件 -│ │ ├── stores/ # Pinia 状态管理 -│ │ ├── api/ # 后端调用封装 -│ │ └── utils/ # 工具函数 -│ └── vite.config.js +├── main.go # 入口:窗口配置、中间件 +├── app.go # App 结构体定义、生命周期、请求类型 +├── app_*.go # 按功能域拆分(config/sftp/oss/...) +├── internal/ +│ ├── api/ # Wails 绑定 API 层 +│ ├── filesystem/ # 文件系统服务 + HTTP 文件服务器 +│ ├── ossdrv/ # OSS 驱动(七牛/阿里云) +│ ├── sftp/ # SFTP 客户端 +│ ├── storage/ # SQLite 存储 +│ └── service/ # 业务服务 +├── frontend/ +│ └── src/ +│ ├── components/FileSystem/ # 文件管理主组件 +│ ├── api/ # 后端调用封装 +│ ├── composables/ # Vue Composables +│ └── stores/ # Pinia 状态管理 ├── build/ # 构建配置(跨平台) -│ ├── config.yml # Wails 项目配置 -│ └── windows/ # Windows 构建脚本 └── configs/ # 运行时配置 ``` ## 开发 ```bash -# 安装依赖 -wails3 task common:install:frontend:deps - -# 启动开发模式(热重载) +# 启动开发模式 wails3 dev # 生产构建 @@ -59,16 +64,16 @@ wails3 build ### 构建标签 - `production` — 生产模式,使用嵌入的 frontend dist -- `devtools` — 在生产构建中保留 DevTools(F12) +- `devtools` — 在生产构建中保留 DevTools ## 快捷键 | 快捷键 | 功能 | |--------|------| | Ctrl+B | 切换侧边栏 | -| Ctrl+H | 历史记录 | -| Ctrl+F | 聚焦搜索 | +| Ctrl+Shift+B | BGM 播放栏 | +| F11 | 全屏预览 | ## 版本历史 -详见 [CHANGELOG.md](./CHANGELOG.md) +详见 [CHANGELOG.md](./CHANGELOG.md) | 内部技术细节见 [CHANGELOG.internal.md](./CHANGELOG.internal.md) diff --git a/app.go b/app.go index 005eea1..667937d 100644 --- a/app.go +++ b/app.go @@ -5,18 +5,13 @@ package main import ( "context" - "encoding/json" "fmt" - "os" - "path/filepath" - stdruntime "runtime" - "strconv" - "strings" + "log/slog" + "sync" "time" "u-desk/internal/api" - "u-desk/internal/common" "u-desk/internal/filewatch" "u-desk/internal/filesystem" "u-desk/internal/hotkey" @@ -24,15 +19,9 @@ import ( "u-desk/internal/service" "u-desk/internal/sftp" "u-desk/internal/storage" - "u-desk/internal/storage/models" - "u-desk/internal/system" - - "golang.org/x/sys/windows/registry" "github.com/wailsapp/wails/v3/pkg/application" - "github.com/wailsapp/wails/v3/pkg/w32" - "gorm.io/gorm" -) + ) // App 应用结构体 type App struct { @@ -61,6 +50,13 @@ func NewApp() *App { return &App{} } +// emitEvent 向前端发送事件 +func (a *App) emitEvent(name string, data ...any) { + if a.mainWindow != nil { + a.mainWindow.EmitEvent(name, data...) + } +} + // SetMainWindow 设置主窗口引用(由 main.go 在创建窗口后调用) func (a *App) SetMainWindow(w *application.WebviewWindow) { a.mainWindow = w @@ -83,15 +79,15 @@ func (a *App) RegisterGlobalHotkey() { } hwnd := uintptr(a.mainWindow.NativeWindow()) if hwnd == 0 { - fmt.Println("[全局热键] HWND 为 0,注册跳过") + slog.Warn("HWND 为 0,注册跳过") return } const id int32 = 1 if err := hotkey.Register(hwnd, id, hotkey.ModControl|hotkey.ModShift, 0x42); err != nil { - fmt.Printf("[全局热键] RegisterHotKey Ctrl+Shift+B 失败: %v\n", err) + slog.Error("RegisterHotKey Ctrl+Shift+B 失败", "error", err) return } - fmt.Println("[全局热键] Ctrl+Shift+B 已注册") + slog.Info("Ctrl+Shift+B 已注册") a.unregisterHotkey = func() { hotkey.Unregister(hwnd, id) } } @@ -123,25 +119,27 @@ func (a *App) ServiceStartup(ctx context.Context, _ application.ServiceOptions) a.configAPI = configService // 2.5. 迁移旧配置 - _ = a.configAPI.MigrateTabConfig() + if err := a.configAPI.MigrateTabConfig(); err != nil { + slog.Warn("配置迁移失败", "error", err) + } // 2.6. 初始化PDF导出API - fmt.Println("[启动] 初始化PDF导出模块...") + slog.Info("初始化PDF导出模块") pdfAPI, err := api.NewPdfAPI() if err != nil { - fmt.Printf("[启动] PDF导出API初始化失败: %v\n", err) + slog.Error("PDF导出API初始化失败", "error", err) } else { a.pdfAPI = pdfAPI - fmt.Println("[启动] PDF导出模块初始化完成") + slog.Info("PDF导出模块初始化完成") } // 3. 初始化版本号(提前触发缓存,避免后续重复计算) version := service.GetCurrentVersion() - fmt.Printf("[启动] 当前版本: %s\n", version) + slog.Info("当前版本", "version", version) // 4. 读取配置,获取可见的 Tabs visibleTabs := a.getVisibleTabs() - fmt.Printf("[启动] 可用的模块: %v\n", visibleTabs) + slog.Info("可用的模块", "modules", visibleTabs) // 4. 根据配置初始化模块(条件初始化) if err := a.initModulesByConfig(visibleTabs); err != nil { @@ -153,7 +151,11 @@ func (a *App) ServiceStartup(ctx context.Context, _ application.ServiceOptions) // 6. 异步初始化:UpdateAPI(涉及网络请求,完全异步) go func() { - if updateAPI, err := api.NewUpdateAPI("https://c.1216.top/last-version.json"); err == nil { + checkURL := "https://c.1216.top/last-version.json" + if cfg, err := service.LoadUpdateConfig(); err == nil && cfg.CheckURL != "" { + checkURL = cfg.CheckURL + } + if updateAPI, err := api.NewUpdateAPI(checkURL); err == nil { a.mu.Lock() a.updateAPI = updateAPI a.mu.Unlock() @@ -183,76 +185,12 @@ func (a *App) ServiceStartup(ctx context.Context, _ application.ServiceOptions) return } } - fmt.Println("[全局热键] 等待窗口超时") + slog.Warn("等待窗口超时") }() return nil } -// getVisibleTabs 获取配置中的可见 Tabs -func (a *App) getVisibleTabs() []string { - config, err := a.configAPI.GetAppConfig() - if err != nil { - fmt.Printf("[启动] 读取配置失败,使用默认配置: %v\n", err) - return common.DefaultVisibleTabs - } - - success, ok := config["success"].(bool) - if !ok || !success { - fmt.Printf("[启动] 配置读取失败,使用默认配置\n") - return common.DefaultVisibleTabs - } - - data, ok := config["data"].(map[string]interface{}) - if !ok { - return common.DefaultVisibleTabs - } - - visibleTabsInterface, ok := data["visibleTabs"].([]interface{}) - if !ok { - return common.DefaultVisibleTabs - } - - visibleTabs := common.InterfaceSliceToStringSlice(visibleTabsInterface) - - if len(visibleTabs) == 0 { - return common.DefaultVisibleTabs - } - - return visibleTabs -} - -// initModulesByConfig 根据配置初始化模块 -func (a *App) initModulesByConfig(visibleTabs []string) error { - if common.Contains(visibleTabs, common.TabFileSystem) { - fmt.Println("[启动] 初始化文件系统模块...") - - fsConfig := filesystem.DefaultConfig() - var err error - a.filesystem, err = filesystem.NewFileSystemService(fsConfig) - if err != nil { - return fmt.Errorf("文件系统服务初始化失败: %w", err) - } - - go a.startFileServer() - - fmt.Println("[启动] 文件系统模块初始化完成") - } else { - fmt.Println("[启动] 跳过文件系统模块(未启用)") - } - - return nil -} - -// startFileServer 启动文件服务器 -func (a *App) startFileServer() { - if _, err := filesystem.StartLocalFileServer(); err != nil { - fmt.Printf("[文件服务器] 启动失败: %v\n", err) - return - } - fmt.Printf("[文件服务器] 启动在 http://%s\n", filesystem.GetLocalFileServerAddr()) -} - // ServiceShutdown Wails v3 服务关闭生命周期(替代 v2 的 Shutdown) func (a *App) ServiceShutdown() error { if a.updateTicker != nil { @@ -266,19 +204,19 @@ func (a *App) ServiceShutdown() error { defer cancel() if a.filesystem != nil { - fmt.Println("[文件系统服务] 正在关闭...") + slog.Info("文件系统服务正在关闭") if err := a.filesystem.Close(shutdownCtx); err != nil { - fmt.Printf("[文件系统服务] 关闭失败: %v\n", err) + slog.Error("文件系统服务关闭失败", "error", err) } else { - fmt.Println("[文件系统服务] 已关闭") + slog.Info("文件系统服务已关闭") } } - fmt.Println("[文件服务器] 正在关闭...") + slog.Info("文件服务器正在关闭") if err := filesystem.ShutdownLocalFileServer(); err != nil { - fmt.Printf("[文件服务器] 关闭失败: %v\n", err) + slog.Error("文件服务器关闭失败", "error", err) } else { - fmt.Println("[文件服务器] 已关闭") + slog.Info("文件服务器已关闭") } // 关闭所有 SFTP 连接 + 清理临时文件 @@ -293,514 +231,24 @@ func (a *App) ServiceShutdown() error { return nil } -// GetSystemInfo 获取系统信息 -func (a *App) GetSystemInfo() (map[string]interface{}, error) { - return system.GetSystemInfo() -} - -// GetCPUInfo 获取 CPU 信息 -func (a *App) GetCPUInfo() (map[string]interface{}, error) { - return system.GetCPUInfo() -} - -// GetMemoryInfo 获取内存信息 -func (a *App) GetMemoryInfo() (map[string]interface{}, error) { - return system.GetMemoryInfo() -} - -// GetDiskInfo 获取磁盘信息 -func (a *App) GetDiskInfo() ([]map[string]interface{}, error) { - return system.GetDiskInfo() -} - -// ReadFile 读取文件 -func (a *App) ReadFile(path string) (string, error) { - return a.filesystem.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() - } -} - // WriteFileRequest 写入文件请求结构体 type WriteFileRequest struct { Path string `json:"path"` Content string `json:"content"` } -// WriteFile 写入文件 -func (a *App) WriteFile(req WriteFileRequest) error { - return a.filesystem.WriteFile(req.Path, req.Content) -} - // SaveBase64FileRequest 保存 Base64 编码的二进制文件 type SaveBase64FileRequest struct { Path string `json:"path"` Content string `json:"content"` // base64 编码的文件内容 } -// SaveBase64File 将 base64 内容解码后写入文件(用于图片等二进制数据) -func (a *App) SaveBase64File(req SaveBase64FileRequest) error { - return a.filesystem.SaveBase64File(req.Path, req.Content) -} - -// ListDir 列出目录 -func (a *App) ListDir(path string) ([]map[string]interface{}, error) { - return a.filesystem.ListDir(path) -} - -// CreateDir 创建目录 -func (a *App) CreateDir(path string) (*filesystem.FileOperationResult, error) { - return a.filesystem.CreateDir(path) -} - -// CreateFile 创建文件 -func (a *App) CreateFile(path string) (*filesystem.FileOperationResult, error) { - return a.filesystem.CreateFile(path) -} - -// DeletePath 删除文件或目录 -func (a *App) DeletePath(path string) (*filesystem.FileOperationResult, error) { - return a.filesystem.DeletePath(path) -} - // RenamePathRequest 重命名文件或目录请求结构体 type RenamePathRequest struct { OldPath string `json:"oldPath"` NewPath string `json:"newPath"` } -// RenamePath 重命名文件或目录 -func (a *App) RenamePath(req RenamePathRequest) (*filesystem.FileOperationResult, error) { - return a.filesystem.RenamePath(req.OldPath, req.NewPath) -} - -// GetFileInfo 获取文件信息 -func (a *App) GetFileInfo(path string) (map[string]interface{}, error) { - return a.filesystem.GetFileInfo(path) -} - -// GetEnvVars 获取环境变量 -func (a *App) GetEnvVars() (map[string]string, error) { - envVars := make(map[string]string) - for _, env := range os.Environ() { - if key, value, found := strings.Cut(env, "="); found { - envVars[key] = value - } - } - return envVars, nil -} - -// OpenPath 使用系统默认程序打开文件或目录 -func (a *App) OpenPath(path string) error { - return a.filesystem.OpenPath(path) -} - -// ========== Zip 文件操作接口 ========== - -// ListZipContents 列出 zip 文件内容 -func (a *App) ListZipContents(zipPath string) ([]map[string]interface{}, error) { - return a.filesystem.ListZipContents(zipPath) -} - -// ExtractFileFromZip 从 zip 文件中提取单个文件内容 -func (a *App) ExtractFileFromZip(zipPath, filePath string) (string, error) { - return a.filesystem.ExtractFileFromZip(zipPath, filePath) -} - -// ExtractFileFromZipToTemp 从 zip 文件中提取单个文件到临时目录 -func (a *App) ExtractFileFromZipToTemp(zipPath, filePath string) (string, error) { - return a.filesystem.ExtractFileFromZipToTemp(zipPath, filePath) -} - -// GetZipFileInfo 获取 zip 文件中特定文件的信息 -func (a *App) GetZipFileInfo(zipPath, filePath string) (map[string]interface{}, error) { - return a.filesystem.GetZipFileInfo(zipPath, filePath) -} - -// ResolveShortcut 解析快捷方式文件,返回目标路径信息 -func (a *App) ResolveShortcut(lnkPath string) (map[string]interface{}, error) { - targetPath, err := a.filesystem.ResolveShortcut(lnkPath) - if err != nil { - return map[string]interface{}{ - "success": false, - "message": err.Error(), - }, err - } - - fileInfo, err := a.filesystem.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 -} - - -// getWindowsSpecialFolder 从注册表读取 Windows 特殊文件夹的真实路径 -func getWindowsSpecialFolder(guid string, fallbackName string) string { - key, err := registry.OpenKey(registry.CURRENT_USER, - `Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders`, - registry.READ) - if err != nil { - return "" - } - defer key.Close() - - val, _, err := key.GetStringValue(guid) - if err != nil || val == "" { - return "" - } - - path := os.ExpandEnv(val) - if _, err := os.Stat(path); err != nil { - return "" - } - return path -} - -// GetCommonPaths 获取常用系统路径 -func (a *App) GetCommonPaths() (map[string]string, error) { - homeDir, err := os.UserHomeDir() - if err != nil { - return nil, err - } - - paths := map[string]string{ - "home": homeDir, - } - - folderGUIDs := map[string]string{ - "desktop": "{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}", - "documents": "{D20B4C7F-5EA7-424C-B25E-039F6F1FCC8A}", - "downloads": "{374DE290-123F-4565-9164-39C4925E467B}", - } - for name, guid := range folderGUIDs { - if p := getWindowsSpecialFolder(guid, name); p != "" { - paths[name] = p - } else { - paths[name] = filepath.Join(homeDir, strings.ToUpper(name[:1])+name[1:]) - } - } - - if stdruntime.GOOS == "windows" { - for _, drive := range "ABCDEFGHIJKLMNOPQRSTUVWXYZ" { - path := string(drive) + ":\\" - if _, err := os.Stat(path); err == nil { - key := fmt.Sprintf("root_%c", drive) - paths[key] = path - } - } - } - - return paths, nil -} - -// Reload 重新加载窗口(用于菜单项) -func (a *App) Reload() { - if a.mainWindow != nil { - a.mainWindow.Reload() - } -} - -// ClearCache 清理本地缓存(用于菜单项) -func (a *App) ClearCache() { - if a.mainWindow != nil { - a.mainWindow.EmitEvent("clear-cache") - } -} - -// ========== 窗口控制方法 ========== - -// WindowMinimize 最小化窗口 -func (a *App) WindowMinimize() { - if a.mainWindow != nil { - a.mainWindow.Minimise() - } -} - -// WindowMaximize 最大化/还原窗口 -func (a *App) WindowMaximize() { - if a.mainWindow == nil { - return - } - if a.mainWindow.IsMaximised() { - a.mainWindow.UnMaximise() - } else { - a.mainWindow.Maximise() - } -} - -// WindowClose 关闭窗口 -func (a *App) WindowClose() { - application.Get().Quit() -} - -// WindowIsMaximized 检查窗口是否最大化 -func (a *App) WindowIsMaximized() bool { - if a.mainWindow != nil { - return a.mainWindow.IsMaximised() - } - return false -} - -// WindowToggleAlwaysOnTop 切换窗口置顶 -func (a *App) WindowToggleAlwaysOnTop() bool { - if a.mainWindow == nil { - return false - } - a.isAlwaysOnTop = !a.isAlwaysOnTop - a.mainWindow.SetAlwaysOnTop(a.isAlwaysOnTop) - return a.isAlwaysOnTop -} - -// SetWindowTitleBarColor 设置原生标题栏颜色 + 主题模式(0x00BBGGRR 格式) -func (a *App) SetWindowTitleBarColor(color uint32, isDark bool) { - if a.mainWindow == nil || stdruntime.GOOS != "windows" { - return - } - hwnd := uintptr(a.mainWindow.NativeWindow()) - if hwnd == 0 { - return - } - w32.SetTheme(hwnd, isDark) - w32.SetTitleBarColour(hwnd, color) -} - -// ========== 版本更新管理接口 ========== - -// requireUpdateAPI 检查 updateAPI 是否已初始化 -func (a *App) requireUpdateAPI() (*api.UpdateAPI, error) { - a.mu.Lock() - defer a.mu.Unlock() - if a.updateAPI == nil { - return nil, fmt.Errorf("更新功能正在初始化中") - } - return a.updateAPI, nil -} - -// CheckUpdate 检查更新 -func (a *App) CheckUpdate() (map[string]interface{}, error) { - api, err := a.requireUpdateAPI() - if err != nil { - return nil, err - } - return api.CheckUpdate() -} - -// GetCurrentVersion 获取当前版本号 -func (a *App) GetCurrentVersion() (map[string]interface{}, error) { - api, err := a.requireUpdateAPI() - if err != nil { - return nil, err - } - return api.GetCurrentVersion() -} - -// GetUpdateConfig 获取更新配置 -func (a *App) GetUpdateConfig() (map[string]interface{}, error) { - api, err := a.requireUpdateAPI() - if err != nil { - return nil, err - } - return api.GetUpdateConfig() -} - -// SetUpdateConfig 设置更新配置 -func (a *App) SetUpdateConfig(autoCheckEnabled bool, checkIntervalMinutes int, checkURL string) (map[string]interface{}, error) { - api, err := a.requireUpdateAPI() - if err != nil { - return nil, err - } - return api.SetUpdateConfig(autoCheckEnabled, checkIntervalMinutes, checkURL) -} - -// DownloadUpdate 下载更新包 -func (a *App) DownloadUpdate(downloadURL string) (map[string]interface{}, error) { - api, err := a.requireUpdateAPI() - if err != nil { - return nil, err - } - return api.DownloadUpdate(downloadURL) -} - -// InstallUpdate 安装更新包 -func (a *App) InstallUpdate(installerPath string, autoRestart bool) (map[string]interface{}, error) { - api, err := a.requireUpdateAPI() - if err != nil { - return nil, err - } - return api.InstallUpdate(installerPath, autoRestart) -} - -// InstallUpdateWithHash 安装更新包(带哈希验证) -func (a *App) InstallUpdateWithHash(installerPath string, autoRestart bool, expectedHash string, hashType string) (map[string]interface{}, error) { - api, err := a.requireUpdateAPI() - if err != nil { - return nil, err - } - return api.InstallUpdateWithHash(installerPath, autoRestart, expectedHash, hashType) -} - -// VerifyUpdateFile 验证更新文件哈希值 -func (a *App) VerifyUpdateFile(filePath string, expectedHash string, hashType string) (map[string]interface{}, error) { - api, err := a.requireUpdateAPI() - if err != nil { - return nil, err - } - return api.VerifyUpdateFile(filePath, expectedHash, hashType) -} - -// startAutoUpdateCheck 启动自动更新检查 -func (a *App) startAutoUpdateCheck() { - if a.updateAPI == nil { - return - } - - config, err := a.updateAPI.GetUpdateConfig() - if err != nil { - return - } - success, ok := config["success"].(bool) - if !ok || !success { - return - } - - configData, ok := config["data"].(map[string]interface{}) - if !ok { - return - } - - autoCheckEnabled, ok := configData["auto_check_enabled"].(bool) - if !ok || !autoCheckEnabled { - return - } - - interval, ok := configData["check_interval_minutes"].(int) - if !ok || interval <= 0 { - interval = 5 - } - - go a.checkUpdate() - - a.updateTicker = time.NewTicker(time.Duration(interval) * time.Minute) - go func() { - for range a.updateTicker.C { - a.checkUpdate() - } - }() -} - -// checkUpdate 执行更新检查 -func (a *App) checkUpdate() { - defer func() { - if r := recover(); r != nil { - fmt.Printf("[自动检查更新] 发生错误: %v\n", r) - } - }() - - a.mu.Lock() - api := a.updateAPI - a.mu.Unlock() - - if api == nil { - return - } - - result, err := api.CheckUpdate() - if err != nil { - return - } - - success, ok := result["success"].(bool) - if !ok || !success { - return - } - - data, ok := result["data"].(map[string]interface{}) - if !ok { - return - } - - hasUpdate, ok := data["has_update"].(bool) - if ok && hasUpdate && a.mainWindow != nil { - a.mainWindow.EmitEvent("update-available", data) - } -} - -// ========== 审计日志接口 ========== - -// GetAuditLogs 获取审计日志 -func (a *App) GetAuditLogs(limit int) ([]map[string]interface{}, error) { - return a.filesystem.GetAuditLogs(limit) -} - -// ========== 文件服务器接口 ========== - -// 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) -} - -// ========== 回收站接口 ========== - -// GetRecycleBinEntries 获取回收站条目 -func (a *App) GetRecycleBinEntries() ([]map[string]interface{}, error) { - return a.filesystem.GetRecycleBinEntries() -} - -// RestoreFromRecycleBin 从回收站恢复文件 -func (a *App) RestoreFromRecycleBin(recyclePath string) error { - return a.filesystem.RestoreFromRecycleBin(recyclePath) -} - -// DeletePermanently 永久删除回收站中的文件 -func (a *App) DeletePermanently(recyclePath string) error { - return a.filesystem.DeletePermanently(recyclePath) -} - -// EmptyRecycleBin 清空回收站 -func (a *App) EmptyRecycleBin() error { - return a.filesystem.EmptyRecycleBin() -} - -// ========== 应用配置接口 ========== - -// GetAppConfig 获取应用配置 -func (a *App) GetAppConfig() (map[string]interface{}, error) { - if a.configAPI == nil { - return nil, fmt.Errorf("配置服务正在初始化中") - } - return a.configAPI.GetAppConfig() -} - // SaveAppConfigRequest 保存应用配置请求 type SaveAppConfigRequest struct { Tabs []api.AppTabDefinition `json:"tabs"` @@ -808,136 +256,6 @@ type SaveAppConfigRequest struct { DefaultTab string `json:"defaultTab"` } -// SaveAppConfig 保存应用配置 -func (a *App) SaveAppConfig(req SaveAppConfigRequest) (map[string]interface{}, error) { - if a.configAPI == nil { - return nil, fmt.Errorf("配置服务正在初始化中") - } - - oldConfig, _ := a.configAPI.GetAppConfig() - var oldVisibleTabs []string - if success, ok := oldConfig["success"].(bool); ok && success { - if data, ok := oldConfig["data"].(map[string]interface{}); ok { - if vtInterface, ok := data["visibleTabs"].([]interface{}); ok { - oldVisibleTabs = common.InterfaceSliceToStringSlice(vtInterface) - } - } - } - - apiReq := api.SaveAppConfigRequest{ - Tabs: req.Tabs, - VisibleTabs: req.VisibleTabs, - DefaultTab: req.DefaultTab, - } - - result, err := a.configAPI.SaveAppConfig(apiReq) - if err != nil { - return result, err - } - - if success, ok := result["success"].(bool); ok && success { - a.handleNewlyEnabledModules(oldVisibleTabs, req.VisibleTabs) - } - - return result, nil -} - -// handleNewlyEnabledModules 处理新启用的模块 -func (a *App) handleNewlyEnabledModules(oldTabs, newTabs []string) { - newlyEnabled := common.Difference(newTabs, oldTabs) - - if len(newlyEnabled) == 0 { - return - } - - fmt.Printf("[模块] 检测到新启用的模块: %v\n", newlyEnabled) - - for _, tab := range newlyEnabled { - switch tab { - case common.TabFileSystem: - a.initFilesystemModule() - case common.TabDevice: - fmt.Println("[模块] 设备测试模块已启用") - } - } -} - -// initFilesystemModule 延迟初始化文件系统模块 -func (a *App) initFilesystemModule() { - if a.filesystem != nil { - fmt.Println("[模块] 文件系统模块已初始化,跳过") - return - } - - fmt.Println("[模块] 延迟初始化文件系统模块...") - fsConfig := filesystem.DefaultConfig() - - var err error - a.filesystem, err = filesystem.NewFileSystemService(fsConfig) - if err != nil { - fmt.Printf("[模块] 文件系统模块初始化失败: %v\n", err) - return - } - - go a.startFileServer() - - fmt.Println("[模块] 文件系统模块初始化完成") -} - -// ExportPDF 导出PDF文件 -func (a *App) ExportPDF(content string, title string, fileName string, fontSize int, pageWidth int, pageHeight int) (map[string]interface{}, error) { - if a.pdfAPI == nil { - return map[string]interface{}{ - "success": false, - "message": "PDF导出功能未初始化", - }, fmt.Errorf("PDF导出功能未初始化") - } - - req := api.PdfExportRequest{ - Content: content, - Title: title, - FileName: fileName, - FontSize: fontSize, - PageWidth: pageWidth, - PageHeight: pageHeight, - } - - result, err := a.pdfAPI.ExportMarkdownToPDF(req) - if err != nil { - return map[string]interface{}{ - "success": false, - "message": err.Error(), - }, err - } - - return map[string]interface{}{ - "success": result.Success, - "message": result.Message, - "path": result.Path, - "size": result.Size, - }, nil -} - -// SelectPDFSaveDirectory 选择PDF保存目录 -func (a *App) SelectPDFSaveDirectory() (string, error) { - if a.pdfAPI == nil { - return "", fmt.Errorf("PDF导出功能未初始化") - } - - return a.pdfAPI.SelectDirectory() -} - -// ========== SFTP 接口 ========== - -func (a *App) ensureSftpService() *sftp.Service { - a.mu.Lock() - defer a.mu.Unlock() - if a.sftpService == nil { - a.sftpService = sftp.NewService() - } - return a.sftpService -} - // SftpConnectRequest SFTP 连接请求 type SftpConnectRequest struct { Host string `json:"host"` @@ -948,59 +266,6 @@ type SftpConnectRequest struct { KeyPassphrase string `json:"key_passphrase"` } -// SftpConnect 建立 SFTP 连接,返回连接标识符 connID -func (a *App) SftpConnect(req SftpConnectRequest) (string, error) { - config := &sftp.Config{ - Host: req.Host, - Port: req.Port, - Username: req.Username, - Password: req.Password, - KeyPath: req.KeyPath, - KeyPassphrase: req.KeyPassphrase, - } - if config.Port == 0 { - config.Port = 22 - } - if config.Timeout == 0 { - config.Timeout = 15 * time.Second - } - - svc := a.ensureSftpService() - _, err := svc.GetManager().Connect(config) - if err != nil { - return "", sftp.ToUserMessage(err) - } - - connID := sftp.ConnID(config.Host, config.Port) - return connID, nil -} - -// SftpDisconnect 断开 SFTP 连接 -func (a *App) SftpDisconnect(connID string) error { - parts := strings.SplitN(connID, ":", 2) - if len(parts) < 2 { - return fmt.Errorf("无效的连接标识符") - } - host := parts[0] - port, err := strconv.Atoi(parts[1]) - if err != nil { - return fmt.Errorf("无效的端口号") - } - - sftp.GetManager().Disconnect(host, port) - return nil -} - -// SftpListDir SFTP 列出目录 -func (a *App) SftpListDir(connID string, dirPath string) ([]map[string]interface{}, error) { - return a.ensureSftpService().ListDir(connID, dirPath) -} - -// SftpReadFile SFTP 读取文件内容 -func (a *App) SftpReadFile(connID string, filePath string) (string, error) { - return a.ensureSftpService().ReadFile(connID, filePath) -} - // SftpWriteFileRequest SFTP 写入请求 type SftpWriteFileRequest struct { SessionID string `json:"session_id"` @@ -1008,36 +273,6 @@ type SftpWriteFileRequest struct { Content string `json:"content"` } -// SftpWriteFile SFTP 写入文件 -func (a *App) SftpWriteFile(req SftpWriteFileRequest) error { - return a.ensureSftpService().WriteFile(req.SessionID, req.Path, req.Content) -} - -// SftpWriteBase64File SFTP 写入 base64 编码的二进制文件(粘贴图片等) -func (a *App) SftpWriteBase64File(sessionID, filePath, base64Content string) error { - return a.ensureSftpService().WriteBase64File(sessionID, filePath, base64Content) -} - -// SftpGetFileInfo SFTP 获取文件信息 -func (a *App) SftpGetFileInfo(connID string, filePath string) (map[string]interface{}, error) { - return a.ensureSftpService().GetFileInfo(connID, filePath) -} - -// SftpCreateDir SFTP 创建目录 -func (a *App) SftpCreateDir(connID string, dirPath string) (*filesystem.FileOperationResult, error) { - return a.ensureSftpService().CreateDir(connID, dirPath) -} - -// SftpCreateFile SFTP 创建文件 -func (a *App) SftpCreateFile(connID string, filePath string) (*filesystem.FileOperationResult, error) { - return a.ensureSftpService().CreateFile(connID, filePath) -} - -// SftpDeletePath SFTP 删除文件或目录 -func (a *App) SftpDeletePath(connID string, filePath string) (*filesystem.FileOperationResult, error) { - return a.ensureSftpService().DeletePath(connID, filePath) -} - // SftpRenamePathRequest SFTP 重命名请求 type SftpRenamePathRequest struct { SessionID string `json:"session_id"` @@ -1045,47 +280,6 @@ type SftpRenamePathRequest struct { NewPath string `json:"new_path"` } -// SftpRenamePath SFTP 重命名文件或目录 -func (a *App) SftpRenamePath(req SftpRenamePathRequest) (*filesystem.FileOperationResult, error) { - return a.ensureSftpService().RenamePath(req.SessionID, req.OldPath, req.NewPath) -} - -// SftpDownloadToTemp 下载远程文件到本地临时目录(用于预览) -func (a *App) SftpDownloadToTemp(connID string, remotePath string) (string, error) { - return a.ensureSftpService().DownloadToTemp(connID, remotePath) -} - -// SftpDownloadSiteForPreview 下载 HTML 及其网站资源到本地临时目录 -func (a *App) SftpDownloadSiteForPreview(connID string, remotePath string) (string, error) { - return a.ensureSftpService().DownloadSiteForPreview(connID, remotePath) -} - -// SftpDownloadToTempCached 带缓存的 SFTP 下载(命中缓存直接返回本地路径) -func (a *App) SftpDownloadToTempCached(connID string, remotePath string, fileSize int64, modTime string) (string, error) { - return a.ensureSftpService().DownloadToTempCached(connID, remotePath, fileSize, modTime) -} - -// SftpGetCommonPaths 获取 SFTP 远程主机常用路径 -func (a *App) SftpGetCommonPaths(connID string) (map[string]string, error) { - return a.ensureSftpService().GetCommonPaths(connID) -} - -// SftpGetSystemInfo 获取 SFTP 远程主机系统信息(CPU/内存/磁盘) -func (a *App) SftpGetSystemInfo(connID string) (map[string]interface{}, error) { - return a.ensureSftpService().GetSystemInfo(connID) -} - -// ========== OSS 接口 ========== - -func (a *App) ensureOssService() *osssvc.Service { - a.mu.Lock() - defer a.mu.Unlock() - if a.ossService == nil { - a.ossService = osssvc.NewService() - } - return a.ossService -} - type OssConnectRequest struct { Provider string `json:"provider"` AccessKey string `json:"access_key"` @@ -1093,74 +287,6 @@ type OssConnectRequest struct { Endpoint string `json:"endpoint"` } -func (a *App) OssConnect(req OssConnectRequest) (string, error) { - if err := a.ensureOssService().GetManager().Connect(req.Provider, req.AccessKey, req.SecretKey, req.Endpoint); err != nil { - return "", err - } - return req.Provider, nil -} - -// OssDisconnect 断开 OSS 连接 -func (a *App) OssDisconnect(connID string) error { - osssvc.GetManager().Disconnect(connID) - return nil -} - -// OssListDir OSS 列出目录 -func (a *App) OssListDir(connID string, prefix string) ([]map[string]interface{}, error) { - return a.ensureOssService().ListDir(connID, prefix) -} - -// OssDownloadToTemp OSS 下载到临时文件 -func (a *App) OssDownloadToTemp(connID string, key string) (string, error) { - return a.ensureOssService().DownloadToTemp(connID, key) -} - -// OssDownloadSiteForPreview OSS 下载 HTML 及其引用的资源到临时目录 -func (a *App) OssDownloadSiteForPreview(connID string, key string) (string, error) { - return a.ensureOssService().DownloadSiteForPreview(connID, key) -} - -// OssDownloadToTempCached 带缓存的 OSS 下载(命中缓存直接返回本地路径) -func (a *App) OssDownloadToTempCached(connID string, key string, fileSize int64, modTime string) (string, error) { - return a.ensureOssService().DownloadToTempCached(connID, key, fileSize, modTime) -} - -// OssReadFile OSS 读取文件 -func (a *App) OssReadFile(connID string, key string) (string, error) { - return a.ensureOssService().ReadFile(connID, key) -} - -// OssWriteFile OSS 写入文件 -func (a *App) OssWriteFile(connID string, key string, content string) error { - return a.ensureOssService().WriteFile(connID, key, content) -} - -// OssWriteBase64File OSS 写入 base64 编码文件 -func (a *App) OssWriteBase64File(connID string, key string, base64Content string) error { - return a.ensureOssService().WriteBase64File(connID, key, base64Content) -} - -// OssGetFileInfo OSS 获取文件信息 -func (a *App) OssGetFileInfo(connID string, key string) (map[string]interface{}, error) { - return a.ensureOssService().GetFileInfo(connID, key) -} - -// OssCreateDir OSS 创建目录 -func (a *App) OssCreateDir(connID string, dirPath string) (*filesystem.FileOperationResult, error) { - return a.ensureOssService().CreateDir(connID, dirPath) -} - -// OssCreateFile OSS 创建文件 -func (a *App) OssCreateFile(connID string, filePath string) (*filesystem.FileOperationResult, error) { - return a.ensureOssService().CreateFile(connID, filePath) -} - -// OssDeletePath OSS 删除 -func (a *App) OssDeletePath(connID string, key string) (*filesystem.FileOperationResult, error) { - return a.ensureOssService().DeletePath(connID, key) -} - // OssRenamePathRequest OSS 重命名请求 type OssRenamePathRequest struct { ConnID string `json:"conn_id"` @@ -1168,21 +294,6 @@ type OssRenamePathRequest struct { NewPath string `json:"new_path"` } -// OssRenamePath OSS 重命名 -func (a *App) OssRenamePath(req OssRenamePathRequest) (*filesystem.FileOperationResult, error) { - return a.ensureOssService().RenamePath(req.ConnID, req.OldPath, req.NewPath) -} - -// OssGetCommonPaths OSS 获取常用路径 -func (a *App) OssGetCommonPaths(connID string) (map[string]string, error) { - return a.ensureOssService().GetCommonPaths(connID) -} - -// OssGetSignedURL OSS 获取预签名 URL -func (a *App) OssGetSignedURL(connID string, key string) (string, error) { - return a.ensureOssService().GetSignedURL(connID, key) -} - // --- 连接配置 CRUD (SQLite 持久化) --- type SaveProfileRequest struct { @@ -1204,78 +315,6 @@ type SaveProfileRequest struct { LastConnected *int64 `json:"last_connected"` } -func (a *App) ensureProfileSvc() *service.ProfileService { - a.mu.Lock() - defer a.mu.Unlock() - if a.profileSvc == nil { - a.profileSvc = service.NewProfileService() - } - return a.profileSvc -} - -func (a *App) LoadConnectionProfiles() ([]map[string]interface{}, error) { - list, err := a.ensureProfileSvc().ListProfiles() - if err != nil { - return nil, err - } - var result []map[string]interface{} - data, _ := json.Marshal(list) - json.Unmarshal(data, &result) - return result, nil -} - -func (a *App) SaveConnectionProfile(req SaveProfileRequest) (map[string]interface{}, error) { - p := &models.ConnectionProfile{ - Name: req.Name, Host: req.Host, Port: req.Port, - Username: req.Username, Password: req.Password, - KeyPath: req.KeyPath, Type: req.Type, Provider: req.Provider, Token: req.Token, - AccessKey: req.AccessKey, SecretKey: req.SecretKey, - Bucket: req.Bucket, Region: req.Region, Endpoint: req.Endpoint, - } - if req.LastConnected != nil { - t := time.Unix(*req.LastConnected, 0) - p.LastConnected = &t - } - if req.ID != nil { - p.ID = *req.ID - } - if err := a.ensureProfileSvc().SaveProfile(p); err != nil { - return nil, err - } - return map[string]interface{}{"id": float64(p.ID), "success": true}, nil -} - -func (a *App) DeleteConnectionProfile(id uint) error { - return a.ensureProfileSvc().DeleteProfile(id) -} - -func (a *App) GetLocalSystemInfo() (map[string]interface{}, error) { - info := make(map[string]interface{}) - - cpuInfo, err := system.GetCPUInfo() - if err == nil && cpuInfo != nil { - if v, ok := cpuInfo["usage"].(string); ok { - info["cpu_usage"] = v - } - } - - memInfo, err := system.GetMemoryInfo() - if err == nil && memInfo != nil { - if v, ok := memInfo["usage"].(string); ok { - info["mem_usage"] = v - } - } - - diskInfos, err := system.GetDiskInfo() - if err == nil && len(diskInfos) > 0 { - if v, ok := diskInfos[0]["usage"].(string); ok { - info["disk_usage"] = v - } - } - - return info, nil -} - // ========== BGM 播放列表持久化 ========== // BgmPlaylistItem 播放列表条目 @@ -1284,40 +323,3 @@ type BgmPlaylistItem struct { Path string `json:"path"` ProfileID string `json:"profile_id"` } - -// BgmGetPlaylist 获取播放列表 -func (a *App) BgmGetPlaylist() ([]BgmPlaylistItem, error) { - db := storage.GetDB() - if db == nil { - return nil, fmt.Errorf("数据库未初始化") - } - var rows []models.BgmPlaylist - db.Order("sort ASC").Find(&rows) - items := make([]BgmPlaylistItem, len(rows)) - for i, r := range rows { - items[i] = BgmPlaylistItem{Name: r.Name, Path: r.Path, ProfileID: r.ProfileID} - } - return items, nil -} - -// BgmSavePlaylist 全量保存播放列表(前端调用时传完整列表) -func (a *App) BgmSavePlaylist(items []BgmPlaylistItem) error { - db := storage.GetDB() - if db == nil { - return fmt.Errorf("数据库未初始化") - } - return db.Transaction(func(tx *gorm.DB) error { - if err := tx.Exec("DELETE FROM bgm_playlist").Error; err != nil { - return err - } - sort := uint(0) - for _, item := range items { - if item.Name == "" || item.Path == "" { - continue - } - tx.Create(&models.BgmPlaylist{Name: item.Name, Path: item.Path, ProfileID: item.ProfileID, Sort: sort}) - sort++ - } - return nil - }) -} diff --git a/app_config.go b/app_config.go new file mode 100644 index 0000000..c4b1178 --- /dev/null +++ b/app_config.go @@ -0,0 +1,215 @@ +package main + +import ( + "encoding/json" + "fmt" + "log/slog" + "time" + + "u-desk/internal/api" + "u-desk/internal/common" + "u-desk/internal/service" + "u-desk/internal/storage" + "u-desk/internal/storage/models" + + "gorm.io/gorm" +) + +// ========== 应用配置接口 ========== + +// GetAppConfig 获取应用配置 +func (a *App) GetAppConfig() (map[string]interface{}, error) { + if a.configAPI == nil { + return nil, fmt.Errorf("配置服务正在初始化中") + } + return a.configAPI.GetAppConfig() +} + +// SaveAppConfig 保存应用配置 +func (a *App) SaveAppConfig(req SaveAppConfigRequest) (map[string]interface{}, error) { + if a.configAPI == nil { + return nil, fmt.Errorf("配置服务正在初始化中") + } + + oldConfig, _ := a.configAPI.GetAppConfig() + var oldVisibleTabs []string + if success, ok := oldConfig["success"].(bool); ok && success { + if data, ok := oldConfig["data"].(map[string]interface{}); ok { + if vtInterface, ok := data["visibleTabs"].([]interface{}); ok { + oldVisibleTabs = common.InterfaceSliceToStringSlice(vtInterface) + } + } + } + + apiReq := api.SaveAppConfigRequest{ + Tabs: req.Tabs, + VisibleTabs: req.VisibleTabs, + DefaultTab: req.DefaultTab, + } + + result, err := a.configAPI.SaveAppConfig(apiReq) + if err != nil { + return result, err + } + + if success, ok := result["success"].(bool); ok && success { + a.handleNewlyEnabledModules(oldVisibleTabs, req.VisibleTabs) + } + + return result, nil +} + +func (a *App) LoadConnectionProfiles() ([]map[string]interface{}, error) { + list, err := a.ensureProfileSvc().ListProfiles() + if err != nil { + return nil, err + } + var result []map[string]interface{} + data, _ := json.Marshal(list) + if err := json.Unmarshal(data, &result); err != nil { + return nil, fmt.Errorf("解析连接配置失败: %w", err) + } + return result, nil +} + +func (a *App) SaveConnectionProfile(req SaveProfileRequest) (map[string]interface{}, error) { + p := &models.ConnectionProfile{ + Name: req.Name, Host: req.Host, Port: req.Port, + Username: req.Username, Password: req.Password, + KeyPath: req.KeyPath, Type: req.Type, Provider: req.Provider, Token: req.Token, + AccessKey: req.AccessKey, SecretKey: req.SecretKey, + Bucket: req.Bucket, Region: req.Region, Endpoint: req.Endpoint, + } + if req.LastConnected != nil { + t := time.Unix(*req.LastConnected, 0) + p.LastConnected = &t + } + if req.ID != nil { + p.ID = *req.ID + } + if err := a.ensureProfileSvc().SaveProfile(p); err != nil { + return nil, err + } + return map[string]interface{}{"id": float64(p.ID), "success": true}, nil +} + +func (a *App) DeleteConnectionProfile(id uint) error { + return a.ensureProfileSvc().DeleteProfile(id) +} + +// BgmGetPlaylist 获取播放列表 +func (a *App) BgmGetPlaylist() ([]BgmPlaylistItem, error) { + db := storage.GetDB() + if db == nil { + return nil, fmt.Errorf("数据库未初始化") + } + var rows []models.BgmPlaylist + if err := db.Order("sort ASC").Find(&rows).Error; err != nil { + return nil, fmt.Errorf("查询播放列表失败: %w", err) + } + items := make([]BgmPlaylistItem, len(rows)) + for i, r := range rows { + items[i] = BgmPlaylistItem{Name: r.Name, Path: r.Path, ProfileID: r.ProfileID} + } + return items, nil +} + +// BgmSavePlaylist 全量保存播放列表(前端调用时传完整列表) +func (a *App) BgmSavePlaylist(items []BgmPlaylistItem) error { + db := storage.GetDB() + if db == nil { + return fmt.Errorf("数据库未初始化") + } + return db.Transaction(func(tx *gorm.DB) error { + if err := tx.Exec("DELETE FROM bgm_playlist").Error; err != nil { + return err + } + sort := uint(0) + for _, item := range items { + if item.Name == "" || item.Path == "" { + continue + } + if err := tx.Create(&models.BgmPlaylist{Name: item.Name, Path: item.Path, ProfileID: item.ProfileID, Sort: sort}).Error; err != nil { + return err + } + sort++ + } + return nil + }) +} + +func (a *App) ensureProfileSvc() *service.ProfileService { + a.mu.Lock() + defer a.mu.Unlock() + if a.profileSvc == nil { + a.profileSvc = service.NewProfileService() + } + return a.profileSvc +} + +// getVisibleTabs 获取配置中的可见 Tabs +func (a *App) getVisibleTabs() []string { + config, err := a.configAPI.GetAppConfig() + if err != nil { + slog.Warn("读取配置失败,使用默认配置", "error", err) + return common.DefaultVisibleTabs + } + + success, ok := config["success"].(bool) + if !ok || !success { + slog.Warn("配置读取失败,使用默认配置") + return common.DefaultVisibleTabs + } + + data, ok := config["data"].(map[string]interface{}) + if !ok { + return common.DefaultVisibleTabs + } + + visibleTabsInterface, ok := data["visibleTabs"].([]interface{}) + if !ok { + return common.DefaultVisibleTabs + } + + visibleTabs := common.InterfaceSliceToStringSlice(visibleTabsInterface) + + if len(visibleTabs) == 0 { + return common.DefaultVisibleTabs + } + + return visibleTabs +} + +// handleNewlyEnabledModules 处理新启用的模块 +func (a *App) handleNewlyEnabledModules(oldTabs, newTabs []string) { + newlyEnabled := common.Difference(newTabs, oldTabs) + + if len(newlyEnabled) == 0 { + return + } + + slog.Info("检测到新启用的模块", "modules", newlyEnabled) + + for _, tab := range newlyEnabled { + switch tab { + case common.TabFileSystem: + a.initFilesystemModule() + case common.TabDevice: + slog.Info("设备测试模块已启用") + } + } +} + +// initModulesByConfig 根据配置初始化模块 +func (a *App) initModulesByConfig(visibleTabs []string) error { + if common.Contains(visibleTabs, common.TabFileSystem) { + a.initFilesystemModule() + if a.filesystem == nil { + return fmt.Errorf("文件系统服务初始化失败") + } + } else { + slog.Info("跳过文件系统模块(未启用)") + } + + return nil +} diff --git a/app_filesystem.go b/app_filesystem.go new file mode 100644 index 0000000..0d96f5a --- /dev/null +++ b/app_filesystem.go @@ -0,0 +1,174 @@ +package main + +import ( + "fmt" + "log/slog" + + "u-desk/internal/filesystem" +) + +// ReadFile 读取文件 +func (a *App) ReadFile(path string) (string, error) { + return a.filesystem.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 { + return a.filesystem.WriteFile(req.Path, req.Content) +} + +// SaveBase64File 将 base64 内容解码后写入文件(用于图片等二进制数据) +func (a *App) SaveBase64File(req SaveBase64FileRequest) error { + return a.filesystem.SaveBase64File(req.Path, req.Content) +} + +// ListDir 列出目录 +func (a *App) ListDir(path string) ([]map[string]interface{}, error) { + return a.filesystem.ListDir(path) +} + +// CreateDir 创建目录 +func (a *App) CreateDir(path string) (*filesystem.FileOperationResult, error) { + return a.filesystem.CreateDir(path) +} + +// CreateFile 创建文件 +func (a *App) CreateFile(path string) (*filesystem.FileOperationResult, error) { + return a.filesystem.CreateFile(path) +} + +// DeletePath 删除文件或目录 +func (a *App) DeletePath(path string) (*filesystem.FileOperationResult, error) { + return a.filesystem.DeletePath(path) +} + +// RenamePath 重命名文件或目录 +func (a *App) RenamePath(req RenamePathRequest) (*filesystem.FileOperationResult, error) { + return a.filesystem.RenamePath(req.OldPath, req.NewPath) +} + +// GetFileInfo 获取文件信息 +func (a *App) GetFileInfo(path string) (map[string]interface{}, error) { + return a.filesystem.GetFileInfo(path) +} + +// OpenPath 使用系统默认程序打开文件或目录 +func (a *App) OpenPath(path string) error { + return a.filesystem.OpenPath(path) +} + +// ========== Zip 文件操作接口 ========== + +// ListZipContents 列出 zip 文件内容 +func (a *App) ListZipContents(zipPath string) ([]map[string]interface{}, error) { + return a.filesystem.ListZipContents(zipPath) +} + +// ExtractFileFromZip 从 zip 文件中提取单个文件内容 +func (a *App) ExtractFileFromZip(zipPath, filePath string) (string, error) { + return a.filesystem.ExtractFileFromZip(zipPath, filePath) +} + +// ExtractFileFromZipToTemp 从 zip 文件中提取单个文件到临时目录 +func (a *App) ExtractFileFromZipToTemp(zipPath, filePath string) (string, error) { + return a.filesystem.ExtractFileFromZipToTemp(zipPath, filePath) +} + +// GetZipFileInfo 获取 zip 文件中特定文件的信息 +func (a *App) GetZipFileInfo(zipPath, filePath string) (map[string]interface{}, error) { + return a.filesystem.GetZipFileInfo(zipPath, filePath) +} + +// ResolveShortcut 解析快捷方式文件,返回目标路径信息 +func (a *App) ResolveShortcut(lnkPath string) (map[string]interface{}, error) { + targetPath, err := a.filesystem.ResolveShortcut(lnkPath) + if err != nil { + return map[string]interface{}{ + "success": false, + "message": err.Error(), + }, err + } + + fileInfo, err := a.filesystem.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 + } + + go a.startFileServer() + + slog.Info("文件系统模块初始化完成") +} + +// ========== 审计日志接口 ========== + +// GetAuditLogs 获取审计日志 +func (a *App) GetAuditLogs(limit int) ([]map[string]interface{}, error) { + return a.filesystem.GetAuditLogs(limit) +} diff --git a/app_oss.go b/app_oss.go new file mode 100644 index 0000000..6720369 --- /dev/null +++ b/app_oss.go @@ -0,0 +1,101 @@ +package main + +import ( + "u-desk/internal/filesystem" + osssvc "u-desk/internal/ossdrv" +) + +// ========== OSS 接口 ========== + +func (a *App) ensureOssService() *osssvc.Service { + a.mu.Lock() + defer a.mu.Unlock() + if a.ossService == nil { + a.ossService = osssvc.NewService() + } + return a.ossService +} + +// OssConnect 连接 OSS 存储 +func (a *App) OssConnect(req OssConnectRequest) (string, error) { + if err := a.ensureOssService().GetManager().Connect(req.Provider, req.AccessKey, req.SecretKey, req.Endpoint); err != nil { + return "", err + } + return req.Provider, nil +} + +// OssDisconnect 断开 OSS 连接 +func (a *App) OssDisconnect(connID string) error { + osssvc.GetManager().Disconnect(connID) + return nil +} + +// OssListDir OSS 列出目录 +func (a *App) OssListDir(connID string, prefix string) ([]map[string]interface{}, error) { + return a.ensureOssService().ListDir(connID, prefix) +} + +// OssDownloadToTemp OSS 下载到临时文件 +func (a *App) OssDownloadToTemp(connID string, key string) (string, error) { + return a.ensureOssService().DownloadToTemp(connID, key) +} + +// OssDownloadSiteForPreview OSS 下载 HTML 及其引用的资源到临时目录 +func (a *App) OssDownloadSiteForPreview(connID string, key string) (string, error) { + return a.ensureOssService().DownloadSiteForPreview(connID, key) +} + +// OssDownloadToTempCached 带缓存的 OSS 下载(命中缓存直接返回本地路径) +func (a *App) OssDownloadToTempCached(connID string, key string, fileSize int64, modTime string) (string, error) { + return a.ensureOssService().DownloadToTempCached(connID, key, fileSize, modTime) +} + +// OssReadFile OSS 读取文件 +func (a *App) OssReadFile(connID string, key string) (string, error) { + return a.ensureOssService().ReadFile(connID, key) +} + +// OssWriteFile OSS 写入文件 +func (a *App) OssWriteFile(connID string, key string, content string) error { + return a.ensureOssService().WriteFile(connID, key, content) +} + +// OssWriteBase64File OSS 写入 base64 编码文件 +func (a *App) OssWriteBase64File(connID string, key string, base64Content string) error { + return a.ensureOssService().WriteBase64File(connID, key, base64Content) +} + +// OssGetFileInfo OSS 获取文件信息 +func (a *App) OssGetFileInfo(connID string, key string) (map[string]interface{}, error) { + return a.ensureOssService().GetFileInfo(connID, key) +} + +// OssCreateDir OSS 创建目录 +func (a *App) OssCreateDir(connID string, dirPath string) (*filesystem.FileOperationResult, error) { + return a.ensureOssService().CreateDir(connID, dirPath) +} + +// OssCreateFile OSS 创建文件 +func (a *App) OssCreateFile(connID string, filePath string) (*filesystem.FileOperationResult, error) { + return a.ensureOssService().CreateFile(connID, filePath) +} + +// OssDeletePath OSS 删除 +func (a *App) OssDeletePath(connID string, key string) (*filesystem.FileOperationResult, error) { + return a.ensureOssService().DeletePath(connID, key) +} + +// OssRenamePath OSS 重命名 +func (a *App) OssRenamePath(req OssRenamePathRequest) (*filesystem.FileOperationResult, error) { + return a.ensureOssService().RenamePath(req.ConnID, req.OldPath, req.NewPath) +} + +// OssGetCommonPaths OSS 获取常用路径 +func (a *App) OssGetCommonPaths(connID string) (map[string]string, error) { + return a.ensureOssService().GetCommonPaths(connID) +} + +// OssGetSignedURL OSS 获取预签名 URL +func (a *App) OssGetSignedURL(connID string, key string) (string, error) { + return a.ensureOssService().GetSignedURL(connID, key) +} diff --git a/app_pdf.go b/app_pdf.go new file mode 100644 index 0000000..22368bd --- /dev/null +++ b/app_pdf.go @@ -0,0 +1,50 @@ +package main + +import ( + "fmt" + + "u-desk/internal/api" +) + +// ExportPDF 导出PDF文件 +func (a *App) ExportPDF(content string, title string, fileName string, fontSize int, pageWidth int, pageHeight int) (map[string]interface{}, error) { + if a.pdfAPI == nil { + return map[string]interface{}{ + "success": false, + "message": "PDF导出功能未初始化", + }, fmt.Errorf("PDF导出功能未初始化") + } + + req := api.PdfExportRequest{ + Content: content, + Title: title, + FileName: fileName, + FontSize: fontSize, + PageWidth: pageWidth, + PageHeight: pageHeight, + } + + result, err := a.pdfAPI.ExportMarkdownToPDF(req) + if err != nil { + return map[string]interface{}{ + "success": false, + "message": err.Error(), + }, err + } + + return map[string]interface{}{ + "success": result.Success, + "message": result.Message, + "path": result.Path, + "size": result.Size, + }, nil +} + +// SelectPDFSaveDirectory 选择PDF保存目录 +func (a *App) SelectPDFSaveDirectory() (string, error) { + if a.pdfAPI == nil { + return "", fmt.Errorf("PDF导出功能未初始化") + } + + return a.pdfAPI.SelectDirectory() +} diff --git a/app_recycle.go b/app_recycle.go new file mode 100644 index 0000000..9096444 --- /dev/null +++ b/app_recycle.go @@ -0,0 +1,23 @@ +package main + +// ========== 回收站接口 ========== + +// GetRecycleBinEntries 获取回收站条目 +func (a *App) GetRecycleBinEntries() ([]map[string]interface{}, error) { + return a.filesystem.GetRecycleBinEntries() +} + +// RestoreFromRecycleBin 从回收站恢复文件 +func (a *App) RestoreFromRecycleBin(recyclePath string) error { + return a.filesystem.RestoreFromRecycleBin(recyclePath) +} + +// DeletePermanently 永久删除回收站中的文件 +func (a *App) DeletePermanently(recyclePath string) error { + return a.filesystem.DeletePermanently(recyclePath) +} + +// EmptyRecycleBin 清空回收站 +func (a *App) EmptyRecycleBin() error { + return a.filesystem.EmptyRecycleBin() +} diff --git a/app_sftp.go b/app_sftp.go new file mode 100644 index 0000000..d323710 --- /dev/null +++ b/app_sftp.go @@ -0,0 +1,135 @@ +package main + +import ( + "fmt" + "strconv" + "strings" + "time" + + "u-desk/internal/filesystem" + "u-desk/internal/sftp" +) + +// ========== SFTP 接口 ========== + +func (a *App) ensureSftpService() *sftp.Service { + a.mu.Lock() + defer a.mu.Unlock() + if a.sftpService == nil { + a.sftpService = sftp.NewService() + } + return a.sftpService +} + +// SftpConnect 建立 SFTP 连接,返回连接标识符 connID +func (a *App) SftpConnect(req SftpConnectRequest) (string, error) { + config := &sftp.Config{ + Host: req.Host, + Port: req.Port, + Username: req.Username, + Password: req.Password, + KeyPath: req.KeyPath, + KeyPassphrase: req.KeyPassphrase, + } + if config.Port == 0 { + config.Port = 22 + } + if config.Timeout == 0 { + config.Timeout = 15 * time.Second + } + + svc := a.ensureSftpService() + _, err := svc.GetManager().Connect(config) + if err != nil { + return "", sftp.ToUserMessage(err) + } + + connID := sftp.ConnID(config.Host, config.Port) + return connID, nil +} + +// SftpDisconnect 断开 SFTP 连接 +func (a *App) SftpDisconnect(connID string) error { + parts := strings.SplitN(connID, ":", 2) + if len(parts) < 2 { + return fmt.Errorf("无效的连接标识符") + } + host := parts[0] + port, err := strconv.Atoi(parts[1]) + if err != nil { + return fmt.Errorf("无效的端口号") + } + + sftp.GetManager().Disconnect(host, port) + return nil +} + +// SftpListDir SFTP 列出目录 +func (a *App) SftpListDir(connID string, dirPath string) ([]map[string]interface{}, error) { + return a.ensureSftpService().ListDir(connID, dirPath) +} + +// SftpReadFile SFTP 读取文件内容 +func (a *App) SftpReadFile(connID string, filePath string) (string, error) { + return a.ensureSftpService().ReadFile(connID, filePath) +} + +// SftpWriteFile SFTP 写入文件 +func (a *App) SftpWriteFile(req SftpWriteFileRequest) error { + return a.ensureSftpService().WriteFile(req.SessionID, req.Path, req.Content) +} + +// SftpWriteBase64File SFTP 写入 base64 编码的二进制文件(粘贴图片等) +func (a *App) SftpWriteBase64File(sessionID, filePath, base64Content string) error { + return a.ensureSftpService().WriteBase64File(sessionID, filePath, base64Content) +} + +// SftpGetFileInfo SFTP 获取文件信息 +func (a *App) SftpGetFileInfo(connID string, filePath string) (map[string]interface{}, error) { + return a.ensureSftpService().GetFileInfo(connID, filePath) +} + +// SftpCreateDir SFTP 创建目录 +func (a *App) SftpCreateDir(connID string, dirPath string) (*filesystem.FileOperationResult, error) { + return a.ensureSftpService().CreateDir(connID, dirPath) +} + +// SftpCreateFile SFTP 创建文件 +func (a *App) SftpCreateFile(connID string, filePath string) (*filesystem.FileOperationResult, error) { + return a.ensureSftpService().CreateFile(connID, filePath) +} + +// SftpDeletePath SFTP 删除文件或目录 +func (a *App) SftpDeletePath(connID string, filePath string) (*filesystem.FileOperationResult, error) { + return a.ensureSftpService().DeletePath(connID, filePath) +} + +// SftpRenamePath SFTP 重命名文件或目录 +func (a *App) SftpRenamePath(req SftpRenamePathRequest) (*filesystem.FileOperationResult, error) { + return a.ensureSftpService().RenamePath(req.SessionID, req.OldPath, req.NewPath) +} + +// SftpDownloadToTemp 下载远程文件到本地临时目录(用于预览) +func (a *App) SftpDownloadToTemp(connID string, remotePath string) (string, error) { + return a.ensureSftpService().DownloadToTemp(connID, remotePath) +} + +// SftpDownloadSiteForPreview 下载 HTML 及其网站资源到本地临时目录 +func (a *App) SftpDownloadSiteForPreview(connID string, remotePath string) (string, error) { + return a.ensureSftpService().DownloadSiteForPreview(connID, remotePath) +} + +// SftpDownloadToTempCached 带缓存的 SFTP 下载(命中缓存直接返回本地路径) +func (a *App) SftpDownloadToTempCached(connID string, remotePath string, fileSize int64, modTime string) (string, error) { + return a.ensureSftpService().DownloadToTempCached(connID, remotePath, fileSize, modTime) +} + +// SftpGetCommonPaths 获取 SFTP 远程主机常用路径 +func (a *App) SftpGetCommonPaths(connID string) (map[string]string, error) { + return a.ensureSftpService().GetCommonPaths(connID) +} + +// SftpGetSystemInfo 获取 SFTP 远程主机系统信息(CPU/内存/磁盘) +func (a *App) SftpGetSystemInfo(connID string) (map[string]interface{}, error) { + return a.ensureSftpService().GetSystemInfo(connID) +} diff --git a/app_system.go b/app_system.go new file mode 100644 index 0000000..799a2c0 --- /dev/null +++ b/app_system.go @@ -0,0 +1,84 @@ +package main + +import ( + "os" + "strings" + + "u-desk/internal/system" +) + +// GetSystemInfo 获取系统信息 +func (a *App) GetSystemInfo() (map[string]interface{}, error) { + return system.GetSystemInfo() +} + +// GetCPUInfo 获取 CPU 信息 +func (a *App) GetCPUInfo() (map[string]interface{}, error) { + return system.GetCPUInfo() +} + +// GetMemoryInfo 获取内存信息 +func (a *App) GetMemoryInfo() (map[string]interface{}, error) { + return system.GetMemoryInfo() +} + +// GetDiskInfo 获取磁盘信息 +func (a *App) GetDiskInfo() ([]map[string]interface{}, error) { + return system.GetDiskInfo() +} + +func (a *App) GetLocalSystemInfo() (map[string]interface{}, error) { + info := make(map[string]interface{}) + + cpuInfo, err := system.GetCPUInfo() + if err == nil && cpuInfo != nil { + if v, ok := cpuInfo["usage"].(string); ok { + info["cpu_usage"] = v + } + } + + memInfo, err := system.GetMemoryInfo() + if err == nil && memInfo != nil { + if v, ok := memInfo["usage"].(string); ok { + info["mem_usage"] = v + } + } + + diskInfos, err := system.GetDiskInfo() + if err == nil && len(diskInfos) > 0 { + if v, ok := diskInfos[0]["usage"].(string); ok { + info["disk_usage"] = v + } + } + + return info, nil +} + +// GetCommonPaths 获取常用系统路径 +func (a *App) GetCommonPaths() (map[string]string, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, err + } + + paths := map[string]string{ + "home": homeDir, + } + + for k, v := range resolveFolderPaths(homeDir) { + paths[k] = v + } + + return paths, nil +} + +// GetEnvVars 获取环境变量 +func (a *App) GetEnvVars() (map[string]string, error) { + envVars := make(map[string]string) + for _, env := range os.Environ() { + if key, value, found := strings.Cut(env, "="); found { + envVars[key] = value + } + } + return envVars, nil +} diff --git a/app_update.go b/app_update.go new file mode 100644 index 0000000..76e46ff --- /dev/null +++ b/app_update.go @@ -0,0 +1,172 @@ +package main + +import ( + "fmt" + "log/slog" + "time" + + "u-desk/internal/api" +) + +// ========== 版本更新管理接口 ========== + +// requireUpdateAPI 检查 updateAPI 是否已初始化 +func (a *App) requireUpdateAPI() (*api.UpdateAPI, error) { + a.mu.Lock() + defer a.mu.Unlock() + if a.updateAPI == nil { + return nil, fmt.Errorf("更新功能正在初始化中") + } + return a.updateAPI, nil +} + +// CheckUpdate 检查更新 +func (a *App) CheckUpdate() (map[string]interface{}, error) { + api, err := a.requireUpdateAPI() + if err != nil { + return nil, err + } + return api.CheckUpdate() +} + +// GetCurrentVersion 获取当前版本号 +func (a *App) GetCurrentVersion() (map[string]interface{}, error) { + api, err := a.requireUpdateAPI() + if err != nil { + return nil, err + } + return api.GetCurrentVersion() +} + +// GetUpdateConfig 获取更新配置 +func (a *App) GetUpdateConfig() (map[string]interface{}, error) { + api, err := a.requireUpdateAPI() + if err != nil { + return nil, err + } + return api.GetUpdateConfig() +} + +// SetUpdateConfig 设置更新配置 +func (a *App) SetUpdateConfig(autoCheckEnabled bool, checkIntervalMinutes int, checkURL string) (map[string]interface{}, error) { + api, err := a.requireUpdateAPI() + if err != nil { + return nil, err + } + return api.SetUpdateConfig(autoCheckEnabled, checkIntervalMinutes, checkURL) +} + +// DownloadUpdate 下载更新包 +func (a *App) DownloadUpdate(downloadURL string) (map[string]interface{}, error) { + api, err := a.requireUpdateAPI() + if err != nil { + return nil, err + } + return api.DownloadUpdate(downloadURL) +} + +// InstallUpdate 安装更新包 +func (a *App) InstallUpdate(installerPath string, autoRestart bool) (map[string]interface{}, error) { + api, err := a.requireUpdateAPI() + if err != nil { + return nil, err + } + return api.InstallUpdate(installerPath, autoRestart) +} + +// InstallUpdateWithHash 安装更新包(带哈希验证) +func (a *App) InstallUpdateWithHash(installerPath string, autoRestart bool, expectedHash string, hashType string) (map[string]interface{}, error) { + api, err := a.requireUpdateAPI() + if err != nil { + return nil, err + } + return api.InstallUpdateWithHash(installerPath, autoRestart, expectedHash, hashType) +} + +// VerifyUpdateFile 验证更新文件哈希值 +func (a *App) VerifyUpdateFile(filePath string, expectedHash string, hashType string) (map[string]interface{}, error) { + api, err := a.requireUpdateAPI() + if err != nil { + return nil, err + } + return api.VerifyUpdateFile(filePath, expectedHash, hashType) +} + +// startAutoUpdateCheck 启动自动更新检查 +func (a *App) startAutoUpdateCheck() { + updateAPI, err := a.requireUpdateAPI() + if err != nil { + return + } + + config, err := updateAPI.GetUpdateConfig() + if err != nil { + slog.Warn("获取更新配置失败", "error", err) + return + } + success, ok := config["success"].(bool) + if !ok || !success { + return + } + + configData, ok := config["data"].(map[string]interface{}) + if !ok { + return + } + + autoCheckEnabled, ok := configData["auto_check_enabled"].(bool) + if !ok || !autoCheckEnabled { + return + } + + interval, ok := configData["check_interval_minutes"].(int) + if !ok || interval <= 0 { + interval = 5 + } + + go a.checkUpdate() + + a.updateTicker = time.NewTicker(time.Duration(interval) * time.Minute) + go func() { + for range a.updateTicker.C { + a.checkUpdate() + } + }() +} + +// checkUpdate 执行更新检查 +func (a *App) checkUpdate() { + defer func() { + if r := recover(); r != nil { + slog.Error("自动检查更新发生错误", "error", r) + } + }() + + a.mu.Lock() + api := a.updateAPI + a.mu.Unlock() + + if api == nil { + return + } + + result, err := api.CheckUpdate() + if err != nil { + return + } + + success, ok := result["success"].(bool) + if !ok || !success { + return + } + + data, ok := result["data"].(map[string]interface{}) + if !ok { + return + } + + hasUpdate, ok := data["has_update"].(bool) + if ok && hasUpdate && a.mainWindow != nil { + a.mainWindow.EmitEvent("update-available", data) + } +} diff --git a/app_window.go b/app_window.go new file mode 100644 index 0000000..9caa628 --- /dev/null +++ b/app_window.go @@ -0,0 +1,77 @@ +package main + +import ( + "github.com/wailsapp/wails/v3/pkg/application" +) + +// ========== 窗口控制方法 ========== + +// WindowMinimize 最小化窗口 +func (a *App) WindowMinimize() { + if a.mainWindow != nil { + a.mainWindow.Minimise() + } +} + +// WindowMaximize 最大化/还原窗口 +func (a *App) WindowMaximize() { + if a.mainWindow == nil { + return + } + if a.mainWindow.IsMaximised() { + a.mainWindow.UnMaximise() + } else { + a.mainWindow.Maximise() + } +} + +// WindowClose 关闭窗口 +func (a *App) WindowClose() { + application.Get().Quit() +} + +// WindowIsMaximized 检查窗口是否最大化 +func (a *App) WindowIsMaximized() bool { + if a.mainWindow != nil { + return a.mainWindow.IsMaximised() + } + return false +} + +// WindowToggleAlwaysOnTop 切换窗口置顶 +func (a *App) WindowToggleAlwaysOnTop() bool { + if a.mainWindow == nil { + return false + } + a.mu.Lock() + a.isAlwaysOnTop = !a.isAlwaysOnTop + a.mu.Unlock() + a.mainWindow.SetAlwaysOnTop(a.isAlwaysOnTop) + return a.isAlwaysOnTop +} + +// SetWindowTitleBarColor 设置原生标题栏颜色 + 主题模式(0x00BBGGRR 格式) +func (a *App) SetWindowTitleBarColor(color uint32, isDark bool) { + if a.mainWindow == nil { + return + } + hwnd := uintptr(a.mainWindow.NativeWindow()) + if hwnd == 0 { + return + } + setWindowTitleBar(hwnd, color, isDark) +} + +// Reload 重新加载窗口(用于菜单项) +func (a *App) Reload() { + if a.mainWindow != nil { + a.mainWindow.Reload() + } +} + +// ClearCache 清理本地缓存(用于菜单项) +func (a *App) ClearCache() { + if a.mainWindow != nil { + a.mainWindow.EmitEvent("clear-cache") + } +} diff --git a/app_windows.go b/app_windows.go new file mode 100644 index 0000000..c9572d9 --- /dev/null +++ b/app_windows.go @@ -0,0 +1,75 @@ +//go:build windows + +package main + +import ( + "os" + "path/filepath" + "strings" + + "golang.org/x/sys/windows/registry" + + "github.com/wailsapp/wails/v3/pkg/w32" +) + +// getWindowsSpecialFolder 从注册表读取 Windows 特殊文件夹的真实路径 +func getWindowsSpecialFolder(guid string) string { + key, err := registry.OpenKey(registry.CURRENT_USER, + `Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders`, + registry.READ) + if err != nil { + return "" + } + defer key.Close() + + val, _, err := key.GetStringValue(guid) + if err != nil || val == "" { + return "" + } + + path := os.ExpandEnv(val) + if _, err := os.Stat(path); err != nil { + return "" + } + return path +} + +// getWindowsDrivePaths 返回所有可用盘符路径 +func getWindowsDrivePaths() map[string]string { + paths := make(map[string]string) + for _, drive := range "ABCDEFGHIJKLMNOPQRSTUVWXYZ" { + path := string(drive) + ":\\" + if _, err := os.Stat(path); err == nil { + key := string(drive) + paths[key] = path + } + } + return paths +} + +// setWindowTitleBar 设置原生标题栏颜色 + 主题模式 +func setWindowTitleBar(hwnd uintptr, color uint32, isDark bool) { + w32.SetTheme(hwnd, isDark) + w32.SetTitleBarColour(hwnd, color) +} + +// resolveFolderPaths 解析特殊文件夹路径 +func resolveFolderPaths(homeDir string) map[string]string { + paths := make(map[string]string) + folderGUIDs := map[string]string{ + "desktop": "{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}", + "documents": "{D20B4C7F-5EA7-424C-B25E-039F6F1FCC8A}", + "downloads": "{374DE290-123F-4565-9164-39C4925E467B}", + } + for name, guid := range folderGUIDs { + if p := getWindowsSpecialFolder(guid); p != "" { + paths[name] = p + } else { + paths[name] = filepath.Join(homeDir, strings.ToUpper(name[:1])+name[1:]) + } + } + for k, v := range getWindowsDrivePaths() { + paths["root_"+k] = v + } + return paths +} diff --git a/docs/代码改进清单.md b/docs/代码改进清单.md index 12406cb..e726017 100644 --- a/docs/代码改进清单.md +++ b/docs/代码改进清单.md @@ -15,9 +15,43 @@ ## 待处理 -- [ ] ④ App 结构体拆分 — 1344 行,应拆为 FilesystemService / ProfileService / BgmService / UpdateService 等 v3 Service +- [x] ④ App 结构体拆分 — 1281行 app.go 拆为 9 个域文件:app_update/filesystem/sftp/oss/window/system/config/recycle/pdf + app_windows.go - [ ] ⑦ `internal/api/pdf_api.go:371` SelectDirectory 改用 Wails 原生对话框 `application.Get().Dialog.OpenFile()` - [ ] ⑧ `app.go:176-189` HWND 轮询改事件驱动 — v3 alpha 暂无对应 API,后续跟进 -- [ ] ⑩ `app.go:29` Windows 专用导入 `golang.org/x/sys/windows/registry` 加构建标签拆到 `*_windows.go` -- [ ] ⑪ 全局结构化日志 — `fmt.Println` 替换为 `log/slog`,按优先级分批替换 -- [ ] ⑬ `app.go:158` 更新检查 URL `https://c.1216.top/last-version.json` 移入配置 +- [x] ⑩ `app.go:29` Windows 专用导入拆到 `app_windows.go`(registry/w32 包隔离) +- [x] ⑪ 全局结构化日志 — `fmt.Println/Printf` 全部替换为 `log/slog`(app.go + internal/ 共 45 处) +- [x] ⑬ `app.go` 更新检查 URL 改从 `update_config.json` 动态读取,不再硬编码 + +## 代码审查修复 (2026-05-16) + +> 基于 App 拆分后全量审查,17 项问题已修复 15 项 + +### 已修复 + +- [x] 🔴① app_windows.go 添加 `//go:build windows` 构建标签 +- [x] 🔴② app_windows.go 移除 `fallbackName` 死参数 +- [x] 🔴③ initModulesByConfig 复用 initFilesystemModule,消除重复初始化逻辑 +- [x] 🔴④ WindowToggleAlwaysOnTop 的 isAlwaysOnTop 修改加 mu 锁保护 +- [x] 🟡⑤ GetEnvVars 从 app_filesystem.go 移至 app_system.go(归属修正) +- [x] 🟡⑥ GetAuditLogs 从 app_config.go 移至 app_filesystem.go(归属修正) +- [x] 🟡⑦ BgmGetPlaylist 处理 db.Find 错误 +- [x] 🟡⑧ LoadConnectionProfiles 处理 json.Unmarshal 错误 +- [x] 🟡⑨ startAutoUpdateCheck 改用 requireUpdateAPI 统一加锁 +- [x] 🟡⑩ startAutoUpdateCheck 配置读取失败添加 slog.Warn 日志 +- [x] 🟡⑪ MigrateTabConfig 错误从 `_ =` 改为 slog.Warn +- [x] 🟡⑫ app_recycle.go 移除空 import 块 +- [x] ⚪⑭ OssConnect 补充 godoc 注释 +- [x] ⚪⑮ EmitEvent 闭包提取为 app.emitEvent 方法,消除重复 + +## Bug 修复 (2026-05-16) + +- [x] 🔴 预览状态级联失败: `clearContent()` 未重置 `isBinaryFile`,打开二进制文件后所有图片/视频预览失效 +- [x] 🔴 Tab 切换状态不同步: 跨服务器 Tab 切换时面包屑和文件列表未更新 +- [x] 🟡 AsyncCodeEditor 永久死亡: `defineAsyncComponent` 添加 `onError` 重试(限 2 次) +- [x] 🟡 loadDirectory 静默吞异常: 切换 Tab 加载目录失败时添加用户提示 + +### 暂不处理 + +- [ ] ⚪⑬ fallback 路径在非英文 Windows 上不匹配 — 边缘场景,影响有限 +- [ ] ⚪⑯ SftpDisconnect 未走 ensureSftpService — 功能正确,风格统一待后续 +- [ ] ⚪⑰ ensure 模式 5 种写法统一 — 大规模重构,独立迭代 diff --git a/frontend/bindings/u-desk/app.ts b/frontend/bindings/u-desk/app.ts index edbdcd5..d7e29b6 100644 --- a/frontend/bindings/u-desk/app.ts +++ b/frontend/bindings/u-desk/app.ts @@ -325,6 +325,9 @@ export function OpenPath(path: string): $CancellablePromise { return $Call.ByID(1591734570, path); } +/** + * OssConnect 连接 OSS 存储 + */ export function OssConnect(req: $models.OssConnectRequest): $CancellablePromise { return $Call.ByID(3667022538, req); } diff --git a/frontend/src/components/FileSystem/components/FileEditorPanel.vue b/frontend/src/components/FileSystem/components/FileEditorPanel.vue index 3be6043..869f869 100644 --- a/frontend/src/components/FileSystem/components/FileEditorPanel.vue +++ b/frontend/src/components/FileSystem/components/FileEditorPanel.vue @@ -445,10 +445,15 @@ import { parseLrc, type LrcLine } from '@/utils/lrcParser' import BgmBar, { type BgmTrack } from './FileEditor/BgmBar.vue' // 异步加载 CodeEditor 组件,减少初始包大小 +let editorRetryCount = 0 const AsyncCodeEditor = defineAsyncComponent({ loader: () => import('@/components/CodeEditor.vue'), delay: 200, - timeout: 10000 + timeout: 10000, + onError(error, retry) { + console.error('[AsyncCodeEditor] 加载失败:', error) + if (++editorRetryCount <= 2) retry() + } }) // Office 预览容器引用 diff --git a/frontend/src/components/FileSystem/composables/useFileEdit.ts b/frontend/src/components/FileSystem/composables/useFileEdit.ts index 269543a..c9c80ae 100644 --- a/frontend/src/components/FileSystem/composables/useFileEdit.ts +++ b/frontend/src/components/FileSystem/composables/useFileEdit.ts @@ -499,6 +499,7 @@ ${ext ? `文件类型: ${fileTypeDesc}\n` : ''} const clearContent = () => { fileContent.value = '' originalContent.value = '' + isBinaryFile.value = false } /** diff --git a/frontend/src/components/FileSystem/composables/useFilePreview.ts b/frontend/src/components/FileSystem/composables/useFilePreview.ts index 91d8f03..32ca176 100644 --- a/frontend/src/components/FileSystem/composables/useFilePreview.ts +++ b/frontend/src/components/FileSystem/composables/useFilePreview.ts @@ -22,6 +22,7 @@ const CONTENT_DETECT_MAX_SIZE = 500 * 1024 // 500KB // 缓存检测结果 const contentDetectCache = new Map() const CACHE_TTL = 60000 // 1分钟缓存 +const CACHE_MAX_SIZE = 200 // ====== URL 工具函数(导出供外部使用) ====== @@ -122,6 +123,7 @@ export function useFilePreview() { try { const result = await detectFileTypeByContent(path) const data = { category: result.category, ext: result.extension } + if (contentDetectCache.size >= CACHE_MAX_SIZE) contentDetectCache.clear() contentDetectCache.set(path, { timestamp: Date.now(), result: data }) return data } catch { diff --git a/frontend/src/components/FileSystem/index.vue b/frontend/src/components/FileSystem/index.vue index 23a96e5..5abead7 100644 --- a/frontend/src/components/FileSystem/index.vue +++ b/frontend/src/components/FileSystem/index.vue @@ -1158,6 +1158,14 @@ const activateTab = async (tab: PreviewTab) => { } } selectedFileItem.value = tab.fileItem + + // 更新面包屑和文件列表到文件所在目录 + const dir = getParentPath(tab.fileItem.path) + if (dir && dir !== filePath.value) { + filePath.value = dir + loadDirectory(dir).catch(e => Message.error(`目录加载失败: ${e}`)) + } + if (tab.loaded) { restoreTabState(tab) } else { diff --git a/go.mod b/go.mod index 93d2880..2524150 100644 --- a/go.mod +++ b/go.mod @@ -7,14 +7,14 @@ require ( github.com/chromedp/chromedp v0.15.1 github.com/fsnotify/fsnotify v1.10.1 github.com/glebarez/sqlite v1.11.0 - github.com/labstack/echo/v4 v4.15.1 + github.com/labstack/echo/v4 v4.15.2 github.com/pkg/sftp v1.13.10 github.com/shirou/gopsutil/v3 v3.24.5 - github.com/wailsapp/wails/v3 v3.0.0-alpha.80 - github.com/yuin/goldmark v1.7.16 - golang.org/x/crypto v0.47.0 - golang.org/x/sys v0.42.0 - golang.org/x/text v0.33.0 + github.com/wailsapp/wails/v3 v3.0.0-alpha.92 + github.com/yuin/goldmark v1.8.2 + golang.org/x/crypto v0.51.0 + golang.org/x/sys v0.44.0 + golang.org/x/text v0.37.0 gopkg.in/yaml.v3 v3.0.1 gorm.io/gorm v1.31.1 ) @@ -22,7 +22,7 @@ require ( require ( dario.cat/mergo v1.0.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/ProtonMail/go-crypto v1.3.0 // indirect + github.com/ProtonMail/go-crypto v1.4.1 // indirect github.com/adrg/xdg v0.5.3 // indirect github.com/bep/debounce v1.2.1 // indirect github.com/chromedp/sysutil v1.1.0 // indirect @@ -30,13 +30,13 @@ require ( github.com/coder/websocket v1.8.14 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/ebitengine/purego v0.9.1 // indirect + github.com/ebitengine/purego v0.10.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/glebarez/go-sqlite v1.22.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.7.0 // indirect - github.com/go-git/go-git/v5 v5.16.4 // indirect - github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect + github.com/go-git/go-billy/v5 v5.9.0 // indirect + github.com/go-git/go-git/v5 v5.19.0 // indirect + github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect @@ -48,39 +48,40 @@ require ( github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/kevinburke/ssh_config v1.4.0 // indirect + github.com/kevinburke/ssh_config v1.6.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/kr/fs v0.1.0 // indirect - github.com/labstack/gommon v0.4.2 // indirect + github.com/labstack/gommon v0.5.0 // indirect github.com/leaanthony/go-ansi-parser v1.6.1 // indirect github.com/leaanthony/u v1.1.1 // indirect - github.com/lmittmann/tint v1.1.2 // indirect - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/lmittmann/tint v1.1.3 // indirect + github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-isatty v0.0.22 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect - github.com/pjbgf/sha1cd v0.5.0 // indirect + github.com/pjbgf/sha1cd v0.6.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect - github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/samber/lo v1.52.0 // indirect + github.com/samber/lo v1.53.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect - github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/shoenig/go-m1cpu v0.2.1 // indirect github.com/skeema/knownhosts v1.3.2 // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect + github.com/tklauser/go-sysconf v0.4.0 // indirect + github.com/tklauser/numcpus v0.12.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - github.com/wailsapp/go-webview2 v1.0.23 // indirect + github.com/wailsapp/wails/webview2 v1.0.24 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect - golang.org/x/net v0.49.0 // indirect - golang.org/x/time v0.14.0 // indirect + golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect + golang.org/x/net v0.54.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.45.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - modernc.org/libc v1.67.6 // indirect + modernc.org/libc v1.72.3 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect - modernc.org/sqlite v1.44.3 // indirect + modernc.org/sqlite v1.50.1 // indirect ) diff --git a/go.sum b/go.sum index dc5caf4..7873e20 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,8 @@ dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= -github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= +github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= @@ -30,30 +30,30 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= -github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= -github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= -github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= +github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM= -github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E= +github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA= +github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y= -github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= -github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 h1:vymEbVwYFP/L05h5TKQxvkXoKxNvTpjxYKdF1Nlwuao= -github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= +github.com/go-git/go-git/v5 v5.19.0 h1:+WkVUQZSy/F1Gb13udrMKjIM2PrzsNfDKFSfo5tkMtc= +github.com/go-git/go-git/v5 v5.19.0/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= +github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836 h1:5KGUhXZFTN1PrCY4zUZLe1J8n7uBNmPDbCLCn78EbPQ= +github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= @@ -67,7 +67,6 @@ github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= @@ -84,8 +83,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= -github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= +github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY= +github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= @@ -97,35 +96,35 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo/v4 v4.15.1 h1:S9keusg26gZpjMmPqB5hOEvNKnmd1lNmcHrbbH2lnFs= -github.com/labstack/echo/v4 v4.15.1/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c= -github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= -github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/labstack/echo/v4 v4.15.2 h1:nnh2sCzGCVYnU+wCisMPiYapEg/QVo/gcI9ePKg5/T4= +github.com/labstack/echo/v4 v4.15.2/go.mod h1:Xzp1Ns1RA2c9fY7nSgUJkpkUZGNbEIVHZbtbOMPktBI= +github.com/labstack/gommon v0.5.0 h1:6VSQ2NOzsnEJ5W6+84E0RbcaDDmgB6NIAzWCczTEe6c= +github.com/labstack/gommon v0.5.0/go.mod h1:Rzlg7HHy1maLfzBYGg9NZcVuz1sA68HHhLjhcEllYE0= github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= -github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w= -github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I= +github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= -github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= -github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= +github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU= +github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -134,8 +133,8 @@ github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU= github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -143,16 +142,16 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= -github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= +github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= -github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= -github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= -github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= -github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/shoenig/go-m1cpu v0.2.1 h1:yqRB4fvOge2+FyRXFkXqsyMoqPazv14Yyy+iyccT2E4= +github.com/shoenig/go-m1cpu v0.2.1/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w= +github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk= +github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg= github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= @@ -161,36 +160,36 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= -github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU= +github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI= +github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4= +github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0= -github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= -github.com/wailsapp/wails/v3 v3.0.0-alpha.80 h1:AeJGPYEf4MnEzNAqHzmhWTuvv7ZyKL51hFQsDxEWBks= -github.com/wailsapp/wails/v3 v3.0.0-alpha.80/go.mod h1:4saK4A4K9970X+X7RkMwP2lyGbLogcUz54wVeq4C/V8= +github.com/wailsapp/wails/v3 v3.0.0-alpha.92 h1:LYVe5RcGDB/x1QTWE0JbonXZc7+BmCtjb3+mHa1riao= +github.com/wailsapp/wails/v3 v3.0.0-alpha.92/go.mod h1:4cKvtUppwqYC9tVtvgHWzEmXfUnuLEV3q8d0Jh6xkQQ= +github.com/wailsapp/wails/webview2 v1.0.24 h1:uULnjCSaRfMlU84mS3kjLgPsRosEOIusVK1nFOHZHzs= +github.com/wailsapp/wails/webview2 v1.0.24/go.mod h1:sdf+s0nAdxlzVWf9SCxC15XaxnQPJeY+uU1Ucn3jHQM= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= -github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= -github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= +github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= -golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= -golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw= +golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -202,22 +201,19 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= -golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -230,30 +226,30 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= -modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= -modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= -modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= -modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= -modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY= +modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ= +modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= -modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= +modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= -modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= +modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU= +modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= -modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= -modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= +modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY= -modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= +modernc.org/sqlite v1.50.1 h1:l+cQvn0sd0zJJtfygGHuQJ5AjlrwXmWPw4KP3ZMwr9w= +modernc.org/sqlite v1.50.1/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/internal/filesystem/file_lock.go b/internal/filesystem/file_lock.go index 6ed88a8..ea3b375 100644 --- a/internal/filesystem/file_lock.go +++ b/internal/filesystem/file_lock.go @@ -20,8 +20,8 @@ func NewFileLockChecker() *FileLockChecker { // IsFileLocked 检查文件是否被锁定(被其他进程占用) // 返回: (是否锁定, 错误信息, 错误) func (c *FileLockChecker) IsFileLocked(path string) (bool, string, error) { - // 尝试以独占写模式打开文件 - file, err := os.OpenFile(path, os.O_RDWR|syscall.O_CREAT, 0666) + // 尝试以独占写模式打开文件(不创建新文件) + file, err := os.OpenFile(path, os.O_RDWR, 0) if err != nil { // 检查是否是锁相关的错误 if isLockError(err) { diff --git a/internal/filesystem/recycle_bin.go b/internal/filesystem/recycle_bin.go index 379c7e4..f4e5a50 100644 --- a/internal/filesystem/recycle_bin.go +++ b/internal/filesystem/recycle_bin.go @@ -8,6 +8,7 @@ import ( "math/big" "os" "path/filepath" + "sync" "time" ) @@ -23,9 +24,10 @@ type RecycleBinEntry struct { // RecycleBin 回收站管理器 type RecycleBin struct { - binPath string + mu sync.Mutex + binPath string metadataFile string - entries []RecycleBinEntry + entries []RecycleBinEntry } // NewRecycleBin 创建回收站管理器 @@ -57,6 +59,8 @@ func NewRecycleBin(binPath string) (*RecycleBin, error) { // MoveToRecycleBin 移动文件到回收站 func (rb *RecycleBin) MoveToRecycleBin(path string) error { + rb.mu.Lock() + defer rb.mu.Unlock() // 获取文件信息 info, err := os.Stat(path) if err != nil { @@ -111,6 +115,8 @@ func (rb *RecycleBin) MoveToRecycleBin(path string) error { // RestoreFromRecycleBin 从回收站恢复文件 func (rb *RecycleBin) RestoreFromRecycleBin(recyclePath string) error { + rb.mu.Lock() + defer rb.mu.Unlock() // 查找对应的元数据条目 var entry *RecycleBinEntry for i := range rb.entries { @@ -156,6 +162,8 @@ func (rb *RecycleBin) RestoreFromRecycleBin(recyclePath string) error { // DeletePermanently 永久删除回收站中的文件 func (rb *RecycleBin) DeletePermanently(recyclePath string) error { + rb.mu.Lock() + defer rb.mu.Unlock() // 查找元数据条目 for i, entry := range rb.entries { if entry.DeletedPath == recyclePath { @@ -180,11 +188,15 @@ func (rb *RecycleBin) DeletePermanently(recyclePath string) error { // ListEntries 列出回收站中的所有条目 func (rb *RecycleBin) ListEntries() []RecycleBinEntry { - return rb.entries + rb.mu.Lock() + defer rb.mu.Unlock() + return append([]RecycleBinEntry(nil), rb.entries...) } // Empty 清空回收站 func (rb *RecycleBin) Empty() error { + rb.mu.Lock() + defer rb.mu.Unlock() // 删除所有文件 for _, entry := range rb.entries { if err := os.RemoveAll(entry.DeletedPath); err != nil { @@ -215,6 +227,8 @@ func (rb *RecycleBin) autoCleanup() { // cleanupExpiredEntries 清理过期的条目 func (rb *RecycleBin) cleanupExpiredEntries() { + rb.mu.Lock() + defer rb.mu.Unlock() now := time.Now() expiredEntries := make([]int, 0) diff --git a/internal/filesystem/service.go b/internal/filesystem/service.go index bcbf90b..25e9a79 100644 --- a/internal/filesystem/service.go +++ b/internal/filesystem/service.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "errors" "fmt" + "log/slog" "os" "os/exec" "path/filepath" @@ -248,7 +249,7 @@ func (s *FileSystemService) DeletePathWithContext(ctx context.Context, path stri if !isInRecycleBin(path) { if err := s.recycleBin.MoveToRecycleBin(path); err != nil { // 回收站失败,记录但继续 - fmt.Printf("[警告] 移动到回收站失败: %v\n", err) + slog.Warn("移动到回收站失败", "error", err) } } } diff --git a/internal/ossdrv/service.go b/internal/ossdrv/service.go index 2d55db1..e3ab8b9 100644 --- a/internal/ossdrv/service.go +++ b/internal/ossdrv/service.go @@ -617,13 +617,18 @@ func (s *Service) RenamePath(connID string, oldPath string, newPath string) (*fi if err := c.Copy(ctx, f.Key, newPrefix+relativeKey); err != nil { return nil, fmt.Errorf("复制失败: %w", err) } - c.Delete(ctx, f.Key) + if err := c.Delete(ctx, f.Key); err != nil { + return nil, fmt.Errorf("删除源文件失败: %w", err) + } } if !listResult.IsTruncated { break } } - c.Delete(ctx, oldKey) // marker + if err := c.Delete(ctx, oldKey); err != nil { // marker + // 目录标记删除失败不影响重命名结果 + _ = err + } } else { if err := c.Copy(ctx, oldKey, newKey); err != nil { return nil, fmt.Errorf("复制失败: %w", err) diff --git a/internal/storage/download_cache.go b/internal/storage/download_cache.go index c717a5e..850dc05 100644 --- a/internal/storage/download_cache.go +++ b/internal/storage/download_cache.go @@ -3,6 +3,7 @@ package storage import ( "crypto/sha256" "fmt" + "log/slog" "os" "path/filepath" "time" @@ -94,7 +95,7 @@ func CleanupExpiredCache() { } if len(expired) > 0 { - fmt.Printf("[下载缓存] 清理 %d 条过期记录\n", len(expired)) + slog.Info("清理过期下载缓存", "count", len(expired)) } }