Private
Public Access
1
0

新增:应用配置管理模块,优化文件系统功能

- 新增 ConfigAPI 和 ConfigService 实现配置管理
- 新增 SettingsPanel 和 UpdateNotification 组件
- 文件系统模块化重构,提升代码质量
- 提取公共函数,优化代码结构
- 版本号更新至 0.2.0
This commit is contained in:
2026-01-28 22:48:10 +08:00
parent 7e79a53dae
commit b849e6cc46
31 changed files with 3024 additions and 917 deletions

527
app.go
View File

@@ -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("[模块] 文件系统模块初始化完成")
}