Files
T
sun aa3f09752e tools: 加 cmd/dumpdb ops 工具
dumpdb 是 read-only 的 SQLite 探查工具:
  go run ./cmd/dumpdb /path/to/tts.db

输出:
- 所有表
- schema_version
- settings(敏感字段打码: api_key/auth_key/speaker_id 等)
- voices(完整行,无打码 — 这是 ops 用)
- journal_mode/page_size 等 pragmas

用法: 服务器 db 排查时,直接下 tts.db 下来用这个查,
不用 sqlite3 CLI,也不用写 SQL。

无依赖(用已有的 modernc.org/sqlite)。

未 push(收尾 commit,跟之前一起发)
2026-08-30 11:45:08 +08:00

131 lines
3.2 KiB
Go

// dumpdb 是个 ops 工具: dump tts.db 里的 settings + voices 表(明文打码)。
//
// 用法:
// go run ./cmd/dumpdb /path/to/tts.db
//
// 输出 schema_version / settings(键值,敏感字段打码) / voices(完整行)。
// 调试时方便:不依赖 sqlite3 CLI,不暴露明文 key/speaker ID。
package main
import (
"database/sql"
"fmt"
"os"
_ "modernc.org/sqlite"
)
func main() {
if len(os.Args) < 2 {
fmt.Fprintf(os.Stderr, "usage: %s <db-path>\n", os.Args[0])
os.Exit(1)
}
dbPath := os.Args[1]
dsn := "file:" + dbPath + "?mode=ro&_pragma=query_only"
conn, err := sql.Open("sqlite", dsn)
if err != nil {
fmt.Fprintf(os.Stderr, "open: %v\n", err)
os.Exit(2)
}
defer conn.Close()
// 1. tables
fmt.Println("=== Tables ===")
rows, _ := conn.Query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
for rows.Next() {
var name string
_ = rows.Scan(&name)
fmt.Printf(" %s\n", name)
}
rows.Close()
// 2. schema_version
fmt.Println("\n=== schema_version ===")
var ver int
var ts string
err = conn.QueryRow(`SELECT version, applied_at FROM schema_version LIMIT 1`).Scan(&ver, &ts)
if err != nil {
fmt.Printf(" (no row: %v)\n", err)
} else {
fmt.Printf(" version: %d, applied_at: %s\n", ver, ts)
}
// 3. settings
fmt.Println("\n=== settings ===")
rows, err = conn.Query(`SELECT key, value FROM settings ORDER BY key`)
if err != nil {
fmt.Printf(" err: %v\n", err)
} else {
for rows.Next() {
var k, v string
_ = rows.Scan(&k, &v)
if v == "" {
fmt.Printf(" %-30s = (empty)\n", k)
continue
}
lowK := k
_ = lowK
masked := v
if isSensitive(k) {
if len(v) <= 8 {
masked = "****"
} else {
masked = v[:4] + "****" + v[len(v)-4:]
}
}
fmt.Printf(" %-30s = %s\n", k, masked)
}
rows.Close()
}
// 4. voices
fmt.Println("\n=== voices ===")
rows, err = conn.Query(`SELECT id, name, speaker, resource_id, model, language, description, enabled, created_at, updated_at FROM voices ORDER BY id`)
if err != nil {
fmt.Printf(" err: %v\n", err)
} else {
count := 0
for rows.Next() {
count++
var id int64
var name, speaker, resourceID, model, lang, desc, created, updated string
var enabled int
_ = rows.Scan(&id, &name, &speaker, &resourceID, &model, &lang, &desc, &enabled, &created, &updated)
en := "true"
if enabled == 0 {
en = "false"
}
fmt.Printf(" [%d] name=%s\n speaker=%s\n resource_id=%s\n model=%s\n lang=%s enabled=%s\n created=%s updated=%s\n",
id, name, speaker, resourceID, model, lang, en, created, updated)
if desc != "" {
fmt.Printf(" desc: %s\n", desc)
}
}
fmt.Printf(" total: %d\n", count)
rows.Close()
}
// 5. pragmas
fmt.Println("\n=== Pragmas ===")
for _, p := range []string{"journal_mode", "synchronous", "foreign_keys", "page_size", "page_count"} {
var v string
_ = conn.QueryRow("PRAGMA " + p).Scan(&v)
fmt.Printf(" %-15s = %s\n", p, v)
}
}
func isSensitive(key string) bool {
lowK := key
// 简单 contains (不引 strings 包保持精简)
markers := []string{"key", "token", "speaker"}
for _, m := range markers {
for i := 0; i+len(m) <= len(lowK); i++ {
if lowK[i:i+len(m)] == m {
return true
}
}
}
return false
}