From b849e6cc46b732253878d97b769e78bc0b39cfba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Wed, 28 Jan 2026 22:48:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9A=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=96=87=E4=BB=B6=E7=B3=BB=E7=BB=9F=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ConfigAPI 和 ConfigService 实现配置管理 - 新增 SettingsPanel 和 UpdateNotification 组件 - 文件系统模块化重构,提升代码质量 - 提取公共函数,优化代码结构 - 版本号更新至 0.2.0 --- CHANGELOG.md | 116 +--- app.go | 527 +++++++++++----- internal/api/config_api.go | 137 +++++ internal/common/constants.go | 17 + internal/common/path.go | 45 ++ internal/common/utils.go | 36 ++ internal/filesystem/errors.go | 13 + internal/filesystem/fs.go | 266 +++----- internal/filesystem/service.go | 316 +++++++--- internal/filesystem/service_interfaces.go | 3 + internal/service/config_service.go | 118 ++++ internal/service/update.go | 26 +- internal/service/update_config.go | 4 +- internal/service/update_download.go | 3 +- internal/service/version.go | 2 +- internal/storage/models/app_config.go | 18 + internal/storage/sqlite.go | 1 + main.go | 64 +- wails.json | 1 + web/package-lock.json | 27 + web/package.json | 1 + web/package.json.md5 | 2 +- web/src/App.vue | 441 ++++++++------ web/src/api/system.ts | 25 + web/src/components/CodeEditor.vue | 23 + web/src/components/FileSystem.vue | 701 ++++++++++++++++++++-- web/src/components/SettingsPanel.vue | 369 ++++++++++++ web/src/components/UpdateNotification.vue | 405 +++++++++++++ web/src/components/UpdatePanel.vue | 203 ++++--- web/src/composables/useFavoriteFiles.js | 25 +- web/src/utils/constants.js | 6 +- 31 files changed, 3024 insertions(+), 917 deletions(-) create mode 100644 internal/api/config_api.go create mode 100644 internal/common/constants.go create mode 100644 internal/common/path.go create mode 100644 internal/service/config_service.go create mode 100644 internal/storage/models/app_config.go create mode 100644 web/src/components/SettingsPanel.vue create mode 100644 web/src/components/UpdateNotification.vue diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f6ad33..c90c34d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,113 +1,41 @@ # 更新日志 -## [0.2.0] - 2025-01-28 - -### 变更 🔄 -- **模块重命名**:项目模块名从 `go-desk` 更改为 `u-desk` -- **依赖更新**:所有依赖包更新到最新稳定版本 - - go.mongodb.org/mongo-driver v1.17.6 → v1.17.7 - - github.com/go-sql-driver/mysql v1.8.1 → v1.9.3 - - github.com/redis/go-redis/v9 v9.17.2 → v9.17.3 - - gorm.io/gorm v1.31.0 → v1.31.1 - - modernc.org/sqlite v1.23.1 → v1.44.3 - - golang.org/x/crypto v0.45.0 → v0.47.0 - - golang.org/x/net v0.47.0 → v0.49.0 - - 其他 30+ 个依赖包更新 - ---- - -## [0.1.0] - 2025-01-28 +## [0.2.0] - 2026-01-28 ### 新增 ✨ -- **文件系统模块化架构**:将文件管理功能拆分为多个独立模块 - - 路径验证模块 (`path_validator.go`) - - 文件类型管理模块 (`filetype_manager.go`) - - 目录统计模块 (`directory_stats.go`) - - 审计日志模块 (`audit_log.go`) - - 文件锁模块 (`file_lock.go`) - - 回收站模块 (`recycle_bin.go`) - - ZIP 压缩模块 (`zip.go`, `zip_helper.go`) - - 核心服务模块 (`service.go`) - - 资源处理模块 (`asset_handler.go`) - -- **前端新增组件和工具**: - - `CodeEditor.vue` - 代码编辑器组件 - - `useFileOperations.js` - 文件操作组合式函数 - - `useFavoriteFiles.js` - 收藏文件组合式函数 - - `useLocalStorage.js` - 本地存储组合式函数 - - `constants.js` - 常量定义 - - `fileUtils.js` - 文件工具函数 - - `debugLog.js` - 调试日志工具 - -- **通用工具模块** (`internal/common/`): - - `timeout.go` - 超时处理 - - `utils.go` - 通用工具函数 +- **应用配置管理** - 全新设置面板,支持自定义显示模块和默认启动页 +- **智能更新提醒** - 新增版本更新通知组件,第一时间获取新版本信息 +- **配置服务层** - 新增 ConfigAPI 和 ConfigService 实现统一配置管理 ### 优化 ⚡ -- **应用启动流程优化**: - - SQLite 快速初始化(`InitFast()`) - - 核心 API 同步初始化(`initCoreAPIs()`) - - 文件服务器异步启动(`startFileServer()`) - - UpdateAPI 异步初始化(避免阻塞启动) - -- **代码质量改进**: - - 消除代码重复 60% - - 消除所有魔法数字 - - 统一错误处理模式 - - 改进类型定义 - -### 修复 🐛 -- 修复 `generateRandomString` 性能问题(使用 `crypto/rand` 替代 `time.Sleep`) -- 修复文件锁检查的破坏性操作(使用 `os.OpenFile` 替代 `os.Rename`) - -### 文档 📚 -- 更新 README.md,反映项目当前状态 -- 更新数据库客户端任务规划 -- 创建 PROJECT_STATUS.md 项目状态文档 -- 创建 CHANGELOG.md 更新日志 +- **文件系统模块化重构** - 提升代码质量和可维护性 +- **代码架构优化** - 提取公共函数,消除重复代码 +- **启动流程优化** - 按需加载模块,提升启动性能 --- -## [0.9.0] - 2025-01-27 +## [0.1.5] - 2026-01-22 ### 新增 ✨ -- **文件管理功能**: - - 本地文件系统浏览(支持多盘符) - - 文件预览(图片、文本、代码) - - 文件操作(复制、移动、删除、重命名) - - 常用路径快捷访问(桌面、文档、下载等) - - 搜索与筛选功能 - -- **设备测试功能**: - - 系统设备信息查询 - - 硬件状态检测 - -- **更新管理功能**: - - 应用版本检查 - - 自动更新 - - 更新日志展示 - -### 数据库客户端 🗄️ -- 支持 MySQL、Redis、MongoDB 连接 -- 连接管理(保存、编辑、删除) -- SQL 执行与结果展示 -- 表结构查看 +- **文件管理模块** - 完整的文件浏览、编辑、操作功能 +- **版本更新管理** - 自动检查和应用更新 +- **系统信息查询** - CPU、内存、磁盘等硬件信息 --- -## [0.1.0] - 2025-01-01 +## [0.1.0] - 2026-01-18 -### 初始版本 🎉 -- 项目初始化 -- 基于 Wails 的桌面应用框架 -- 基础 UI 布局 -- 用户查询展示功能 +### 新增 ✨ +- **数据库管理** - 支持多种数据库连接和查询功能 --- -## 版本说明 +## 版本规范 + +版本号格式:`主版本号.次版本号.修订号` (MAJOR.MINOR.PATCH) + +- **主版本号** - 不兼容的 API 修改 +- **次版本号** - 向下兼容的功能性新增 +- **修订号** - 向下兼容的问题修复 + -- **[0.2.0]** - 开发版本(模块重命名、依赖更新) -- **[0.1.0]** - 文件系统重构版本 -- **[0.9.0]** - 功能完善版本 -- **[0.1.0]** - 初始版本 diff --git a/app.go b/app.go index 662dac8..ab15182 100644 --- a/app.go +++ b/app.go @@ -3,28 +3,34 @@ package main import ( "context" "fmt" - "u-desk/internal/api" - "u-desk/internal/database" - "u-desk/internal/filesystem" - "u-desk/internal/storage" - "u-desk/internal/system" "net/http" "os" "path/filepath" "strings" + stdruntime "runtime" + "time" + + "u-desk/internal/api" + "u-desk/internal/common" + "u-desk/internal/database" + "u-desk/internal/filesystem" + "u-desk/internal/storage" + "u-desk/internal/system" "github.com/wailsapp/wails/v2/pkg/runtime" ) // App 应用结构体 type App struct { - ctx context.Context - db *database.DB - connectionAPI *api.ConnectionAPI - sqlAPI *api.SqlAPI - tabAPI *api.TabAPI - updateAPI *api.UpdateAPI - fileServer *http.Server + ctx context.Context + db *database.DB + connectionAPI *api.ConnectionAPI + sqlAPI *api.SqlAPI + tabAPI *api.TabAPI + updateAPI *api.UpdateAPI + configAPI *api.ConfigAPI + fileServer *http.Server + filesystem *filesystem.FileSystemService } // NewApp 创建新的应用实例 @@ -41,24 +47,119 @@ func (a *App) Startup(ctx context.Context) { if err != nil { panic(fmt.Sprintf("SQLite 初始化失败,应用无法启动: %v", err)) } + _ = sqliteDB // 全局 DB 已由 InitFast() 设置 - // 2. 快速初始化核心 API(都是毫秒级操作,不影响启动速度) - if err := a.initCoreAPIs(); err != nil { - panic(fmt.Sprintf("核心 API 初始化失败: %v", err)) + // 2. 初始化配置服务(必需,用于读取模块启用状态) + configService, err := api.NewConfigAPI() + if err != nil { + panic(fmt.Sprintf("配置服务初始化失败: %v", err)) + } + a.configAPI = configService + + // 3. 读取配置,获取可见的 Tabs + visibleTabs := a.getVisibleTabs() + fmt.Printf("[启动] 可用的模块: %v\n", visibleTabs) + + // 4. 根据配置初始化模块(条件初始化) + if err := a.initModulesByConfig(visibleTabs); err != nil { + panic(fmt.Sprintf("模块初始化失败: %v", err)) } - // 3. 异步初始化:文件服务器(不等待) - go a.startFileServer() - - // 4. 异步初始化:UpdateAPI(涉及网络请求,完全异步) + // 5. 异步初始化:UpdateAPI(涉及网络请求,完全异步) go func() { if updateAPI, err := api.NewUpdateAPI("https://img.1216.top/u-desk/last-version.json"); err == nil { a.updateAPI = updateAPI a.updateAPI.SetContext(ctx) + a.startAutoUpdateCheck() } }() - _ = sqliteDB // 标记已使用 +} + +// 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 + data, ok := config["data"].(map[string]interface{}) + if !ok { + return common.DefaultVisibleTabs + } + + // 提取 visibleTabs + 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.TabDatabase) { + fmt.Println("[启动] 初始化数据库模块...") + var err error + + // 初始化 ConnectionAPI + if a.connectionAPI, err = api.NewConnectionAPI(); err != nil { + return err + } + + // 初始化 SqlAPI + if a.sqlAPI, err = api.NewSqlAPI(); err != nil { + return err + } + + // 初始化 TabAPI + if a.tabAPI, err = api.NewTabAPI(); err != nil { + return err + } + + fmt.Println("[启动] 数据库模块初始化完成") + } else { + fmt.Println("[启动] 跳过数据库模块(未启用)") + } + + // 检查是否启用文件系统模块 + 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 启动文件服务器 @@ -79,6 +180,14 @@ func (a *App) startFileServer() { // Shutdown 应用关闭时调用 func (a *App) Shutdown(ctx context.Context) { + // 关闭文件系统服务(优雅关闭,释放资源) + if a.filesystem != nil { + fmt.Println("[文件系统服务] 正在关闭...") + if err := a.filesystem.Close(ctx); err != nil { + fmt.Printf("[文件系统服务] 关闭失败: %v\n", err) + } + } + // 停止文件服务器 if a.fileServer != nil { fmt.Println("[文件服务器] 正在关闭...") @@ -138,7 +247,7 @@ func (a *App) GetDiskInfo() ([]map[string]interface{}, error) { // ReadFile 读取文件 func (a *App) ReadFile(path string) (string, error) { - return filesystem.ReadFile(path) + return a.filesystem.ReadFile(path) } // WriteFileRequest 写入文件请求结构体 @@ -149,32 +258,43 @@ type WriteFileRequest struct { // WriteFile 写入文件 func (a *App) WriteFile(req WriteFileRequest) error { - return filesystem.WriteFile(req.Path, req.Content) + return a.filesystem.WriteFile(req.Path, req.Content) } // ListDir 列出目录 func (a *App) ListDir(path string) ([]map[string]interface{}, error) { - return filesystem.ListDir(path) + return a.filesystem.ListDir(path) } // CreateDir 创建目录 func (a *App) CreateDir(path string) error { - return filesystem.CreateDir(path) + return a.filesystem.CreateDir(path) } // CreateFile 创建文件 func (a *App) CreateFile(path string) error { - return filesystem.CreateFile(path) + return a.filesystem.CreateFile(path) } // DeletePath 删除文件或目录 func (a *App) DeletePath(path string) error { - return filesystem.DeletePath(path) + return a.filesystem.DeletePath(path) +} + +// RenamePathRequest 重命名文件或目录请求结构体 +type RenamePathRequest struct { + OldPath string `json:"oldPath"` + NewPath string `json:"newPath"` +} + +// RenamePath 重命名文件或目录 +func (a *App) RenamePath(req RenamePathRequest) error { + return a.filesystem.RenamePath(req.OldPath, req.NewPath) } // GetFileInfo 获取文件信息 func (a *App) GetFileInfo(path string) (map[string]interface{}, error) { - return filesystem.GetFileInfo(path) + return a.filesystem.GetFileInfo(path) } // GetEnvVars 获取环境变量 @@ -190,30 +310,62 @@ func (a *App) GetEnvVars() (map[string]string, error) { // OpenPath 使用系统默认程序打开文件或目录 func (a *App) OpenPath(path string) error { - return filesystem.OpenPath(path) + return a.filesystem.OpenPath(path) } // ========== Zip 文件操作接口 ========== // ListZipContents 列出 zip 文件内容 func (a *App) ListZipContents(zipPath string) ([]map[string]interface{}, error) { - return filesystem.ListZipContents(zipPath) + return a.filesystem.ListZipContents(zipPath) } // ExtractFileFromZip 从 zip 文件中提取单个文件内容 func (a *App) ExtractFileFromZip(zipPath, filePath string) (string, error) { - return filesystem.ExtractFileFromZip(zipPath, filePath) + return a.filesystem.ExtractFileFromZip(zipPath, filePath) } // ExtractFileFromZipToTemp 从 zip 文件中提取单个文件到临时目录 // 返回临时文件的完整路径,适用于图片等二进制文件 func (a *App) ExtractFileFromZipToTemp(zipPath, filePath string) (string, error) { - return filesystem.ExtractFileFromZipToTemp(zipPath, filePath) + return a.filesystem.ExtractFileFromZipToTemp(zipPath, filePath) } // GetZipFileInfo 获取 zip 文件中特定文件的信息 func (a *App) GetZipFileInfo(zipPath, filePath string) (map[string]interface{}, error) { - return filesystem.GetZipFileInfo(zipPath, filePath) + 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 } // GetCommonPaths 获取常用系统路径 @@ -223,9 +375,6 @@ func (a *App) GetCommonPaths() (map[string]string, error) { return nil, err } - // 获取所有可用驱动器(Windows) - drives := getSystemDrives() - paths := map[string]string{ "home": homeDir, "desktop": filepath.Join(homeDir, "Desktop"), @@ -233,54 +382,22 @@ func (a *App) GetCommonPaths() (map[string]string, error) { "downloads": filepath.Join(homeDir, "Downloads"), } - // 动态添加所有盘符 - for _, drive := range drives { - key := fmt.Sprintf("root_%s", drive[:1]) - paths[key] = drive + // Windows: 动态添加所有盘符 + 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 } -// getSystemDrives 获取系统所有可用驱动器 -func getSystemDrives() []string { - var drives []string - - // Windows: 检查 A-Z 所有盘符 - for _, drive := range "ABCDEFGHIJKLMNOPQRSTUVWXYZ" { - path := string(drive) + ":\\" - if _, err := os.Stat(path); err == nil { - drives = append(drives, path) - } - } - - return drives -} - // ========== 数据库连接管理接口 ========== -// initCoreAPIs 初始化核心 API(快速操作,毫秒级完成) -func (a *App) initCoreAPIs() error { - var err error - - // 初始化 ConnectionAPI - if a.connectionAPI, err = api.NewConnectionAPI(); err != nil { - return err - } - - // 初始化 SqlAPI - if a.sqlAPI, err = api.NewSqlAPI(); err != nil { - return err - } - - // 初始化 TabAPI - if a.tabAPI, err = api.NewTabAPI(); err != nil { - return err - } - - return nil -} - // SaveDbConnection 保存数据库连接配置 func (a *App) SaveDbConnection(req api.SaveConnectionRequest) error { return a.connectionAPI.SaveDbConnection(req) @@ -490,16 +607,74 @@ func (a *App) VerifyUpdateFile(filePath string, expectedHash string, hashType st return a.updateAPI.VerifyUpdateFile(filePath, expectedHash, hashType) } -// ========== 应用生命周期管理 ========== +// startAutoUpdateCheck 启动自动更新检查 +func (a *App) startAutoUpdateCheck() { + if a.updateAPI == nil { + return + } -// shutdown 应用关闭时调用,清理资源 -func (a *App) shutdown(ctx context.Context) { - // 关闭审计日志 - filesystem.CloseAudit() + config, err := a.updateAPI.GetUpdateConfig() + if err != nil || !config["success"].(bool) { + return + } - // 停止文件服务器 - if a.fileServer != nil { - _ = a.fileServer.Shutdown(ctx) + 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() + + // 启动定时器 + ticker := time.NewTicker(time.Duration(interval) * time.Minute) + go func() { + for range ticker.C { + a.checkUpdate() + } + }() +} + +// checkUpdate 执行更新检查 +func (a *App) checkUpdate() { + defer func() { + if r := recover(); r != nil { + fmt.Printf("[自动检查更新] 发生错误: %v\n", r) + } + }() + + if a.updateAPI == nil { + return + } + + result, err := a.updateAPI.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.ctx != nil { + runtime.EventsEmit(a.ctx, "update-available", data) } } @@ -507,29 +682,7 @@ func (a *App) shutdown(ctx context.Context) { // GetAuditLogs 获取审计日志 func (a *App) GetAuditLogs(limit int) ([]map[string]interface{}, error) { - userDataDir := getUserDataDir() - logDir := filepath.Join(userDataDir, "logs") - - entries, err := filesystem.GetRecentLogs(logDir, limit) - if err != nil { - return nil, err - } - - // 转换为map格式 - result := make([]map[string]interface{}, len(entries)) - for i, entry := range entries { - result[i] = map[string]interface{}{ - "timestamp": entry.Timestamp.Format("2006-01-02 15:04:05"), - "operation": entry.Operation, - "path": entry.Path, - "size": entry.Size, - "is_directory": entry.IsDirectory, - "success": entry.Success, - "error": entry.Error, - } - } - - return result, nil + return a.filesystem.GetAuditLogs(limit) } // ========== 文件服务器接口 ========== @@ -543,53 +696,149 @@ func (a *App) GetFileServerURL() string { // GetRecycleBinEntries 获取回收站条目 func (a *App) GetRecycleBinEntries() ([]map[string]interface{}, error) { - bin := filesystem.GetRecycleBin() - if bin == nil { - return []map[string]interface{}{}, nil + 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"` + VisibleTabs []string `json:"visibleTabs"` + DefaultTab string `json:"defaultTab"` +} + +// SaveAppConfig 保存应用配置 +func (a *App) SaveAppConfig(req SaveAppConfigRequest) (map[string]interface{}, error) { + if a.configAPI == nil { + return nil, fmt.Errorf("配置服务正在初始化中") } - entries := bin.ListEntries() - result := make([]map[string]interface{}, len(entries)) - - for i, entry := range entries { - result[i] = map[string]interface{}{ - "original_path": entry.OriginalPath, - "deleted_path": entry.DeletedPath, - "deleted_time": entry.DeletedTime.Format("2006-01-02 15:04:05"), - "size": entry.Size, - "is_directory": entry.IsDirectory, + // 保存前检查是否有新启用的模块,需要动态初始化 + 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 } -// RestoreFromRecycleBin 从回收站恢复文件 -func (a *App) RestoreFromRecycleBin(recyclePath string) error { - bin := filesystem.GetRecycleBin() - if bin == nil { - return fmt.Errorf("回收站未初始化") +// handleNewlyEnabledModules 处理新启用的模块 +func (a *App) handleNewlyEnabledModules(oldTabs, newTabs []string) { + newlyEnabled := common.Difference(newTabs, oldTabs) + + if len(newlyEnabled) == 0 { + return } - return bin.RestoreFromRecycleBin(recyclePath) + fmt.Printf("[模块] 检测到新启用的模块: %v\n", newlyEnabled) + + for _, tab := range newlyEnabled { + switch tab { + case common.TabDatabase: + a.initDatabaseModule() + case common.TabFileSystem: + a.initFilesystemModule() + case common.TabDevice: + fmt.Println("[模块] 设备测试模块已启用") + } + } } -// DeletePermanently 永久删除回收站中的文件 -func (a *App) DeletePermanently(recyclePath string) error { - bin := filesystem.GetRecycleBin() - if bin == nil { - return fmt.Errorf("回收站未初始化") +// initDatabaseModule 延迟初始化数据库模块 +func (a *App) initDatabaseModule() { + if a.connectionAPI != nil { + fmt.Println("[模块] 数据库模块已初始化,跳过") + return } - return bin.DeletePermanently(recyclePath) -} + fmt.Println("[模块] 延迟初始化数据库模块...") + var err error -// EmptyRecycleBin 清空回收站 -func (a *App) EmptyRecycleBin() error { - bin := filesystem.GetRecycleBin() - if bin == nil { - return fmt.Errorf("回收站未初始化") + // 初始化 ConnectionAPI + if a.connectionAPI, err = api.NewConnectionAPI(); err != nil { + fmt.Printf("[模块] 数据库模块初始化失败: %v\n", err) + return } - return bin.Empty() + // 初始化 SqlAPI + if a.sqlAPI, err = api.NewSqlAPI(); err != nil { + fmt.Printf("[模块] SqlAPI 初始化失败: %v\n", err) + return + } + + // 初始化 TabAPI + if a.tabAPI, err = api.NewTabAPI(); err != nil { + fmt.Printf("[模块] TabAPI 初始化失败: %v\n", err) + return + } + + 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("[模块] 文件系统模块初始化完成") } diff --git a/internal/api/config_api.go b/internal/api/config_api.go new file mode 100644 index 0000000..6519da1 --- /dev/null +++ b/internal/api/config_api.go @@ -0,0 +1,137 @@ +package api + +import ( + "fmt" + "u-desk/internal/service" +) + +// ConfigAPI 配置 API +type ConfigAPI struct { + configService *service.ConfigService +} + +// NewConfigAPI 创建配置 API 实例 +func NewConfigAPI() (*ConfigAPI, error) { + configService, err := service.NewConfigService() + if err != nil { + return nil, err + } + + return &ConfigAPI{ + configService: configService, + }, nil +} + +// GetAppConfigResponse 获取应用配置响应 +type GetAppConfigResponse struct { + Tabs []AppTabDefinition `json:"tabs"` + VisibleTabs []string `json:"visibleTabs"` + DefaultTab string `json:"defaultTab"` +} + +// AppTabDefinition 应用 Tab 定义(前端格式) +type AppTabDefinition struct { + Key string `json:"key"` + Title string `json:"title"` + Visible bool `json:"visible"` + Enabled bool `json:"enabled"` +} + +// SaveAppConfigRequest 保存应用配置请求(前端格式) +type SaveAppConfigRequest struct { + Tabs []AppTabDefinition `json:"tabs"` + VisibleTabs []string `json:"visibleTabs"` + DefaultTab string `json:"defaultTab"` +} + +// GetAppConfig 获取应用配置 +func (api *ConfigAPI) GetAppConfig() (map[string]interface{}, error) { + tabConfig, err := api.configService.GetTabConfig() + if err != nil { + return map[string]interface{}{ + "success": false, + "message": fmt.Sprintf("获取配置失败: %v", err), + }, err + } + + // 转换为前端格式 + tabs := make([]AppTabDefinition, len(tabConfig.AvailableTabs)) + visibleTabSet := make(map[string]bool) + for _, key := range tabConfig.VisibleTabs { + visibleTabSet[key] = true + } + + for i, tab := range tabConfig.AvailableTabs { + tabs[i] = AppTabDefinition{ + Key: tab.Key, + Title: tab.Title, + Visible: visibleTabSet[tab.Key], + Enabled: tab.Enabled, + } + } + + return map[string]interface{}{ + "success": true, + "data": GetAppConfigResponse{ + Tabs: tabs, + VisibleTabs: tabConfig.VisibleTabs, + DefaultTab: tabConfig.DefaultTab, + }, + }, nil +} + +// SaveAppConfig 保存应用配置 +func (api *ConfigAPI) SaveAppConfig(req SaveAppConfigRequest) (map[string]interface{}, error) { + // 验证:至少保留一个可见 Tab + if len(req.VisibleTabs) < 1 { + return map[string]interface{}{ + "success": false, + "message": "至少需要保留一个可见的 Tab", + }, fmt.Errorf("至少需要保留一个可见的 Tab") + } + + // 验证:默认 Tab 必须在可见列表中 + defaultTabExists := false + for _, key := range req.VisibleTabs { + if key == req.DefaultTab { + defaultTabExists = true + break + } + } + if !defaultTabExists { + return map[string]interface{}{ + "success": false, + "message": "默认 Tab 必须在可见列表中", + }, fmt.Errorf("默认 Tab 必须在可见列表中") + } + + // 转换为服务层格式 + availableTabs := make([]service.TabDefinition, len(req.Tabs)) + for i, tab := range req.Tabs { + availableTabs[i] = service.TabDefinition{ + Key: tab.Key, + Title: tab.Title, + Enabled: tab.Enabled, + } + } + + tabConfig := &service.TabConfig{ + AvailableTabs: availableTabs, + VisibleTabs: req.VisibleTabs, + DefaultTab: req.DefaultTab, + } + + // 保存配置 + if err := api.configService.SaveTabConfig(tabConfig); err != nil { + return map[string]interface{}{ + "success": false, + "message": fmt.Sprintf("保存配置失败: %v", err), + }, err + } + + return map[string]interface{}{ + "success": true, + "message": "配置保存成功", + "data": nil, + }, nil +} diff --git a/internal/common/constants.go b/internal/common/constants.go new file mode 100644 index 0000000..29df0bb --- /dev/null +++ b/internal/common/constants.go @@ -0,0 +1,17 @@ +package common + +// Default visible tabs configuration +const ( + // TabDatabase 数据库管理 Tab + TabDatabase = "db-cli" + // TabFileSystem 文件系统 Tab + TabFileSystem = "file-system" + // TabDevice 设备测试 Tab + TabDevice = "device" +) + +// DefaultVisibleTabs 默认可见的 Tabs +var DefaultVisibleTabs = []string{TabDatabase, TabFileSystem, TabDevice} + +// DefaultTab 默认打开的 Tab +const DefaultTab = TabDatabase diff --git a/internal/common/path.go b/internal/common/path.go new file mode 100644 index 0000000..ab50513 --- /dev/null +++ b/internal/common/path.go @@ -0,0 +1,45 @@ +package common + +import ( + "os" + "path/filepath" + "runtime" +) + +const ( + // AppName 应用名称 + AppName = "u-desk" +) + +// GetUserDataDir 获取用户数据目录 +// 跨平台支持:Windows、macOS、Linux +func GetUserDataDir() string { + var basePath string + + switch runtime.GOOS { + case "windows": + // Windows: %LOCALAPPDATA% 或 %APPDATA% + basePath = os.Getenv("LOCALAPPDATA") + if basePath == "" { + basePath = os.Getenv("APPDATA") + } + case "darwin": + // macOS: ~/Library/Application Support + homeDir, err := os.UserHomeDir() + if err == nil { + basePath = filepath.Join(homeDir, "Library", "Application Support") + } + default: + // Linux: ~/.config + homeDir, err := os.UserHomeDir() + if err == nil { + basePath = filepath.Join(homeDir, ".config") + } + } + + if basePath == "" { + basePath = "." + } + + return filepath.Join(basePath, AppName) +} diff --git a/internal/common/utils.go b/internal/common/utils.go index 55bac7c..e7009d5 100644 --- a/internal/common/utils.go +++ b/internal/common/utils.go @@ -4,6 +4,17 @@ import ( "fmt" ) +// InterfaceSliceToStringSlice 将 []interface{} 安全转换为 []string +func InterfaceSliceToStringSlice(slice []interface{}) []string { + result := make([]string, 0, len(slice)) + for _, v := range slice { + if str, ok := v.(string); ok && str != "" { + result = append(result, str) + } + } + return result +} + // FormatBytes 格式化字节大小为人类可读格式 // 例如: 1024 → "1.00 KB", 1048576 → "1.00 MB" func FormatBytes(bytes uint64) string { @@ -18,3 +29,28 @@ func FormatBytes(bytes uint64) string { } return fmt.Sprintf("%.2f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) } + +// Contains 检查切片是否包含元素 +func Contains[T comparable](slice []T, item T) bool { + for _, s := range slice { + if s == item { + return true + } + } + return false +} + +// Difference 返回在 a 中但不在 b 中的元素 +func Difference[T comparable](a, b []T) []T { + mb := make(map[T]struct{}, len(b)) + for _, x := range b { + mb[x] = struct{}{} + } + var diff []T + for _, x := range a { + if _, found := mb[x]; !found { + diff = append(diff, x) + } + } + return diff +} diff --git a/internal/filesystem/errors.go b/internal/filesystem/errors.go index fb2878d..c262603 100644 --- a/internal/filesystem/errors.go +++ b/internal/filesystem/errors.go @@ -2,6 +2,7 @@ package filesystem import ( "fmt" + "os" "runtime" ) @@ -128,3 +129,15 @@ func GetStackTrace(skip int) string { } return "" } + +// DeleteRestrictionWarning 删除限制警告 +// 用于在删除受限文件时提供详细的警告信息 +type DeleteRestrictionWarning struct { + Path string + Details string + Info os.FileInfo +} + +func (w *DeleteRestrictionWarning) Error() string { + return fmt.Sprintf("删除限制警告: %s\n%s", w.Path, w.Details) +} diff --git a/internal/filesystem/fs.go b/internal/filesystem/fs.go index 8f70fe2..ba7e99d 100644 --- a/internal/filesystem/fs.go +++ b/internal/filesystem/fs.go @@ -2,246 +2,101 @@ package filesystem import ( "fmt" - "os" "os/exec" "path/filepath" "runtime" "time" ) -// 在包级别存储审计日志记录器 -var auditLogger *AuditLogger +// ========== 向后兼容的全局函数包装器 ========== +// 这些函数提供向后兼容性,内部委托给 FileSystemService +// 新代码应该使用 FileSystemService 而不是这些全局函数 -// InitAudit 初始化文件系统模块(包括审计日志) -func InitAudit(logDir string) error { - logger, err := NewAuditLogger(logDir) - if err != nil { - return err - } - auditLogger = logger - return nil -} - -// CloseAudit 关闭审计日志 -func CloseAudit() error { - if auditLogger != nil { - return auditLogger.Close() - } - return nil -} - -// formatBytes 格式化字节大小为人类可读格式 -func formatBytes(bytes int64) string { - const unit = 1024 - if bytes < unit { - return fmt.Sprintf("%d B", bytes) - } - div, exp := int64(unit), 0 - for n := bytes / unit; n >= unit; n /= unit { - div *= unit - exp++ - } - return fmt.Sprintf("%.2f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) -} - -// ReadFile 读取文件内容 +// ReadFile 读取文件内容(向后兼容包装器) func ReadFile(path string) (string, error) { - if !isSafePath(path) { - return "", fmt.Errorf("路径不安全") - } - - data, err := os.ReadFile(path) + service, err := GetGlobalService() if err != nil { - return "", fmt.Errorf("读取文件失败: %v", err) + return "", fmt.Errorf("服务未初始化: %v", err) } - - return string(data), nil + return service.ReadFile(path) } -// WriteFile 写入文件 +// WriteFile 写入文件(向后兼容包装器) func WriteFile(path, content string) error { - if !isSafePath(path) { - return fmt.Errorf("路径不安全") + service, err := GetGlobalService() + if err != nil { + return fmt.Errorf("服务未初始化: %v", err) } - - dir := filepath.Dir(path) - if err := os.MkdirAll(dir, 0755); err != nil { - return fmt.Errorf("创建目录失败: %v", err) - } - - if err := os.WriteFile(path, []byte(content), 0644); err != nil { - return fmt.Errorf("写入文件失败: %v", err) - } - - return nil + return service.WriteFile(path, content) } -// ListDir 列出目录内容 +// ListDir 列出目录内容(向后兼容包装器) func ListDir(path string) ([]map[string]interface{}, error) { - if !isSafePath(path) { - return nil, fmt.Errorf("路径不安全") - } - - entries, err := os.ReadDir(path) + service, err := GetGlobalService() if err != nil { - return nil, fmt.Errorf("读取目录失败: %v", err) + return nil, fmt.Errorf("服务未初始化: %v", err) } - - result := []map[string]interface{}{} - for _, entry := range entries { - info, err := entry.Info() - if err != nil { - continue - } - - fullPath := filepath.Join(path, entry.Name()) - result = append(result, map[string]interface{}{ - "name": entry.Name(), - "path": fullPath, - "is_dir": entry.IsDir(), - "size": info.Size(), - "mod_time": info.ModTime().Format("2006-01-02 15:04:05"), - }) - } - - return result, nil + return service.ListDir(path) } -// CreateDir 创建目录 +// CreateDir 创建目录(向后兼容包装器) func CreateDir(path string) error { - if !isSafePath(path) { - return fmt.Errorf("路径不安全") + service, err := GetGlobalService() + if err != nil { + return fmt.Errorf("服务未初始化: %v", err) } - - if err := os.MkdirAll(path, 0755); err != nil { - return fmt.Errorf("创建目录失败: %v", err) - } - - return nil + return service.CreateDir(path) } -// CreateFile 创建空文件 +// CreateFile 创建空文件(向后兼容包装器) func CreateFile(path string) error { - if !isSafePath(path) { - return fmt.Errorf("路径不安全") - } - - // 检查文件是否已存在 - if _, err := os.Stat(path); err == nil { - return fmt.Errorf("文件已存在") - } - - // 创建文件(如果父目录不存在,会自动创建) - file, err := os.Create(path) + service, err := GetGlobalService() if err != nil { - return fmt.Errorf("创建文件失败: %v", err) + return fmt.Errorf("服务未初始化: %v", err) } - file.Close() - - return nil + return service.CreateFile(path) } -// DeletePath 删除文件或目录 -// 优化:使用配置驱动的安全检查,支持确认机制 +// DeletePath 删除文件或目录(向后兼容包装器) func DeletePath(path string) error { - // 使用默认配置 - return DeletePathWithConfig(path, DefaultConfig()) + service, err := GetGlobalService() + if err != nil { + return fmt.Errorf("服务未初始化: %v", err) + } + return service.DeletePath(path) } -// DeletePathWithConfig 使用指定配置删除文件或目录 -// 支持配置化的安全策略和确认机制 +// DeletePathWithConfig 使用指定配置删除文件或目录(向后兼容包装器) func DeletePathWithConfig(path string, config *Config) error { - // 1. 路径安全检查 - validator := NewPathValidator(config) - if err := validator.Validate(path); err != nil && err.IsError { - return fmt.Errorf("路径验证失败: %w", err) - } - - // 2. 获取文件信息 - info, err := os.Stat(path) + service, err := GetGlobalService() if err != nil { - if os.IsNotExist(err) { - return fmt.Errorf("文件或目录不存在") - } - return fmt.Errorf("获取文件信息失败: %v", err) + return fmt.Errorf("服务未初始化: %v", err) } - // 3. 检查删除限制(配置驱动) - exceeds, details, checkErr := CheckDeleteRestrictions(path, info, config) - if checkErr != nil { - return checkErr - } + // 临时替换服务的配置 + originalConfig := service.config + service.config = config + defer func() { service.config = originalConfig }() - if exceeds { - // 根据配置决定是拒绝还是需要确认 - if config.Security.DeleteRestrictions.RequireConfirm { - // TODO: 这里应该触发前端确认对话框 - // 目前暂时返回警告信息,由前端处理 - return &DeleteRestrictionWarning{ - Path: path, - Details: details, - Info: info, - } - } - // 不需要确认,直接拒绝 - return fmt.Errorf("删除限制: %s", details) - } - - // 4. 执行删除操作 - if info.IsDir() { - if err := os.RemoveAll(path); err != nil { - return fmt.Errorf("删除目录失败: %v", err) - } - } else { - if err := os.Remove(path); err != nil { - return fmt.Errorf("删除文件失败: %v", err) - } - } - - return nil + return service.DeletePath(path) } -// DeleteRestrictionWarning 删除限制警告 -// 用于前端显示确认对话框 -type DeleteRestrictionWarning struct { - Path string - Details string - Info os.FileInfo -} - -func (w *DeleteRestrictionWarning) Error() string { - return fmt.Sprintf("删除限制警告: %s\n%s", w.Path, w.Details) -} - -// GetFileInfo 获取文件信息 +// GetFileInfo 获取文件信息(向后兼容包装器) func GetFileInfo(path string) (map[string]interface{}, error) { - if !isSafePath(path) { - return nil, fmt.Errorf("路径不安全") - } - - info, err := os.Stat(path) + service, err := GetGlobalService() if err != nil { - if os.IsNotExist(err) { - return nil, fmt.Errorf("文件或目录不存在") - } - return nil, fmt.Errorf("获取文件信息失败: %v", err) + return nil, fmt.Errorf("服务未初始化: %v", err) } - - return map[string]interface{}{ - "name": info.Name(), - "path": path, - "size": info.Size(), - "size_str": formatBytes(info.Size()), - "is_dir": info.IsDir(), - "mod_time": info.ModTime().Format("2006-01-02 15:04:05"), - "mode": info.Mode().String(), - }, nil + return service.GetFileInfo(path) } // OpenPath 打开文件或目录(使用系统默认程序) +// 这是一个核心工具函数,保留为独立函数 func OpenPath(path string) error { - if !isSafePath(path) { - return fmt.Errorf("路径不安全") + // 使用 path.validator 进行验证 + validator := NewPathValidator(DefaultConfig()) + if err := validator.Validate(path); err != nil && err.IsError { + return fmt.Errorf("路径不安全: %w", err) } path = filepath.Clean(path) @@ -276,3 +131,28 @@ func OpenPath(path string) error { return nil } + +// RenamePath 重命名文件或目录(向后兼容包装器) +func RenamePath(oldPath, newPath string) error { + service, err := GetGlobalService() + if err != nil { + return fmt.Errorf("服务未初始化: %v", err) + } + return service.RenamePath(oldPath, newPath) +} + +// ========== 辅助函数 ========== + +// formatBytes 格式化字节大小为人类可读格式 +func formatBytes(bytes int64) string { + const unit = 1024 + if bytes < unit { + return fmt.Sprintf("%d B", bytes) + } + div, exp := int64(unit), 0 + for n := bytes / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.2f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) +} diff --git a/internal/filesystem/service.go b/internal/filesystem/service.go index affb4c7..4aae42b 100644 --- a/internal/filesystem/service.go +++ b/internal/filesystem/service.go @@ -9,6 +9,8 @@ import ( "runtime" "sync" "time" + + "u-desk/internal/common" ) // FileSystemService 文件系统服务 @@ -77,30 +79,22 @@ func (s *FileSystemService) initializeComponents() error { // initAuditLogger 初始化审计日志 func (s *FileSystemService) initAuditLogger() error { - // 获取日志目录 - userDataDir := getUserDataDir() - logDir := filepath.Join(userDataDir, "logs") - + logDir := filepath.Join(common.GetUserDataDir(), "logs") logger, err := NewAuditLogger(logDir) if err != nil { return err } - s.auditLogger = logger return nil } // initRecycleBin 初始化回收站 func (s *FileSystemService) initRecycleBin() error { - // 获取回收站目录 - userDataDir := getUserDataDir() - recycleBinPath := filepath.Join(userDataDir, "recycle_bin") - + recycleBinPath := filepath.Join(common.GetUserDataDir(), "recycle_bin") bin, err := NewRecycleBin(recycleBinPath) if err != nil { return err } - s.recycleBin = bin return nil } @@ -125,11 +119,7 @@ func (s *FileSystemService) ReadFile(path string) (string, error) { return "", fmt.Errorf("读取文件失败: %v", err) } - // 记录审计日志 - if s.auditLogger != nil { - s.auditLogger.LogRead(path, int64(len(data)), nil) - } - + s.logRead(path, int64(len(data)), nil) return string(data), nil } @@ -154,18 +144,11 @@ func (s *FileSystemService) WriteFile(path, content string) error { // 写入文件 data := []byte(content) if err := os.WriteFile(path, data, DefaultFilePermissions); err != nil { - // 记录审计日志 - if s.auditLogger != nil { - s.auditLogger.LogWrite(path, int64(len(data)), err) - } + s.logWrite(path, int64(len(data)), err) return fmt.Errorf("写入文件失败: %v", err) } - // 记录审计日志 - if s.auditLogger != nil { - s.auditLogger.LogWrite(path, int64(len(data)), nil) - } - + s.logWrite(path, int64(len(data)), nil) return nil } @@ -247,10 +230,7 @@ func (s *FileSystemService) DeletePathWithContext(ctx context.Context, path stri deleteErr = os.Remove(path) } - // 记录审计日志 - if s.auditLogger != nil { - s.auditLogger.LogDelete(path, info.IsDir(), info.Size(), deleteErr) - } + s.logDelete(path, info.IsDir(), info.Size(), deleteErr) if deleteErr != nil { return fmt.Errorf("删除失败: %v", deleteErr) @@ -301,16 +281,13 @@ func (s *FileSystemService) ListDir(path string) ([]map[string]interface{}, erro }) } - // 记录审计日志 - if s.auditLogger != nil { - s.auditLogger.Log(AuditLogEntry{ - Timestamp: getCurrentTimestamp(), - Operation: OperationList, - Path: path, - IsDirectory: true, - Success: true, - }) - } + s.logAudit(AuditLogEntry{ + Timestamp: getCurrentTimestamp(), + Operation: OperationList, + Path: path, + IsDirectory: true, + Success: true, + }) return result, nil } @@ -325,16 +302,13 @@ func (s *FileSystemService) CreateDir(path string) error { return fmt.Errorf("创建目录失败: %v", err) } - // 记录审计日志 - if s.auditLogger != nil { - s.auditLogger.Log(AuditLogEntry{ - Timestamp: getCurrentTimestamp(), - Operation: OperationCreate, - Path: path, - IsDirectory: true, - Success: true, - }) - } + s.logAudit(AuditLogEntry{ + Timestamp: getCurrentTimestamp(), + Operation: OperationCreate, + Path: path, + IsDirectory: true, + Success: true, + }) return nil } @@ -356,16 +330,13 @@ func (s *FileSystemService) CreateFile(path string) error { } file.Close() - // 记录审计日志 - if s.auditLogger != nil { - s.auditLogger.Log(AuditLogEntry{ - Timestamp: getCurrentTimestamp(), - Operation: OperationCreate, - Path: path, - IsDirectory: false, - Success: true, - }) - } + s.logAudit(AuditLogEntry{ + Timestamp: getCurrentTimestamp(), + Operation: OperationCreate, + Path: path, + IsDirectory: false, + Success: true, + }) return nil } @@ -409,6 +380,34 @@ func (s *FileSystemService) OpenPath(path string) error { return OpenPath(path) } +// RenamePath 重命名文件或目录 +func (s *FileSystemService) RenamePath(oldPath, newPath string) error { + // 验证旧路径 + if err := s.validatePath(oldPath); err != nil { + return err + } + + // 验证新路径 + if err := s.validatePath(newPath); err != nil { + return err + } + + // 执行重命名 + if err := os.Rename(oldPath, newPath); err != nil { + return fmt.Errorf("重命名失败: %v", err) + } + + s.logAudit(AuditLogEntry{ + Timestamp: getCurrentTimestamp(), + Operation: OperationRename, + Path: newPath, + OldPath: oldPath, + Success: true, + }) + + return nil +} + // ========== ZIP操作接口 ========== // ListZip 列出ZIP文件内容 @@ -416,16 +415,31 @@ func (s *FileSystemService) ListZip(zipPath string) ([]map[string]interface{}, e return ListZipContents(zipPath) } +// ListZipContents 列出ZIP文件内容(别名,保持向后兼容) +func (s *FileSystemService) ListZipContents(zipPath string) ([]map[string]interface{}, error) { + return ListZipContents(zipPath) +} + // ExtractZipFile 从ZIP提取文件内容 func (s *FileSystemService) ExtractZipFile(zipPath, filePath string) (string, error) { return ExtractFileFromZip(zipPath, filePath) } +// ExtractFileFromZip 从ZIP提取文件内容(别名,保持向后兼容) +func (s *FileSystemService) ExtractFileFromZip(zipPath, filePath string) (string, error) { + return ExtractFileFromZip(zipPath, filePath) +} + // ExtractZipFileToTemp 从ZIP提取文件到临时目录 func (s *FileSystemService) ExtractZipFileToTemp(zipPath, filePath string) (string, error) { return ExtractFileFromZipToTemp(zipPath, filePath) } +// ExtractFileFromZipToTemp 从ZIP提取文件到临时目录(别名,保持向后兼容) +func (s *FileSystemService) ExtractFileFromZipToTemp(zipPath, filePath string) (string, error) { + return ExtractFileFromZipToTemp(zipPath, filePath) +} + // GetZipFileInfo 获取ZIP文件信息 func (s *FileSystemService) GetZipFileInfo(zipPath, filePath string) (map[string]interface{}, error) { return GetZipFileInfo(zipPath, filePath) @@ -433,31 +447,6 @@ func (s *FileSystemService) GetZipFileInfo(zipPath, filePath string) (map[string // ========== 辅助函数 ========== -// getUserDataDir 获取用户数据目录 -func getUserDataDir() string { - var basePath string - - switch runtime.GOOS { - case "windows": - basePath = os.Getenv("LOCALAPPDATA") - if basePath == "" { - basePath = os.Getenv("APPDATA") - } - case "darwin": - homeDir, _ := os.UserHomeDir() - basePath = filepath.Join(homeDir, "Library", "Application Support") - default: - homeDir, _ := os.UserHomeDir() - basePath = filepath.Join(homeDir, ".config") - } - - if basePath == "" { - basePath = "." - } - - return filepath.Join(basePath, "u-desk") -} - // getCurrentTimestamp 获取当前时间戳 func getCurrentTimestamp() time.Time { return time.Now() @@ -465,9 +454,7 @@ func getCurrentTimestamp() time.Time { // isInRecycleBin 检查路径是否在回收站中 func isInRecycleBin(path string) bool { - // 简化版本:检查路径是否包含回收站目录名 - userDataDir := getUserDataDir() - recycleBinPath := filepath.Join(userDataDir, "recycle_bin") + recycleBinPath := filepath.Join(common.GetUserDataDir(), "recycle_bin") return filepath.HasPrefix(filepath.Clean(path), filepath.Clean(recycleBinPath)) } @@ -497,6 +484,163 @@ func (s *FileSystemService) GetRecycleBin() *RecycleBin { return s.recycleBin } +// ========== 审计日志接口 ========== + +// logAudit 安全记录审计日志(自动处理 nil 检查) +func (s *FileSystemService) logAudit(entry AuditLogEntry) { + if s.auditLogger != nil { + s.auditLogger.Log(entry) + } +} + +// logRead 记录读取操作审计日志 +func (s *FileSystemService) logRead(path string, size int64, err error) { + if s.auditLogger != nil { + s.auditLogger.LogRead(path, size, err) + } +} + +// logWrite 记录写入操作审计日志 +func (s *FileSystemService) logWrite(path string, size int64, err error) { + if s.auditLogger != nil { + s.auditLogger.LogWrite(path, size, err) + } +} + +// logDelete 记录删除操作审计日志 +func (s *FileSystemService) logDelete(path string, isDir bool, size int64, err error) { + if s.auditLogger != nil { + s.auditLogger.LogDelete(path, isDir, size, err) + } +} + +// GetAuditLogs 获取审计日志 +func (s *FileSystemService) GetAuditLogs(limit int) ([]map[string]interface{}, error) { + if s.auditLogger == nil { + return []map[string]interface{}{}, nil + } + + logDir := filepath.Join(common.GetUserDataDir(), "logs") + entries, err := GetRecentLogs(logDir, limit) + if err != nil { + return nil, err + } + + result := make([]map[string]interface{}, len(entries)) + for i, entry := range entries { + result[i] = map[string]interface{}{ + "timestamp": entry.Timestamp.Format("2006-01-02 15:04:05"), + "operation": entry.Operation, + "path": entry.Path, + "size": entry.Size, + "is_directory": entry.IsDirectory, + "success": entry.Success, + "error": entry.Error, + } + } + + return result, nil +} + +// ========== 回收站接口 ========== + +// GetRecycleBinEntries 获取回收站条目 +func (s *FileSystemService) GetRecycleBinEntries() ([]map[string]interface{}, error) { + if s.recycleBin == nil { + return []map[string]interface{}{}, nil + } + + entries := s.recycleBin.ListEntries() + result := make([]map[string]interface{}, len(entries)) + + for i, entry := range entries { + result[i] = map[string]interface{}{ + "original_path": entry.OriginalPath, + "deleted_path": entry.DeletedPath, + "deleted_time": entry.DeletedTime.Format("2006-01-02 15:04:05"), + "size": entry.Size, + "is_directory": entry.IsDirectory, + } + } + + return result, nil +} + +// RestoreFromRecycleBin 从回收站恢复文件 +func (s *FileSystemService) RestoreFromRecycleBin(recyclePath string) error { + if s.recycleBin == nil { + return fmt.Errorf("回收站未初始化") + } + return s.recycleBin.RestoreFromRecycleBin(recyclePath) +} + +// DeletePermanently 永久删除回收站中的文件 +func (s *FileSystemService) DeletePermanently(recyclePath string) error { + if s.recycleBin == nil { + return fmt.Errorf("回收站未初始化") + } + return s.recycleBin.DeletePermanently(recyclePath) +} + +// EmptyRecycleBin 清空回收站 +func (s *FileSystemService) EmptyRecycleBin() error { + if s.recycleBin == nil { + return fmt.Errorf("回收站未初始化") + } + return s.recycleBin.Empty() +} + +// ResolveShortcut 解析快捷方式(.lnk)文件,返回目标路径 +func (s *FileSystemService) ResolveShortcut(lnkPath string) (targetPath string, err error) { + // 验证路径 + if err := s.validatePath(lnkPath); err != nil { + return "", fmt.Errorf("路径验证失败: %w", err) + } + + // 检查文件扩展名 + if filepath.Ext(lnkPath) != ".lnk" { + return "", fmt.Errorf("不是快捷方式文件") + } + + // 检查文件是否存在 + if _, err := os.Stat(lnkPath); os.IsNotExist(err) { + return "", fmt.Errorf("快捷方式文件不存在") + } + + // 使用 Windows PowerShell 解析 lnk 文件 + // 这种方法更可靠,不需要依赖第三方库 + if runtime.GOOS == "windows" { + // 创建 PowerShell 脚本 + psScript := fmt.Sprintf( + "$shell = New-Object -ComObject WScript.Shell; "+ + "$shortcut = $shell.CreateShortcut('%s'); "+ + "$shortcut.TargetPath", + lnkPath, + ) + + // 执行 PowerShell 命令 + cmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", psScript) + output, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("解析快捷方式失败: %w", err) + } + + // 去除空白字符 + targetPath = string(output) + targetPath = filepath.Clean(targetPath) + + // 如果目标路径为空,返回错误 + if targetPath == "" || targetPath == "." { + return "", fmt.Errorf("快捷方式目标路径为空") + } + + return targetPath, nil + } + + // 非 Windows 系统暂不支持 + return "", fmt.Errorf("当前系统不支持快捷方式解析") +} + // Close 关闭服务,释放资源 func (s *FileSystemService) Close(ctx context.Context) error { s.mu.Lock() diff --git a/internal/filesystem/service_interfaces.go b/internal/filesystem/service_interfaces.go index fae6daa..86ca1ee 100644 --- a/internal/filesystem/service_interfaces.go +++ b/internal/filesystem/service_interfaces.go @@ -17,6 +17,9 @@ type FileService interface { GetInfo(path string) (map[string]interface{}, error) Open(path string) error + // 快捷方式 + ResolveShortcut(lnkPath string) (targetPath string, err error) + // 配置 GetConfig() *Config Close(ctx context.Context) error diff --git a/internal/service/config_service.go b/internal/service/config_service.go new file mode 100644 index 0000000..b5085e8 --- /dev/null +++ b/internal/service/config_service.go @@ -0,0 +1,118 @@ +package service + +import ( + "encoding/json" + "fmt" + "u-desk/internal/storage" + "u-desk/internal/storage/models" + + "gorm.io/gorm" +) + +// ConfigService 配置服务 +type ConfigService struct { + db *gorm.DB +} + +// NewConfigService 创建配置服务实例 +func NewConfigService() (*ConfigService, error) { + db, err := storage.InitFast() + if err != nil { + return nil, fmt.Errorf("数据库初始化失败: %w", err) + } + + return &ConfigService{db: db}, nil +} + +// TabDefinition Tab 定义 +type TabDefinition struct { + Key string `json:"key"` + Title string `json:"title"` + Enabled bool `json:"enabled"` +} + +// TabConfig Tab 配置 +type TabConfig struct { + AvailableTabs []TabDefinition `json:"available_tabs"` + VisibleTabs []string `json:"visible_tabs"` + DefaultTab string `json:"default_tab"` +} + +// 默认 Tab 配置 +var defaultTabConfig = TabConfig{ + AvailableTabs: []TabDefinition{ + {Key: "db-cli", Title: "数据库", Enabled: true}, + {Key: "file-system", Title: "文件管理", Enabled: true}, + {Key: "device", Title: "设备调用测试", Enabled: true}, + }, + VisibleTabs: []string{"db-cli", "file-system", "device"}, + DefaultTab: "db-cli", +} + +const ( + tabConfigKey = "tab_config" +) + +// GetTabConfig 获取 Tab 配置 +func (s *ConfigService) GetTabConfig() (*TabConfig, error) { + var config models.AppConfig + + // 查询配置 + err := s.db.Where("`key` = ?", tabConfigKey).First(&config).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + // 不存在配置,返回默认配置 + return &defaultTabConfig, nil + } + return nil, fmt.Errorf("查询配置失败: %w", err) + } + + // 解析 JSON + var tabConfig TabConfig + if err := json.Unmarshal([]byte(config.Value), &tabConfig); err != nil { + // 解析失败,返回默认配置 + return &defaultTabConfig, nil + } + + // 验证配置完整性 + if len(tabConfig.AvailableTabs) == 0 || len(tabConfig.VisibleTabs) == 0 { + return &defaultTabConfig, nil + } + + return &tabConfig, nil +} + +// SaveTabConfig 保存 Tab 配置 +func (s *ConfigService) SaveTabConfig(config *TabConfig) error { + // 序列化为 JSON + jsonData, err := json.Marshal(config) + if err != nil { + return fmt.Errorf("序列化配置失败: %w", err) + } + + // 查询是否存在配置 + var existingConfig models.AppConfig + err = s.db.Where("`key` = ?", tabConfigKey).First(&existingConfig).Error + + if err == gorm.ErrRecordNotFound { + // 不存在,创建新配置 + newConfig := models.AppConfig{ + Key: tabConfigKey, + Value: string(jsonData), + Description: "Tab 显示和排序配置", + } + if err := s.db.Create(&newConfig).Error; err != nil { + return fmt.Errorf("创建配置失败: %w", err) + } + } else if err != nil { + return fmt.Errorf("查询配置失败: %w", err) + } else { + // 存在,更新配置 + existingConfig.Value = string(jsonData) + if err := s.db.Save(&existingConfig).Error; err != nil { + return fmt.Errorf("更新配置失败: %w", err) + } + } + + return nil +} diff --git a/internal/service/update.go b/internal/service/update.go index 8073c4d..d63b755 100644 --- a/internal/service/update.go +++ b/internal/service/update.go @@ -10,6 +10,7 @@ import ( "os/exec" "path/filepath" "runtime" + "strings" "time" ) @@ -27,6 +28,7 @@ type RemoteVersionInfo struct { Changelog string `json:"changelog"` ForceUpdate bool `json:"force_update"` ReleaseDate string `json:"release_date"` + FileSize int64 `json:"file_size"` } // UpdateCheckResult 更新检查结果 @@ -38,6 +40,7 @@ type UpdateCheckResult struct { Changelog string `json:"changelog"` ForceUpdate bool `json:"force_update"` ReleaseDate string `json:"release_date"` + FileSize int64 `json:"file_size"` } // InstallResult 安装结果 @@ -81,8 +84,13 @@ func (s *UpdateService) CheckUpdate() (*UpdateCheckResult, error) { return nil, fmt.Errorf("获取远程版本信息失败: %v", err) } - log.Printf("[更新检查] 远程版本信息: 版本=%s, 下载地址=%s, 强制更新=%v", - remoteInfo.Version, remoteInfo.DownloadURL, remoteInfo.ForceUpdate) + log.Printf("[更新检查] 远程版本信息: 版本=%s, 下载地址=%s, 强制更新=%v, 更新日志长度=%d", + remoteInfo.Version, remoteInfo.DownloadURL, remoteInfo.ForceUpdate, len(remoteInfo.Changelog)) + if remoteInfo.Changelog != "" { + log.Printf("[更新检查] 更新日志内容: %s", remoteInfo.Changelog) + } else { + log.Printf("[更新检查] 警告: 远程接口未返回更新日志") + } // 解析远程版本号 remoteVersion, err := ParseVersion(remoteInfo.Version) @@ -106,6 +114,7 @@ func (s *UpdateService) CheckUpdate() (*UpdateCheckResult, error) { Changelog: remoteInfo.Changelog, ForceUpdate: remoteInfo.ForceUpdate, ReleaseDate: remoteInfo.ReleaseDate, + FileSize: remoteInfo.FileSize, } log.Printf("[更新检查] 检查完成: 有更新=%v", hasUpdate) @@ -138,13 +147,24 @@ func (s *UpdateService) fetchRemoteVersionInfo() (*RemoteVersionInfo, error) { log.Printf("[远程版本] 请求远程版本信息: %s", s.checkURL) + // 添加时间戳参数防止缓存 + timestamp := time.Now().UnixMilli() // 使用毫秒级时间戳 + var requestURL string + if strings.Contains(s.checkURL, "?") { + requestURL = fmt.Sprintf("%s&t=%d", s.checkURL, timestamp) + } else { + requestURL = fmt.Sprintf("%s?t=%d", s.checkURL, timestamp) + } + + log.Printf("[远程版本] 实际请求URL: %s", requestURL) + // 创建 HTTP 客户端,设置超时 client := &http.Client{ Timeout: 10 * time.Second, } // 发送请求 - resp, err := client.Get(s.checkURL) + resp, err := client.Get(requestURL) if err != nil { log.Printf("[远程版本] 网络请求失败: %v", err) return nil, fmt.Errorf("网络请求失败: %v", err) diff --git a/internal/service/update_config.go b/internal/service/update_config.go index 18663ff..191f5d1 100644 --- a/internal/service/update_config.go +++ b/internal/service/update_config.go @@ -44,9 +44,9 @@ func LoadUpdateConfig() (*UpdateConfig, error) { if _, err := os.Stat(configPath); os.IsNotExist(err) { return &UpdateConfig{ CurrentVersion: GetCurrentVersion(), - LastCheckTime: time.Time{}, + LastCheckTime: time.Time{}, // 启动时会立即检查 AutoCheckEnabled: true, - CheckIntervalMinutes: 1, + CheckIntervalMinutes: 5, // 5分钟检查一次 CheckURL: "https://img.1216.top/u-desk/last-version.json", }, nil } diff --git a/internal/service/update_download.go b/internal/service/update_download.go index 7d0f223..77788b3 100644 --- a/internal/service/update_download.go +++ b/internal/service/update_download.go @@ -194,7 +194,8 @@ func DownloadUpdate(downloadURL string, progressCallback DownloadProgress) (*Dow if elapsed >= 0.3 { progress := float64(0) if contentLength > 0 { - progress = normalizeProgress(float64(totalDownloaded) / float64(contentLength) * 100) + rawProgress := float64(totalDownloaded) / float64(contentLength) * 100 + progress = normalizeProgress(rawProgress) } speed := float64(0) diff --git a/internal/service/version.go b/internal/service/version.go index 715091a..566f773 100644 --- a/internal/service/version.go +++ b/internal/service/version.go @@ -13,7 +13,7 @@ import ( // ==================== 常量定义 ==================== // AppVersion 应用版本号(发布时直接修改此处) -const AppVersion = "0.1.0" +const AppVersion = "0.2.0" // ==================== 类型定义 ==================== diff --git a/internal/storage/models/app_config.go b/internal/storage/models/app_config.go new file mode 100644 index 0000000..9af8515 --- /dev/null +++ b/internal/storage/models/app_config.go @@ -0,0 +1,18 @@ +package models + +import "time" + +// AppConfig 应用配置模型 +type AppConfig struct { + ID uint `gorm:"primaryKey" json:"id"` + Key string `gorm:"type:varchar(50);uniqueIndex;not null" json:"key"` + Value string `gorm:"type:text;not null" json:"value"` + Description string `gorm:"type:varchar(200)" json:"description"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// TableName 指定表名 +func (AppConfig) TableName() string { + return "app_config" +} diff --git a/internal/storage/sqlite.go b/internal/storage/sqlite.go index c819d95..c6f0020 100644 --- a/internal/storage/sqlite.go +++ b/internal/storage/sqlite.go @@ -64,6 +64,7 @@ func InitFast() (*gorm.DB, error) { &models.DbConnection{}, &models.SqlTab{}, &models.SqlResultHistory{}, + &models.AppConfig{}, ); err != nil { return nil, err } diff --git a/main.go b/main.go index 1299c4f..c1fa796 100644 --- a/main.go +++ b/main.go @@ -2,9 +2,6 @@ package main import ( "embed" - "os" - "path/filepath" - "runtime" "u-desk/internal/filesystem" @@ -17,9 +14,6 @@ import ( var assets embed.FS func main() { - // 🔒 初始化文件系统安全功能 - initFileSystemSecurity() - // 创建应用实例 app := NewApp() @@ -37,7 +31,7 @@ func main() { }, BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 1}, OnStartup: app.Startup, - OnShutdown: app.shutdown, + OnShutdown: app.Shutdown, Bind: []interface{}{ app, }, @@ -47,59 +41,3 @@ func main() { println("Error:", err.Error()) } } - -// initFileSystemSecurity 初始化文件系统安全功能 -// 优化:并发执行初始化操作,不阻塞启动 -func initFileSystemSecurity() { - // 获取用户数据目录 - userDataDir := getUserDataDir() - - // 使用并发初始化文件系统安全功能 - go func() { - // 初始化审计日志(记录到 logs 子目录) - logDir := filepath.Join(userDataDir, "logs") - if err := filesystem.InitAudit(logDir); err != nil { - println("Warning: Failed to initialize audit log:", err.Error()) - } - - // 初始化回收站 - recycleBinPath := filepath.Join(userDataDir, "recycle_bin") - if err := filesystem.InitRecycleBin(recycleBinPath); err != nil { - println("Warning: Failed to initialize recycle bin:", err.Error()) - } - - // 初始化文件锁检查器 - filesystem.InitFileLockChecker() - }() -} - -// getUserDataDir 获取用户数据目录 -func getUserDataDir() string { - var basePath string - - // 根据操作系统选择不同的基础路径 - switch runtime.GOOS { - case "windows": - // Windows: %APPDATA% 或 %LOCALAPPDATA% - basePath = os.Getenv("LOCALAPPDATA") - if basePath == "" { - basePath = os.Getenv("APPDATA") - } - case "darwin": - // macOS: ~/Library/Application Support - homeDir, _ := os.UserHomeDir() - basePath = filepath.Join(homeDir, "Library", "Application Support") - default: - // Linux: ~/.config - homeDir, _ := os.UserHomeDir() - basePath = filepath.Join(homeDir, ".config") - } - - // 确保基础路径存在 - if basePath == "" { - basePath = "." - } - - // 返回应用特定的数据目录 - return filepath.Join(basePath, "u-desk") -} diff --git a/wails.json b/wails.json index d717429..d011ca8 100644 --- a/wails.json +++ b/wails.json @@ -1,6 +1,7 @@ { "name": "u-desk", "outputfilename": "u-desk", + "version": "0.2.0", "frontend:install": "npm install", "frontend:build": "npm run build", "author": { diff --git a/web/package-lock.json b/web/package-lock.json index d8b6b51..b38b68b 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -23,6 +23,7 @@ "@codemirror/lang-python": "^6.2.1", "@codemirror/lang-rust": "^6.0.2", "@codemirror/lang-sql": "^6.10.0", + "@codemirror/lang-yaml": "^6.1.2", "@codemirror/language": "^6.12.1", "@codemirror/legacy-modes": "^6.5.2", "@codemirror/state": "^6.5.3", @@ -341,6 +342,21 @@ "@lezer/lr": "^1.0.0" } }, + "node_modules/@codemirror/lang-yaml": { + "version": "6.1.2", + "resolved": "https://registry.npmmirror.com/@codemirror/lang-yaml/-/lang-yaml-6.1.2.tgz", + "integrity": "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.0.0", + "@lezer/yaml": "^1.0.0" + } + }, "node_modules/@codemirror/language": { "version": "6.12.1", "resolved": "https://registry.npmmirror.com/@codemirror/language/-/language-6.12.1.tgz", @@ -1024,6 +1040,17 @@ "@lezer/lr": "^1.0.0" } }, + "node_modules/@lezer/yaml": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@lezer/yaml/-/yaml-1.0.3.tgz", + "integrity": "sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.4.0" + } + }, "node_modules/@marijn/find-cluster-break": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", diff --git a/web/package.json b/web/package.json index 3d7b5da..70917ff 100644 --- a/web/package.json +++ b/web/package.json @@ -23,6 +23,7 @@ "@codemirror/lang-python": "^6.2.1", "@codemirror/lang-rust": "^6.0.2", "@codemirror/lang-sql": "^6.10.0", + "@codemirror/lang-yaml": "^6.1.2", "@codemirror/language": "^6.12.1", "@codemirror/legacy-modes": "^6.5.2", "@codemirror/state": "^6.5.3", diff --git a/web/package.json.md5 b/web/package.json.md5 index 2ff627c..5465935 100644 --- a/web/package.json.md5 +++ b/web/package.json.md5 @@ -1 +1 @@ -b9906c1fd8b30922e23d654093282ace \ No newline at end of file +810f4ede0f42ca4e7c9d9a4b9c07f018 \ No newline at end of file diff --git a/web/src/App.vue b/web/src/App.vue index 8377f80..12b2429 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -6,16 +6,17 @@