mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-14 18:39:54 +08:00
287 lines
8.7 KiB
Go
287 lines
8.7 KiB
Go
package app
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
stdRuntime "runtime"
|
||
"strings"
|
||
|
||
"GoNavi-Wails/internal/appdata"
|
||
"GoNavi-Wails/internal/connection"
|
||
"GoNavi-Wails/internal/db"
|
||
"GoNavi-Wails/internal/logger"
|
||
|
||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||
)
|
||
|
||
var dataRootMigrationExcludedEntries = map[string]struct{}{
|
||
"storage_root.json": {},
|
||
}
|
||
|
||
func (a *App) GetDataRootDirectoryInfo() connection.QueryResult {
|
||
return connection.QueryResult{Success: true, Message: "OK", Data: dataRootInfoPayload(a.configDir)}
|
||
}
|
||
|
||
func (a *App) SelectDataRootDirectory(currentDir string) connection.QueryResult {
|
||
defaultDir := strings.TrimSpace(currentDir)
|
||
if defaultDir == "" {
|
||
defaultDir = appdata.MustResolveActiveRoot()
|
||
}
|
||
if !filepath.IsAbs(defaultDir) {
|
||
if abs, err := filepath.Abs(defaultDir); err == nil {
|
||
defaultDir = abs
|
||
}
|
||
}
|
||
|
||
selection, err := runtime.OpenDirectoryDialog(a.ctx, runtime.OpenDialogOptions{
|
||
Title: "选择 GoNavi 数据目录",
|
||
DefaultDirectory: defaultDir,
|
||
CanCreateDirectories: true,
|
||
})
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
if strings.TrimSpace(selection) == "" {
|
||
return connection.QueryResult{Success: false, Message: "已取消"}
|
||
}
|
||
resolved, err := appdata.ResolveRoot(selection)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
return connection.QueryResult{Success: true, Data: dataRootInfoPayload(resolved)}
|
||
}
|
||
|
||
func (a *App) ApplyDataRootDirectory(directory string, migrate bool) connection.QueryResult {
|
||
currentRoot := appdata.MustResolveActiveRoot()
|
||
targetRoot, err := appdata.ResolveRoot(directory)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
if filepath.Clean(currentRoot) == filepath.Clean(targetRoot) {
|
||
a.configDir = targetRoot
|
||
db.SetExternalDriverDownloadDirectory(appdata.DriverRoot(targetRoot))
|
||
return connection.QueryResult{
|
||
Success: true,
|
||
Message: "数据目录未发生变化",
|
||
Data: dataRootInfoPayload(targetRoot),
|
||
}
|
||
}
|
||
|
||
if migrate {
|
||
if err := migrateDataRootContents(currentRoot, targetRoot); err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
}
|
||
|
||
appliedRoot, err := appdata.SetActiveRoot(targetRoot)
|
||
if err != nil {
|
||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||
}
|
||
a.configDir = appliedRoot
|
||
db.SetExternalDriverDownloadDirectory(appdata.DriverRoot(appliedRoot))
|
||
message := "数据目录已更新,建议重启应用以让 AI 与其他运行态模块完全切换到新目录"
|
||
if migrate {
|
||
message = "数据已迁移并切换到新目录,建议重启应用以完成全部模块切换"
|
||
}
|
||
return connection.QueryResult{Success: true, Message: message, Data: dataRootInfoPayload(appliedRoot)}
|
||
}
|
||
|
||
func (a *App) OpenDataRootDirectory() connection.QueryResult {
|
||
root := appdata.MustResolveActiveRoot()
|
||
if stat, err := os.Stat(root); err != nil || !stat.IsDir() {
|
||
return connection.QueryResult{Success: false, Message: "数据目录不存在或不可访问"}
|
||
}
|
||
var cmd *exec.Cmd
|
||
switch stdRuntime.GOOS {
|
||
case "darwin":
|
||
cmd = exec.Command("open", root)
|
||
case "windows":
|
||
cmd = exec.Command("explorer", root)
|
||
case "linux":
|
||
cmd = exec.Command("xdg-open", root)
|
||
default:
|
||
return connection.QueryResult{Success: false, Message: fmt.Sprintf("当前平台暂不支持打开目录:%s", stdRuntime.GOOS)}
|
||
}
|
||
if err := cmd.Start(); err != nil {
|
||
logger.Error(err, "打开数据目录失败")
|
||
return connection.QueryResult{Success: false, Message: fmt.Sprintf("打开数据目录失败:%v", err)}
|
||
}
|
||
return connection.QueryResult{Success: true, Message: "已打开数据目录", Data: dataRootInfoPayload(root)}
|
||
}
|
||
|
||
func migrateDataRootContents(sourceRoot string, targetRoot string) error {
|
||
sourceRoot = strings.TrimSpace(sourceRoot)
|
||
targetRoot = strings.TrimSpace(targetRoot)
|
||
if sourceRoot == "" || targetRoot == "" {
|
||
return fmt.Errorf("数据目录不能为空")
|
||
}
|
||
sourceAbs, err := filepath.Abs(sourceRoot)
|
||
if err != nil {
|
||
return fmt.Errorf("解析源数据目录失败:%w", err)
|
||
}
|
||
targetAbs, err := filepath.Abs(targetRoot)
|
||
if err != nil {
|
||
return fmt.Errorf("解析目标数据目录失败:%w", err)
|
||
}
|
||
if filepath.Clean(sourceAbs) == filepath.Clean(targetAbs) {
|
||
return nil
|
||
}
|
||
if rel, err := filepath.Rel(sourceAbs, targetAbs); err == nil && rel != "." && rel != "" && !strings.HasPrefix(rel, "..") && !filepath.IsAbs(rel) {
|
||
return fmt.Errorf("目标数据目录不能位于源目录内部")
|
||
}
|
||
sourceRoot = sourceAbs
|
||
targetRoot = targetAbs
|
||
if err := os.MkdirAll(targetRoot, 0o755); err != nil {
|
||
return fmt.Errorf("创建目标数据目录失败:%w", err)
|
||
}
|
||
entries, err := os.ReadDir(sourceRoot)
|
||
if err != nil {
|
||
return fmt.Errorf("读取源数据目录失败:%w", err)
|
||
}
|
||
for _, entry := range entries {
|
||
name := strings.TrimSpace(entry.Name())
|
||
if name == "" {
|
||
continue
|
||
}
|
||
if _, excluded := dataRootMigrationExcludedEntries[name]; excluded {
|
||
continue
|
||
}
|
||
sourcePath := filepath.Join(sourceRoot, name)
|
||
targetPath := filepath.Join(targetRoot, name)
|
||
info, err := entry.Info()
|
||
if err != nil {
|
||
return fmt.Errorf("读取源数据失败(%s):%w", name, err)
|
||
}
|
||
if info.IsDir() {
|
||
if err := copyDir(sourcePath, targetPath); err != nil {
|
||
return fmt.Errorf("迁移目录失败(%s):%w", name, err)
|
||
}
|
||
continue
|
||
}
|
||
if err := copyFile(sourcePath, targetPath, info.Mode()); err != nil {
|
||
return fmt.Errorf("迁移文件失败(%s):%w", name, err)
|
||
}
|
||
}
|
||
if err := rewriteMigratedDataRootState(targetRoot); err != nil {
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func rewriteMigratedDataRootState(targetRoot string) error {
|
||
if err := rewriteSecurityUpdateBackupPaths(targetRoot); err != nil {
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func rewriteSecurityUpdateBackupPaths(targetRoot string) error {
|
||
repo := newSecurityUpdateStateRepository(targetRoot)
|
||
marker, err := repo.readMarker()
|
||
if err != nil {
|
||
if os.IsNotExist(err) {
|
||
return nil
|
||
}
|
||
return fmt.Errorf("读取迁移后的安全更新状态失败:%w", err)
|
||
}
|
||
|
||
migrationID := strings.TrimSpace(marker.MigrationID)
|
||
if migrationID == "" {
|
||
return nil
|
||
}
|
||
|
||
targetBackupPath := repo.backupPath(migrationID)
|
||
marker.BackupPath = targetBackupPath
|
||
if err := repo.writeMarker(marker); err != nil {
|
||
return fmt.Errorf("写入迁移后的安全更新状态失败:%w", err)
|
||
}
|
||
|
||
manifestPath := repo.manifestPath(migrationID)
|
||
manifestData, err := os.ReadFile(manifestPath)
|
||
if err != nil {
|
||
if !os.IsNotExist(err) {
|
||
return fmt.Errorf("读取迁移后的安全更新备份清单失败:%w", err)
|
||
}
|
||
} else {
|
||
var manifest securityUpdateBackupManifest
|
||
if err := json.Unmarshal(manifestData, &manifest); err != nil {
|
||
return fmt.Errorf("解析迁移后的安全更新备份清单失败:%w", err)
|
||
}
|
||
manifest.BackupPath = targetBackupPath
|
||
if err := securityUpdateWriteJSONFile(manifestPath, manifest); err != nil {
|
||
return fmt.Errorf("写入迁移后的安全更新备份清单失败:%w", err)
|
||
}
|
||
}
|
||
|
||
resultPath := repo.resultPath(migrationID)
|
||
resultData, err := os.ReadFile(resultPath)
|
||
if err != nil {
|
||
if !os.IsNotExist(err) {
|
||
return fmt.Errorf("读取迁移后的安全更新结果失败:%w", err)
|
||
}
|
||
} else {
|
||
var result SecurityUpdateStatus
|
||
if err := json.Unmarshal(resultData, &result); err != nil {
|
||
return fmt.Errorf("解析迁移后的安全更新结果失败:%w", err)
|
||
}
|
||
result.BackupPath = targetBackupPath
|
||
result.BackupAvailable = strings.TrimSpace(targetBackupPath) != ""
|
||
if err := securityUpdateWriteJSONFile(resultPath, result); err != nil {
|
||
return fmt.Errorf("写入迁移后的安全更新结果失败:%w", err)
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func copyDir(sourceDir string, targetDir string) error {
|
||
return filepath.WalkDir(sourceDir, func(path string, entry os.DirEntry, walkErr error) error {
|
||
if walkErr != nil {
|
||
return walkErr
|
||
}
|
||
relativePath, err := filepath.Rel(sourceDir, path)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
targetPath := filepath.Join(targetDir, relativePath)
|
||
if entry.IsDir() {
|
||
info, err := entry.Info()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return os.MkdirAll(targetPath, info.Mode())
|
||
}
|
||
info, err := entry.Info()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return copyFile(path, targetPath, info.Mode())
|
||
})
|
||
}
|
||
|
||
func copyFile(sourcePath string, targetPath string, mode os.FileMode) error {
|
||
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
|
||
return err
|
||
}
|
||
sourceFile, err := os.Open(sourcePath)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer sourceFile.Close()
|
||
|
||
targetFile, err := os.Create(targetPath)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer targetFile.Close()
|
||
|
||
if _, err := io.Copy(targetFile, sourceFile); err != nil {
|
||
return err
|
||
}
|
||
return os.Chmod(targetPath, mode)
|
||
}
|