// 将本地 sqlite 库从 baihu 前缀迁移为 taskpool 前缀,并更新触发类型/站点标题。 // // 用法(项目根目录): // // go run ./scripts/migrate-db-brand.go // go run ./scripts/migrate-db-brand.go -db data/baihu.db -from baihu_ -to taskpool_ package main import ( "database/sql" "flag" "fmt" "os" "path/filepath" "strings" "time" _ "github.com/glebarez/sqlite" ) func main() { dbPath := flag.String("db", "data/baihu.db", "sqlite database path") fromPrefix := flag.String("from", "baihu_", "old table prefix") toPrefix := flag.String("to", "taskpool_", "new table prefix") outPath := flag.String("out", "data/taskpool.db", "output database path (copy then migrate)") dryRun := flag.Bool("dry-run", false, "only print actions") flag.Parse() if err := run(*dbPath, *outPath, *fromPrefix, *toPrefix, *dryRun); err != nil { fmt.Fprintf(os.Stderr, "migrate failed: %v\n", err) os.Exit(1) } } func run(srcDB, outDB, fromPrefix, toPrefix string, dryRun bool) error { if _, err := os.Stat(srcDB); err != nil { return fmt.Errorf("source db not found: %s", srcDB) } // 先 checkpoint wal,确保主库完整 if err := checkpoint(srcDB); err != nil { fmt.Printf("warn: checkpoint: %v\n", err) } absSrc, _ := filepath.Abs(srcDB) absOut, _ := filepath.Abs(outDB) fmt.Printf("source: %s\noutput: %s\nprefix: %s -> %s\n", absSrc, absOut, fromPrefix, toPrefix) if dryRun { db, err := sql.Open("sqlite", srcDB) if err != nil { return err } defer db.Close() tables, err := listTables(db, fromPrefix) if err != nil { return err } for _, t := range tables { fmt.Printf("would rename %s -> %s\n", t, strings.Replace(t, fromPrefix, toPrefix, 1)) } return nil } if err := os.MkdirAll(filepath.Dir(outDB), 0o755); err != nil { return err } // 备份源库 bak := srcDB + ".bak-" + time.Now().Format("20060102-150405") if err := copyFile(srcDB, bak); err != nil { return fmt.Errorf("backup failed: %w", err) } fmt.Printf("backup: %s\n", bak) // 复制到目标(若目标就是源,则原地改) same := filepath.Clean(absSrc) == filepath.Clean(absOut) work := outDB if !same { // 清理目标旁路文件 _ = os.Remove(outDB) _ = os.Remove(outDB + "-wal") _ = os.Remove(outDB + "-shm") if err := copyFile(srcDB, outDB); err != nil { return fmt.Errorf("copy to output failed: %w", err) } // 若源有 wal,已 checkpoint,通常足够 } else { work = srcDB } db, err := sql.Open("sqlite", work) if err != nil { return err } defer db.Close() if _, err := db.Exec(`PRAGMA foreign_keys = OFF`); err != nil { return err } tables, err := listTables(db, fromPrefix) if err != nil { return err } if len(tables) == 0 { // 也许已经迁过 tables2, _ := listTables(db, toPrefix) if len(tables2) > 0 { fmt.Println("tables already use new prefix; continue data fixes") } else { return fmt.Errorf("no tables with prefix %q found", fromPrefix) } } tx, err := db.Begin() if err != nil { return err } for _, oldName := range tables { newName := strings.Replace(oldName, fromPrefix, toPrefix, 1) if newName == oldName { continue } // 若新表已存在则跳过 var exists int _ = tx.QueryRow(`SELECT COUNT(1) FROM sqlite_master WHERE type='table' AND name=?`, newName).Scan(&exists) if exists > 0 { fmt.Printf("skip (exists): %s\n", newName) continue } sqlStmt := fmt.Sprintf(`ALTER TABLE "%s" RENAME TO "%s"`, oldName, newName) if _, err := tx.Exec(sqlStmt); err != nil { _ = tx.Rollback() return fmt.Errorf("rename %s -> %s: %w", oldName, newName, err) } fmt.Printf("renamed: %s -> %s\n", oldName, newName) } // 索引名里若带旧前缀,SQLite 一般会随表走;对显式旧索引再尝试重命名 indexes, err := listIndexes(tx, fromPrefix) if err != nil { _ = tx.Rollback() return err } for _, oldIdx := range indexes { newIdx := strings.Replace(oldIdx, fromPrefix, toPrefix, 1) if newIdx == oldIdx { continue } var exists int _ = tx.QueryRow(`SELECT COUNT(1) FROM sqlite_master WHERE type='index' AND name=?`, newIdx).Scan(&exists) if exists > 0 { continue } // SQLite 3.26+ supports ALTER INDEX RENAME; if fails, ignore stmt := fmt.Sprintf(`ALTER INDEX "%s" RENAME TO "%s"`, oldIdx, newIdx) if _, err := tx.Exec(stmt); err != nil { fmt.Printf("warn: index rename %s: %v\n", oldIdx, err) } else { fmt.Printf("renamed index: %s -> %s\n", oldIdx, newIdx) } } settingsTable := toPrefix + "settings" tasksTable := toPrefix + "tasks" // 站点标题 if hasTable(tx, settingsTable) { res, err := tx.Exec(`UPDATE "`+settingsTable+`" SET value = ? WHERE section = 'site' AND key = 'title' AND (value = '白虎面板' OR value = 'Baihu Panel' OR value LIKE '%白虎%')`, "任务池") if err != nil { fmt.Printf("warn: update site title: %v\n", err) } else if n, _ := res.RowsAffected(); n > 0 { fmt.Printf("updated site title rows: %d\n", n) } } // 触发类型 if hasTable(tx, tasksTable) { res, err := tx.Exec(`UPDATE "`+tasksTable+`" SET trigger_type = 'taskpool_startup' WHERE trigger_type = 'baihu_startup'`) if err != nil { fmt.Printf("warn: update trigger_type: %v\n", err) } else if n, _ := res.RowsAffected(); n > 0 { fmt.Printf("updated trigger_type rows: %d\n", n) } } if err := tx.Commit(); err != nil { return err } if _, err := db.Exec(`VACUUM`); err != nil { fmt.Printf("warn: vacuum: %v\n", err) } if _, err := db.Exec(`PRAGMA wal_checkpoint(FULL)`); err != nil { fmt.Printf("warn: final checkpoint: %v\n", err) } fmt.Println("migrate done") fmt.Println("next:") fmt.Println(" 1) update configs/config.ini:") fmt.Println(" path = data/taskpool.db") fmt.Println(" table_prefix = taskpool_") fmt.Println(" dbname = taskpool") fmt.Println(" 2) restart: bin\\taskpool.exe server") return nil } func checkpoint(dbPath string) error { db, err := sql.Open("sqlite", dbPath) if err != nil { return err } defer db.Close() _, err = db.Exec(`PRAGMA wal_checkpoint(FULL)`) return err } func listTables(db *sql.DB, prefix string) ([]string, error) { rows, err := db.Query(`SELECT name FROM sqlite_master WHERE type='table' AND name LIKE ? AND name NOT LIKE 'sqlite_%' ORDER BY name`, prefix+"%") if err != nil { return nil, err } defer rows.Close() var out []string for rows.Next() { var name string if err := rows.Scan(&name); err != nil { return nil, err } out = append(out, name) } return out, rows.Err() } func listIndexes(tx *sql.Tx, prefix string) ([]string, error) { rows, err := tx.Query(`SELECT name FROM sqlite_master WHERE type='index' AND name LIKE ? AND name NOT LIKE 'sqlite_%' ORDER BY name`, prefix+"%") if err != nil { return nil, err } defer rows.Close() var out []string for rows.Next() { var name string if err := rows.Scan(&name); err != nil { return nil, err } out = append(out, name) } return out, rows.Err() } func hasTable(tx *sql.Tx, name string) bool { var n int _ = tx.QueryRow(`SELECT COUNT(1) FROM sqlite_master WHERE type='table' AND name=?`, name).Scan(&n) return n > 0 } func copyFile(src, dst string) error { in, err := os.ReadFile(src) if err != nil { return err } return os.WriteFile(dst, in, 0o644) }