e6956aa001
- React frontend with route-level code splitting - Backend rebranded from Baihu to TaskPool - DB brand migration script and local compatibility
267 lines
7.2 KiB
Go
267 lines
7.2 KiB
Go
// 将本地 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)
|
|
}
|