mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-10 17:43:15 +08:00
🐛 fix(windows-upgrade): 修复Windows升级后连接列表丢失问题
- 启动参数新增固定 WebviewUserDataPath 到 %APPDATA%/GoNavi/WebView2 - 首次启动自动迁移历史 WebView 数据目录 - 保留现有存储键,避免破坏已落盘配置 - 前端持久化读取增加历史结构兼容 - refs #125
This commit is contained in:
@@ -206,10 +206,27 @@ const sanitizeConnectionConfig = (value: unknown): ConnectionConfig => {
|
||||
return safeConfig;
|
||||
};
|
||||
|
||||
const resolveConnectionConfigPayload = (raw: Record<string, unknown>): unknown => {
|
||||
if (raw.config && typeof raw.config === 'object') {
|
||||
return raw.config;
|
||||
}
|
||||
// 兼容历史/导入场景:连接对象可能是扁平结构(无 config 包装)。
|
||||
const hasLegacyFlatConfig =
|
||||
raw.type !== undefined ||
|
||||
raw.host !== undefined ||
|
||||
raw.port !== undefined ||
|
||||
raw.user !== undefined ||
|
||||
raw.database !== undefined;
|
||||
if (hasLegacyFlatConfig) {
|
||||
return raw;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const sanitizeSavedConnection = (value: unknown, index: number): SavedConnection | null => {
|
||||
if (!value || typeof value !== 'object') return null;
|
||||
const raw = value as Record<string, unknown>;
|
||||
const config = sanitizeConnectionConfig(raw.config);
|
||||
const config = sanitizeConnectionConfig(resolveConnectionConfigPayload(raw));
|
||||
const id = toTrimmedString(raw.id, `conn-${index + 1}`) || `conn-${index + 1}`;
|
||||
const fallbackName = config.host ? `${config.type}-${config.host}` : `连接-${index + 1}`;
|
||||
const name = toTrimmedString(raw.name, fallbackName) || fallbackName;
|
||||
@@ -392,6 +409,17 @@ const sanitizeAppearance = (
|
||||
return nextAppearance;
|
||||
};
|
||||
|
||||
const unwrapPersistedAppState = (persistedState: unknown): Record<string, unknown> => {
|
||||
if (!persistedState || typeof persistedState !== 'object') {
|
||||
return {};
|
||||
}
|
||||
const raw = persistedState as Record<string, unknown>;
|
||||
if (raw.state && typeof raw.state === 'object') {
|
||||
return raw.state as Record<string, unknown>;
|
||||
}
|
||||
return raw;
|
||||
};
|
||||
|
||||
export const useStore = create<AppState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
@@ -544,10 +572,7 @@ export const useStore = create<AppState>()(
|
||||
name: 'lite-db-storage', // name of the item in the storage (must be unique)
|
||||
version: 3,
|
||||
migrate: (persistedState: unknown, version: number) => {
|
||||
if (!persistedState || typeof persistedState !== 'object') {
|
||||
return persistedState as AppState;
|
||||
}
|
||||
const state = persistedState as Partial<AppState>;
|
||||
const state = unwrapPersistedAppState(persistedState) as Partial<AppState>;
|
||||
const nextState: Partial<AppState> = { ...state };
|
||||
nextState.connections = sanitizeConnections(state.connections);
|
||||
nextState.savedQueries = sanitizeSavedQueries(state.savedQueries);
|
||||
@@ -560,9 +585,7 @@ export const useStore = create<AppState>()(
|
||||
return nextState as AppState;
|
||||
},
|
||||
merge: (persistedState, currentState) => {
|
||||
const state = (persistedState && typeof persistedState === 'object')
|
||||
? persistedState as Partial<AppState>
|
||||
: {};
|
||||
const state = unwrapPersistedAppState(persistedState) as Partial<AppState>;
|
||||
return {
|
||||
...currentState,
|
||||
...state,
|
||||
|
||||
1
main.go
1
main.go
@@ -41,6 +41,7 @@ func main() {
|
||||
BackdropType: windows.Acrylic,
|
||||
DisableWindowIcon: false,
|
||||
DisableFramelessWindowDecorations: false,
|
||||
WebviewUserDataPath: resolveWindowsWebviewUserDataPath(),
|
||||
},
|
||||
Mac: &mac.Options{
|
||||
WebviewIsTransparent: true,
|
||||
|
||||
123
main_windows_webview_userdata.go
Normal file
123
main_windows_webview_userdata.go
Normal file
@@ -0,0 +1,123 @@
|
||||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func resolveWindowsWebviewUserDataPath() string {
|
||||
appDataDir := strings.TrimSpace(os.Getenv("APPDATA"))
|
||||
if appDataDir == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
targetDir := filepath.Join(appDataDir, "GoNavi", "WebView2")
|
||||
_ = migrateLegacyWindowsWebviewUserData(appDataDir, targetDir)
|
||||
return targetDir
|
||||
}
|
||||
|
||||
func migrateLegacyWindowsWebviewUserData(appDataDir, targetDir string) error {
|
||||
if dirHasContent(targetDir) {
|
||||
return nil
|
||||
}
|
||||
|
||||
exeName := "GoNavi.exe"
|
||||
if exePath, err := os.Executable(); err == nil {
|
||||
base := strings.TrimSpace(filepath.Base(exePath))
|
||||
if base != "" {
|
||||
exeName = base
|
||||
}
|
||||
}
|
||||
exeBase := strings.TrimSuffix(exeName, filepath.Ext(exeName))
|
||||
|
||||
candidates := []string{
|
||||
filepath.Join(appDataDir, exeName),
|
||||
filepath.Join(appDataDir, exeBase),
|
||||
filepath.Join(appDataDir, "GoNavi.exe"),
|
||||
filepath.Join(appDataDir, "GoNavi"),
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{}, len(candidates))
|
||||
for _, candidate := range candidates {
|
||||
src := filepath.Clean(strings.TrimSpace(candidate))
|
||||
if src == "" || strings.EqualFold(src, filepath.Clean(targetDir)) {
|
||||
continue
|
||||
}
|
||||
key := strings.ToLower(src)
|
||||
if _, exists := seen[key]; exists {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
|
||||
if !dirHasContent(src) {
|
||||
continue
|
||||
}
|
||||
return copyDirTree(src, targetDir)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func dirHasContent(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil || !info.IsDir() {
|
||||
return false
|
||||
}
|
||||
entries, err := os.ReadDir(path)
|
||||
return err == nil && len(entries) > 0
|
||||
}
|
||||
|
||||
func copyDirTree(srcDir, dstDir string) error {
|
||||
if err := os.MkdirAll(dstDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return filepath.WalkDir(srcDir, func(srcPath string, d os.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
relPath, err := filepath.Rel(srcDir, srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if relPath == "." {
|
||||
return nil
|
||||
}
|
||||
dstPath := filepath.Join(dstDir, relPath)
|
||||
|
||||
if d.IsDir() {
|
||||
return os.MkdirAll(dstPath, 0o755)
|
||||
}
|
||||
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return copyFileWithMode(srcPath, dstPath, info.Mode())
|
||||
})
|
||||
}
|
||||
|
||||
func copyFileWithMode(srcPath, dstPath string, mode os.FileMode) error {
|
||||
srcFile, err := os.Open(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(dstPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
dstFile, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode.Perm())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
if _, err := io.Copy(dstFile, srcFile); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
7
main_windows_webview_userdata_stub.go
Normal file
7
main_windows_webview_userdata_stub.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build !windows
|
||||
|
||||
package main
|
||||
|
||||
func resolveWindowsWebviewUserDataPath() string {
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user