mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-11 17:29:42 +08:00
- 合并已按 issue 拆分提交的 backlog 修复与 SQL 结果集同步能力 - 解决 DataGrid、Sidebar 以及 legacy WebKit 存储迁移测试的合并冲突 - 保留 dev 分支当前结构并移除已废弃的 issue backlog 跟踪文档
181 lines
4.5 KiB
Go
181 lines
4.5 KiB
Go
package app
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"database/sql"
|
||
"encoding/binary"
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"unicode/utf16"
|
||
"unicode/utf8"
|
||
|
||
"GoNavi-Wails/internal/connection"
|
||
"GoNavi-Wails/internal/logger"
|
||
|
||
_ "modernc.org/sqlite"
|
||
)
|
||
|
||
const legacyPersistKey = "lite-db-storage"
|
||
|
||
var legacyWebKitBundleIDs = []string{
|
||
"com.wails.GoNavi",
|
||
"com.wails.GoNavi-Wails",
|
||
}
|
||
|
||
type legacyWebKitVisibleConfig struct {
|
||
Connections []connection.LegacySavedConnection
|
||
GlobalProxy *connection.LegacyGlobalProxyInput
|
||
}
|
||
|
||
func currentBuildType(ctx context.Context) string {
|
||
if ctx == nil {
|
||
return ""
|
||
}
|
||
buildType := ctx.Value("buildtype")
|
||
if value, ok := buildType.(string); ok {
|
||
return strings.TrimSpace(value)
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func shouldAttemptLegacyWebKitStorageMigration(buildType string) bool {
|
||
return runtimeGOOS() == "darwin" && strings.EqualFold(strings.TrimSpace(buildType), "dev")
|
||
}
|
||
|
||
func migrateLegacyWebKitStorageIfNeeded(a *App) error {
|
||
return migrateLegacyWebKitStorageIfNeededWithHome(a, currentBuildType(a.ctx), os.UserHomeDir)
|
||
}
|
||
|
||
func migrateLegacyWebKitStorageIfNeededWithHome(a *App, buildType string, resolveHomeDir func() (string, error)) error {
|
||
if a == nil || !shouldAttemptLegacyWebKitStorageMigration(buildType) {
|
||
return nil
|
||
}
|
||
|
||
repo := a.savedConnectionRepository()
|
||
if _, err := os.Stat(repo.connectionsPath()); err == nil {
|
||
return nil
|
||
} else if err != nil && !os.IsNotExist(err) {
|
||
return err
|
||
}
|
||
|
||
homeDir, err := resolveHomeDir()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
legacy, sourcePath, err := findLegacyWebKitVisibleConfig(homeDir)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if len(legacy.Connections) == 0 && legacy.GlobalProxy == nil {
|
||
return nil
|
||
}
|
||
|
||
if len(legacy.Connections) > 0 {
|
||
if _, err := a.ImportLegacyConnections(legacy.Connections); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
if legacy.GlobalProxy != nil {
|
||
if _, err := os.Stat(globalProxyMetadataPath(a.configDir)); os.IsNotExist(err) {
|
||
if _, err := a.ImportLegacyGlobalProxy(*legacy.GlobalProxy); err != nil {
|
||
return err
|
||
}
|
||
} else if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
logger.Infof("已从旧 WebKit 本地存储迁移 %d 条连接(source=%s)", len(legacy.Connections), sourcePath)
|
||
return nil
|
||
}
|
||
|
||
func findLegacyWebKitVisibleConfig(homeDir string) (legacyWebKitVisibleConfig, string, error) {
|
||
var best legacyWebKitVisibleConfig
|
||
var bestPath string
|
||
bestScore := -1
|
||
|
||
for _, bundleID := range legacyWebKitBundleIDs {
|
||
pattern := filepath.Join(homeDir, "Library", "WebKit", bundleID, "WebsiteData", "Default", "*", "*", "LocalStorage", "localstorage.sqlite3")
|
||
matches, err := filepath.Glob(pattern)
|
||
if err != nil {
|
||
return legacyWebKitVisibleConfig{}, "", err
|
||
}
|
||
for _, dbPath := range matches {
|
||
current, err := readLegacyWebKitVisibleConfig(dbPath)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
score := len(current.Connections) * 10
|
||
if current.GlobalProxy != nil {
|
||
score++
|
||
}
|
||
if score > bestScore {
|
||
best = current
|
||
bestPath = dbPath
|
||
bestScore = score
|
||
}
|
||
}
|
||
}
|
||
|
||
if bestScore < 0 {
|
||
return legacyWebKitVisibleConfig{}, "", nil
|
||
}
|
||
return best, bestPath, nil
|
||
}
|
||
|
||
func readLegacyWebKitVisibleConfig(dbPath string) (legacyWebKitVisibleConfig, error) {
|
||
db, err := sql.Open("sqlite", dbPath)
|
||
if err != nil {
|
||
return legacyWebKitVisibleConfig{}, err
|
||
}
|
||
defer db.Close()
|
||
|
||
var raw []byte
|
||
if err := db.QueryRow(`SELECT CAST(value AS BLOB) FROM ItemTable WHERE key = ?`, legacyPersistKey).Scan(&raw); err != nil {
|
||
if err == sql.ErrNoRows {
|
||
return legacyWebKitVisibleConfig{}, nil
|
||
}
|
||
return legacyWebKitVisibleConfig{}, err
|
||
}
|
||
|
||
payload := decodeLegacyWebKitJSON(raw)
|
||
if strings.TrimSpace(payload) == "" {
|
||
return legacyWebKitVisibleConfig{}, nil
|
||
}
|
||
|
||
var envelope struct {
|
||
State legacyWebKitVisibleConfig `json:"state"`
|
||
}
|
||
if err := json.Unmarshal([]byte(payload), &envelope); err != nil {
|
||
return legacyWebKitVisibleConfig{}, fmt.Errorf("parse legacy webkit storage %s: %w", dbPath, err)
|
||
}
|
||
return envelope.State, nil
|
||
}
|
||
|
||
func decodeLegacyWebKitJSON(raw []byte) string {
|
||
trimmed := bytes.TrimSpace(raw)
|
||
if len(trimmed) == 0 {
|
||
return ""
|
||
}
|
||
if utf8.Valid(trimmed) && !bytes.Contains(trimmed, []byte{0x00}) {
|
||
return string(trimmed)
|
||
}
|
||
if len(trimmed)%2 == 0 {
|
||
u16 := make([]uint16, 0, len(trimmed)/2)
|
||
for i := 0; i < len(trimmed); i += 2 {
|
||
u16 = append(u16, binary.LittleEndian.Uint16(trimmed[i:i+2]))
|
||
}
|
||
decoded := strings.TrimRight(string(utf16.Decode(u16)), "\x00")
|
||
if utf8.ValidString(decoded) {
|
||
return strings.TrimSpace(decoded)
|
||
}
|
||
}
|
||
return strings.TrimSpace(string(trimmed))
|
||
}
|