重构: App拆分10域文件+并发安全+结构化日志+依赖升级

This commit is contained in:
lxy
2026-05-16 20:45:14 +08:00
parent d17c20c579
commit 767311fc04
27 changed files with 1457 additions and 1218 deletions
+55 -2
View File
@@ -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 架构 - **Wails v2.12 → v3 alpha.80**: 全面迁移至 Wails v3 架构
+32 -30
View File
@@ -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 存储**: 七牛云/阿里云双厂商,AK/SK 认证,桶级客户端懒创建+缓存
- **多桶导航**: 根目录自动列出所有桶,点进桶浏览文件,桶级客户端懒创建+缓存 - **OSS 全功能 CRUD**: 列目录/读文件/写文件/创建/删除/重命名/预签名 URL
- **OSS 全功能 CRUD**: 列目录/读文件/写文件/创建/删除/重命名/预签名URL - **OSS 区域自动嗅探**: 连接时自动探测桶区域,无需手动填写
- **GBK 编码自动转换**: 文件预览智能检测编码(UTF-8/GBK),解决 LRC 等中文文件乱码 - **七牛云桶管理**: 查询桶域名、真实区域、设置访问权限(公开/私有)
- **桶图标 🪣**: OSS 桶与普通文件夹图标区分 - **HTML 网站预览**: OSS/SFTP 上的 HTML 文件自动下载关联资源(JS/CSS/图片),逐级嗅探网站根目录,完整预览静态站点
- **连接对话框 OSS 分类**: 「云OSS」父分类 + 厂商子选择(七牛云/阿里云) - **BGM 音乐播放器**: 编辑器底部控制条,支持播放/暂停/上下首/进度拖拽/四种播放模式(单曲/顺序/随机/循环),播放列表持久化,失败自动跳过
- **Sidebar 折叠架构**: 收藏夹和帮助文档独立区块,各自支持折叠/展开 - **LRC 歌词同步显示**: 播放时自动查找同名 .lrc 文件,实时高亮滚动歌词,点击歌词行跳转
- **帮助文档区块**: 静态快捷键参考面板,默认展示 - **全局热键**: Ctrl+Shift+B 系统级快捷键,应用最小化时快速唤起
- **收藏夹内部滚动**: 收藏内容多时列表区域独立滚动,帮助区块固定底部不溢出 - **文件变更监听**: 文件外部被修改时自动刷新,避免编辑冲突
- **下载缓存**: 远程文件预览缓存到本地 SQLite(24 小时过期),重启后仍有效
- **远程服务器监控**: 侧边栏显示磁盘/CPU/内存使用率,超 90% 红色警告,15 秒自动刷新
- **连接指示器快速导航**: 悬停弹出根目录子文件夹列表,可递归展开,快速跳转
- **侧边栏自定义**: 设置面板支持服务器/收藏夹/帮助区块拖拽排序和显隐控制
### 重构 🔧 ### 优化 🚀
- **Wails v3 迁移**: 从 Wails v2 升级至 v3 alpha.80,全面重构项目架构 - **侧边栏折叠架构**: 服务器、收藏夹、帮助独立可折叠区块,收藏夹内部独立滚动
- **前端目录规范化**: `web/` → `frontend/`,对齐 Wails v3 标准目录结构 - **七牛云目录层级**: ListFiles 支持 delimiter,正确展示文件夹层级而非平铺
- **跨平台构建配置**: 新增 Android/iOS/Linux/macOS/Docker 构建模板(Taskfile.yml) - **七牛云签名 URL**: 修正私有空间下载/预览签名格式
- **v3 Bindings**: 自动生成的 TypeScript 绑定替代手动维护的 wailsjs - **七牛云下载域名**: 智能解析策略:临时域名→自定义域名→CDN 兜底,失败自动重试
- **GBK 编码自动转换**: 文件预览智能检测编码(UTF-8/GBK),中文文件不再乱码
- **SFTP 功能对齐**: 补全粘贴图片、网站预览、系统监控、常用路径等能力
- **预览缓存迁移**: 前端 LRU → 后端 SQLite,重启后缓存仍有效
- **Wails v3 迁移**: 框架升级至 v3 alpha.92,核心依赖全面更新
### 修复 🐛 ### 修复 🐛
- **MP3 误报加载失败**: 音频 @canplay 清除错误状态 + previewUrl watcher 重置 - **二进制文件后预览失效**: 打开二进制文件后图片/视频无法预览
- **启动路径恢复错误**: 本地模式跳过 Linux/OSS 路径残留,避免 `open /bucket` 报错 - **跨服务器 Tab 切换**: 面包屑和文件列表未同步更新
- **阿里云签名修复**: ListFiles 签名不含 list 查询参数(prefix/delimiter/marker/max-keys 非子资源) - **代码编辑器加载失败**: 异步组件失败后自动重试
- **阿里云 XML 解析**: `<Contents>` 直接映射文件字段,修正 `Contents.Object` 嵌套错误 - **OSS 收藏打开失败**: provider 字段映射修正
- **阿里云 LastModified**: 宽容时间解析(4 种格式兼容) - **阿里云 OSS 签名/解析**: 文件列表获取失败、时间格式不兼容等问题
- **临时文件白名单放行**: OSS/SFTP 预览文件绕过文件类型限制 - **启动路径错误**: 本地模式跳过无效的 OSS 路径残留
- **custom.js 404**: AssetOptions Middleware 拦截返回空响应,消除控制台报错 - **OSS 目录重命名**: 单文件失败不中断整体操作
- **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)
--- ---
+35 -30
View File
@@ -4,52 +4,57 @@
## 功能 ## 功能
- 文件浏览 / 编辑 / 预览(文本、Markdown、图片、Office、PDF) - **本地文件管理**: 浏览、编辑、重命名、删除、回收站
- 收藏夹管理(折叠/展开、拖拽排序、置顶) - **SFTP 远程连接**: SSH 密钥/密码认证,文件上传下载
- Markdown 编辑器(实时预览、语法高亮、Mermaid 图表) - **云 OSS 存储**: 七牛云/阿里云,多桶浏览
- 远程文件服务器连接 - **多格式预览**: 图片/视频/音频/PDF/Office/HTML
- 主题切换(亮色/暗色) - **Markdown 编辑器**: 实时预览、Mermaid 图表、语法高亮
- 版本更新检查 - **收藏夹管理**: 折叠/展开、拖拽排序、置顶
- **BGM 播放**: 音频播放列表、全局热键 Ctrl+Shift+B
- **PDF 导出**: Markdown/HTML 导出为 PDF
- **版本更新检查**: 自动检测新版本
- **主题切换**: 亮色/暗色
## 技术栈 ## 技术栈
| 层 | 技术 | | 层 | 技术 |
|---|------| |---|------|
| 桌面框架 | Wails v3 (alpha.80) | | 桌面框架 | Wails v3 (alpha.82) |
| 后端 | Go 1.22+ | | 后端 | Go 1.24 |
| 前端 | Vue 3 + TypeScript | | 前端 | Vue 3 + TypeScript |
| UI 组件库 | Arco Design Vue | | UI 组件库 | Arco Design Vue |
| 编辑器 | CodeMirror 6 | | 编辑器 | CodeMirror 6 |
| 构建 | Vite 7 + Taskfile | | 构建 | Vite 7 + Taskfile |
| 数据库 | SQLite (配置/历史) |
| 文件服务 | 内置 HTTP 文件服务器 (端口 2652) |
## 项目结构 ## 项目结构
``` ```
├── main.go # 入口:窗口配置、中间件、DevTools ├── main.go # 入口:窗口配置、中间件
├── app.go # 应用逻辑:文件系统、更新检查等 ├── app.go # App 结构体定义、生命周期、请求类型
├── internal/ # 内部模块 ├── app_*.go # 按功能域拆分(config/sftp/oss/...)
│ ├── filesystem/ # 文件操作、锁、预览服务 ├── internal/
│ └── api/ # API 处理器 │ ├── api/ # Wails 绑定 API 层
├── frontend/ # 前端代码 (Vue 3) │ ├── filesystem/ # 文件系统服务 + HTTP 文件服务器
│ ├── src/ │ ├── ossdrv/ # OSS 驱动(七牛/阿里云)
│ │ ├── components/FileSystem/ # 文件管理主组件 │ ├── sftp/ # SFTP 客户端
│ │ ├── stores/ # Pinia 状态管理 │ ├── storage/ # SQLite 存储
│ │ ├── api/ # 后端调用封装 │ └── service/ # 业务服务
│ │ └── utils/ # 工具函数 ├── frontend/
│ └── vite.config.js │ └── src/
│ ├── components/FileSystem/ # 文件管理主组件
│ ├── api/ # 后端调用封装
│ ├── composables/ # Vue Composables
│ └── stores/ # Pinia 状态管理
├── build/ # 构建配置(跨平台) ├── build/ # 构建配置(跨平台)
│ ├── config.yml # Wails 项目配置
│ └── windows/ # Windows 构建脚本
└── configs/ # 运行时配置 └── configs/ # 运行时配置
``` ```
## 开发 ## 开发
```bash ```bash
# 安装依赖 # 启动开发模式
wails3 task common:install:frontend:deps
# 启动开发模式(热重载)
wails3 dev wails3 dev
# 生产构建 # 生产构建
@@ -59,16 +64,16 @@ wails3 build
### 构建标签 ### 构建标签
- `production` — 生产模式,使用嵌入的 frontend dist - `production` — 生产模式,使用嵌入的 frontend dist
- `devtools` — 在生产构建中保留 DevTools(F12) - `devtools` — 在生产构建中保留 DevTools
## 快捷键 ## 快捷键
| 快捷键 | 功能 | | 快捷键 | 功能 |
|--------|------| |--------|------|
| Ctrl+B | 切换侧边栏 | | Ctrl+B | 切换侧边栏 |
| Ctrl+H | 历史记录 | | Ctrl+Shift+B | BGM 播放栏 |
| Ctrl+F | 聚焦搜索 | | F11 | 全屏预览 |
## 版本历史 ## 版本历史
详见 [CHANGELOG.md](./CHANGELOG.md) 详见 [CHANGELOG.md](./CHANGELOG.md) | 内部技术细节见 [CHANGELOG.internal.md](./CHANGELOG.internal.md)
+33 -1031
View File
File diff suppressed because it is too large Load Diff
+215
View File
@@ -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
}
+174
View File
@@ -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)
}
+101
View File
@@ -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)
}
+50
View File
@@ -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()
}
+23
View File
@@ -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()
}
+135
View File
@@ -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)
}
+84
View File
@@ -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
}
+172
View File
@@ -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)
}
}
+77
View File
@@ -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")
}
}
+75
View File
@@ -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
}
+38 -4
View File
@@ -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()` - [ ] ⑦ `internal/api/pdf_api.go:371` SelectDirectory 改用 Wails 原生对话框 `application.Get().Dialog.OpenFile()`
- [ ] ⑧ `app.go:176-189` HWND 轮询改事件驱动 — v3 alpha 暂无对应 API,后续跟进 - [ ] ⑧ `app.go:176-189` HWND 轮询改事件驱动 — v3 alpha 暂无对应 API,后续跟进
- [ ] ⑩ `app.go:29` Windows 专用导入 `golang.org/x/sys/windows/registry` 加构建标签拆到 `*_windows.go` - [x] ⑩ `app.go:29` Windows 专用导入拆到 `app_windows.go`(registry/w32 包隔离)
- [ ] ⑪ 全局结构化日志 — `fmt.Println` 替换为 `log/slog`,按优先级分批替换 - [x] ⑪ 全局结构化日志 — `fmt.Println/Printf` 全部替换为 `log/slog`(app.go + internal/ 共 45 处)
- [ ] ⑬ `app.go:158` 更新检查 URL `https://c.1216.top/last-version.json` 移入配置 - [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 种写法统一 — 大规模重构,独立迭代
+3
View File
@@ -325,6 +325,9 @@ export function OpenPath(path: string): $CancellablePromise<void> {
return $Call.ByID(1591734570, path); return $Call.ByID(1591734570, path);
} }
/**
* OssConnect 连接 OSS 存储
*/
export function OssConnect(req: $models.OssConnectRequest): $CancellablePromise<string> { export function OssConnect(req: $models.OssConnectRequest): $CancellablePromise<string> {
return $Call.ByID(3667022538, req); return $Call.ByID(3667022538, req);
} }
@@ -445,10 +445,15 @@ import { parseLrc, type LrcLine } from '@/utils/lrcParser'
import BgmBar, { type BgmTrack } from './FileEditor/BgmBar.vue' import BgmBar, { type BgmTrack } from './FileEditor/BgmBar.vue'
// 异步加载 CodeEditor 组件,减少初始包大小 // 异步加载 CodeEditor 组件,减少初始包大小
let editorRetryCount = 0
const AsyncCodeEditor = defineAsyncComponent({ const AsyncCodeEditor = defineAsyncComponent({
loader: () => import('@/components/CodeEditor.vue'), loader: () => import('@/components/CodeEditor.vue'),
delay: 200, delay: 200,
timeout: 10000 timeout: 10000,
onError(error, retry) {
console.error('[AsyncCodeEditor] 加载失败:', error)
if (++editorRetryCount <= 2) retry()
}
}) })
// Office 预览容器引用 // Office 预览容器引用
@@ -499,6 +499,7 @@ ${ext ? `文件类型: ${fileTypeDesc}\n` : ''}
const clearContent = () => { const clearContent = () => {
fileContent.value = '' fileContent.value = ''
originalContent.value = '' originalContent.value = ''
isBinaryFile.value = false
} }
/** /**
@@ -22,6 +22,7 @@ const CONTENT_DETECT_MAX_SIZE = 500 * 1024 // 500KB
// 缓存检测结果 // 缓存检测结果
const contentDetectCache = new Map<string, { timestamp: number; result: any }>() const contentDetectCache = new Map<string, { timestamp: number; result: any }>()
const CACHE_TTL = 60000 // 1分钟缓存 const CACHE_TTL = 60000 // 1分钟缓存
const CACHE_MAX_SIZE = 200
// ====== URL 工具函数(导出供外部使用) ====== // ====== URL 工具函数(导出供外部使用) ======
@@ -122,6 +123,7 @@ export function useFilePreview() {
try { try {
const result = await detectFileTypeByContent(path) const result = await detectFileTypeByContent(path)
const data = { category: result.category, ext: result.extension } const data = { category: result.category, ext: result.extension }
if (contentDetectCache.size >= CACHE_MAX_SIZE) contentDetectCache.clear()
contentDetectCache.set(path, { timestamp: Date.now(), result: data }) contentDetectCache.set(path, { timestamp: Date.now(), result: data })
return data return data
} catch { } catch {
@@ -1158,6 +1158,14 @@ const activateTab = async (tab: PreviewTab) => {
} }
} }
selectedFileItem.value = tab.fileItem 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) { if (tab.loaded) {
restoreTabState(tab) restoreTabState(tab)
} else { } else {
+30 -29
View File
@@ -7,14 +7,14 @@ require (
github.com/chromedp/chromedp v0.15.1 github.com/chromedp/chromedp v0.15.1
github.com/fsnotify/fsnotify v1.10.1 github.com/fsnotify/fsnotify v1.10.1
github.com/glebarez/sqlite v1.11.0 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/pkg/sftp v1.13.10
github.com/shirou/gopsutil/v3 v3.24.5 github.com/shirou/gopsutil/v3 v3.24.5
github.com/wailsapp/wails/v3 v3.0.0-alpha.80 github.com/wailsapp/wails/v3 v3.0.0-alpha.92
github.com/yuin/goldmark v1.7.16 github.com/yuin/goldmark v1.8.2
golang.org/x/crypto v0.47.0 golang.org/x/crypto v0.51.0
golang.org/x/sys v0.42.0 golang.org/x/sys v0.44.0
golang.org/x/text v0.33.0 golang.org/x/text v0.37.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
gorm.io/gorm v1.31.1 gorm.io/gorm v1.31.1
) )
@@ -22,7 +22,7 @@ require (
require ( require (
dario.cat/mergo v1.0.2 // indirect dario.cat/mergo v1.0.2 // indirect
github.com/Microsoft/go-winio v0.6.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/adrg/xdg v0.5.3 // indirect
github.com/bep/debounce v1.2.1 // indirect github.com/bep/debounce v1.2.1 // indirect
github.com/chromedp/sysutil v1.1.0 // indirect github.com/chromedp/sysutil v1.1.0 // indirect
@@ -30,13 +30,13 @@ require (
github.com/coder/websocket v1.8.14 // indirect github.com/coder/websocket v1.8.14 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/dustin/go-humanize v1.0.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/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/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.7.0 // indirect github.com/go-git/go-billy/v5 v5.9.0 // indirect
github.com/go-git/go-git/v5 v5.16.4 // indirect github.com/go-git/go-git/v5 v5.19.0 // indirect
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // 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/go-ole/go-ole v1.3.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // 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/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // 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/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kr/fs v0.1.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/go-ansi-parser v1.6.1 // indirect
github.com/leaanthony/u v1.1.1 // indirect github.com/leaanthony/u v1.1.1 // indirect
github.com/lmittmann/tint v1.1.2 // indirect github.com/lmittmann/tint v1.1.3 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect
github.com/mattn/go-colorable v0.1.14 // 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/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/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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // 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/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/skeema/knownhosts v1.3.2 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/go-sysconf v0.4.0 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect github.com/tklauser/numcpus v0.12.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // 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/xanzy/ssh-agent v0.3.3 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect
golang.org/x/net v0.49.0 // indirect golang.org/x/net v0.54.0 // indirect
golang.org/x/time v0.14.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 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/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.44.3 // indirect modernc.org/sqlite v1.50.1 // indirect
) )
+78 -82
View File
@@ -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.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 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.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= 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 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 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/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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 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.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= 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 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= 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 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 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 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= 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.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= 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 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= 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 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= 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 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 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.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
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/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 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-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.19.0 h1:+WkVUQZSy/F1Gb13udrMKjIM2PrzsNfDKFSfo5tkMtc=
github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-git/go-git/v5 v5.19.0/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 h1:vymEbVwYFP/L05h5TKQxvkXoKxNvTpjxYKdF1Nlwuao= github.com/go-json-experiment/json v0.0.0-20260505212615-e40f80bf6836 h1:5KGUhXZFTN1PrCY4zUZLe1J8n7uBNmPDbCLCn78EbPQ=
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= 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.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 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= 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/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 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= 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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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= 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/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 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 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.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= 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 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= 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.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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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.2 h1:nnh2sCzGCVYnU+wCisMPiYapEg/QVo/gcI9ePKg5/T4=
github.com/labstack/echo/v4 v4.15.1/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c= github.com/labstack/echo/v4 v4.15.2/go.mod h1:Xzp1Ns1RA2c9fY7nSgUJkpkUZGNbEIVHZbtbOMPktBI=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.5.0 h1:6VSQ2NOzsnEJ5W6+84E0RbcaDDmgB6NIAzWCczTEe6c=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 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 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= 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 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= 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 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= 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.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I=
github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= 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.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 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= 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 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 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.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 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 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= 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 h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= 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.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= 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 h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
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/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 h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 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= 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/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 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 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.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= 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 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 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 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= 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.2.1 h1:yqRB4fvOge2+FyRXFkXqsyMoqPazv14Yyy+iyccT2E4=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/go-m1cpu v0.2.1/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= 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/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 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= 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.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 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 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.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= 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 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 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 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 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/wails/v3 v3.0.0-alpha.92 h1:LYVe5RcGDB/x1QTWE0JbonXZc7+BmCtjb3+mHa1riao=
github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= github.com/wailsapp/wails/v3 v3.0.0-alpha.92/go.mod h1:4cKvtUppwqYC9tVtvgHWzEmXfUnuLEV3q8d0Jh6xkQQ=
github.com/wailsapp/wails/v3 v3.0.0-alpha.80 h1:AeJGPYEf4MnEzNAqHzmhWTuvv7ZyKL51hFQsDxEWBks= github.com/wailsapp/wails/webview2 v1.0.24 h1:uULnjCSaRfMlU84mS3kjLgPsRosEOIusVK1nFOHZHzs=
github.com/wailsapp/wails/v3 v3.0.0-alpha.80/go.mod h1:4saK4A4K9970X+X7RkMwP2lyGbLogcUz54wVeq4C/V8= 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 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 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.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= 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 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 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.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.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= 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.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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-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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/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.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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.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.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= 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.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 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.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.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 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= 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 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= 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.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= 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 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= 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.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= 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 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= 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.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= 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 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= 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 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= 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 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY= modernc.org/sqlite v1.50.1 h1:l+cQvn0sd0zJJtfygGHuQJ5AjlrwXmWPw4KP3ZMwr9w=
modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= 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 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+2 -2
View File
@@ -20,8 +20,8 @@ func NewFileLockChecker() *FileLockChecker {
// IsFileLocked 检查文件是否被锁定(被其他进程占用) // IsFileLocked 检查文件是否被锁定(被其他进程占用)
// 返回: (是否锁定, 错误信息, 错误) // 返回: (是否锁定, 错误信息, 错误)
func (c *FileLockChecker) IsFileLocked(path string) (bool, string, error) { 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 err != nil {
// 检查是否是锁相关的错误 // 检查是否是锁相关的错误
if isLockError(err) { if isLockError(err) {
+15 -1
View File
@@ -8,6 +8,7 @@ import (
"math/big" "math/big"
"os" "os"
"path/filepath" "path/filepath"
"sync"
"time" "time"
) )
@@ -23,6 +24,7 @@ type RecycleBinEntry struct {
// RecycleBin 回收站管理器 // RecycleBin 回收站管理器
type RecycleBin struct { type RecycleBin struct {
mu sync.Mutex
binPath string binPath string
metadataFile string metadataFile string
entries []RecycleBinEntry entries []RecycleBinEntry
@@ -57,6 +59,8 @@ func NewRecycleBin(binPath string) (*RecycleBin, error) {
// MoveToRecycleBin 移动文件到回收站 // MoveToRecycleBin 移动文件到回收站
func (rb *RecycleBin) MoveToRecycleBin(path string) error { func (rb *RecycleBin) MoveToRecycleBin(path string) error {
rb.mu.Lock()
defer rb.mu.Unlock()
// 获取文件信息 // 获取文件信息
info, err := os.Stat(path) info, err := os.Stat(path)
if err != nil { if err != nil {
@@ -111,6 +115,8 @@ func (rb *RecycleBin) MoveToRecycleBin(path string) error {
// RestoreFromRecycleBin 从回收站恢复文件 // RestoreFromRecycleBin 从回收站恢复文件
func (rb *RecycleBin) RestoreFromRecycleBin(recyclePath string) error { func (rb *RecycleBin) RestoreFromRecycleBin(recyclePath string) error {
rb.mu.Lock()
defer rb.mu.Unlock()
// 查找对应的元数据条目 // 查找对应的元数据条目
var entry *RecycleBinEntry var entry *RecycleBinEntry
for i := range rb.entries { for i := range rb.entries {
@@ -156,6 +162,8 @@ func (rb *RecycleBin) RestoreFromRecycleBin(recyclePath string) error {
// DeletePermanently 永久删除回收站中的文件 // DeletePermanently 永久删除回收站中的文件
func (rb *RecycleBin) DeletePermanently(recyclePath string) error { func (rb *RecycleBin) DeletePermanently(recyclePath string) error {
rb.mu.Lock()
defer rb.mu.Unlock()
// 查找元数据条目 // 查找元数据条目
for i, entry := range rb.entries { for i, entry := range rb.entries {
if entry.DeletedPath == recyclePath { if entry.DeletedPath == recyclePath {
@@ -180,11 +188,15 @@ func (rb *RecycleBin) DeletePermanently(recyclePath string) error {
// ListEntries 列出回收站中的所有条目 // ListEntries 列出回收站中的所有条目
func (rb *RecycleBin) ListEntries() []RecycleBinEntry { func (rb *RecycleBin) ListEntries() []RecycleBinEntry {
return rb.entries rb.mu.Lock()
defer rb.mu.Unlock()
return append([]RecycleBinEntry(nil), rb.entries...)
} }
// Empty 清空回收站 // Empty 清空回收站
func (rb *RecycleBin) Empty() error { func (rb *RecycleBin) Empty() error {
rb.mu.Lock()
defer rb.mu.Unlock()
// 删除所有文件 // 删除所有文件
for _, entry := range rb.entries { for _, entry := range rb.entries {
if err := os.RemoveAll(entry.DeletedPath); err != nil { if err := os.RemoveAll(entry.DeletedPath); err != nil {
@@ -215,6 +227,8 @@ func (rb *RecycleBin) autoCleanup() {
// cleanupExpiredEntries 清理过期的条目 // cleanupExpiredEntries 清理过期的条目
func (rb *RecycleBin) cleanupExpiredEntries() { func (rb *RecycleBin) cleanupExpiredEntries() {
rb.mu.Lock()
defer rb.mu.Unlock()
now := time.Now() now := time.Now()
expiredEntries := make([]int, 0) expiredEntries := make([]int, 0)
+2 -1
View File
@@ -5,6 +5,7 @@ import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"log/slog"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@@ -248,7 +249,7 @@ func (s *FileSystemService) DeletePathWithContext(ctx context.Context, path stri
if !isInRecycleBin(path) { if !isInRecycleBin(path) {
if err := s.recycleBin.MoveToRecycleBin(path); err != nil { if err := s.recycleBin.MoveToRecycleBin(path); err != nil {
// 回收站失败,记录但继续 // 回收站失败,记录但继续
fmt.Printf("[警告] 移动到回收站失败: %v\n", err) slog.Warn("移动到回收站失败", "error", err)
} }
} }
} }
+7 -2
View File
@@ -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 { if err := c.Copy(ctx, f.Key, newPrefix+relativeKey); err != nil {
return nil, fmt.Errorf("复制失败: %w", err) 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 { if !listResult.IsTruncated {
break break
} }
} }
c.Delete(ctx, oldKey) // marker if err := c.Delete(ctx, oldKey); err != nil { // marker
// 目录标记删除失败不影响重命名结果
_ = err
}
} else { } else {
if err := c.Copy(ctx, oldKey, newKey); err != nil { if err := c.Copy(ctx, oldKey, newKey); err != nil {
return nil, fmt.Errorf("复制失败: %w", err) return nil, fmt.Errorf("复制失败: %w", err)
+2 -1
View File
@@ -3,6 +3,7 @@ package storage
import ( import (
"crypto/sha256" "crypto/sha256"
"fmt" "fmt"
"log/slog"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
@@ -94,7 +95,7 @@ func CleanupExpiredCache() {
} }
if len(expired) > 0 { if len(expired) > 0 {
fmt.Printf("[下载缓存] 清理 %d 条过期记录\n", len(expired)) slog.Info("清理过期下载缓存", "count", len(expired))
} }
} }