package store // backup.go: the F4 SPOF guard (backlog row 83). The owner's SIGNED bank, every checkpoint and the spend // ledger live in ONE SQLite file per project, so a paid run must (1) leave a fresh, consistent restore point // before it starts and (2) refuse to start on a corrupt file — a silent loss of the signed bank is // unrecoverable. Both are cheap, deterministic and provider-free. The caller (tmctl) decides WHEN to run // this; here we only verify integrity and copy. import ( "database/sql" "fmt" "net/url" "os" "path/filepath" "strings" ) // IntegrityCheck runs `PRAGMA integrity_check` against the SQLite file at dbPath and returns a loud error // unless the check reports the single row "ok". It opens a private read-only connection (query_only), so it // can never mutate the file it is verifying and is safe to run against a database another reader holds. func IntegrityCheck(dbPath string) error { db, err := openForBackup(dbPath, true) if err != nil { return err } defer db.Close() return integrityCheck(db, dbPath) } // BackupSQLite verifies the project database's structural integrity and then writes a transactionally // consistent copy to backupDir/.db via `VACUUM INTO` — the SQLite-blessed online-backup path (a // defragmented snapshot, no torn WAL frames). stamp is supplied by the caller (a timestamp) so the backup // name is deterministic under test. A non-"ok" integrity_check is a LOUD error and NO backup is written (a // corrupt source must never be trusted as a restore point). It refuses to overwrite an existing backup, so a // caller cannot silently clobber a good restore point. Returns the backup file's path. func BackupSQLite(dbPath, backupDir, stamp string) (string, error) { if _, err := os.Stat(dbPath); err != nil { return "", fmt.Errorf("store: backup source %s does not exist: %w", dbPath, err) } db, err := openForBackup(dbPath, false) // VACUUM INTO is a write statement — query_only would block it if err != nil { return "", err } defer db.Close() if err := integrityCheck(db, dbPath); err != nil { return "", err } if err := os.MkdirAll(backupDir, 0o755); err != nil { return "", fmt.Errorf("store: backup dir %s: %w", backupDir, err) } backupPath := filepath.Join(backupDir, stamp+".db") if _, err := os.Stat(backupPath); err == nil { return "", fmt.Errorf("store: backup %s already exists (refusing to overwrite a restore point)", backupPath) } // VACUUM INTO takes a string literal path; the single-quote is the only SQL metachar, escaped by doubling. if _, err := db.Exec("VACUUM INTO '" + strings.ReplaceAll(backupPath, "'", "''") + "'"); err != nil { return "", fmt.Errorf("store: VACUUM INTO %s: %w", backupPath, err) } return backupPath, nil } // openForBackup opens a private single connection to dbPath. readOnly adds query_only(1) for a pure check; // the backup path needs a writable connection because VACUUM INTO is classified as a write statement (it // still only READS the source and writes a separate file). func openForBackup(dbPath string, readOnly bool) (*sql.DB, error) { v := url.Values{} v.Add("_pragma", "busy_timeout(5000)") if readOnly { v.Add("_pragma", "query_only(1)") } db, err := sql.Open("sqlite", "file:"+dbPath+"?"+v.Encode()) if err != nil { return nil, fmt.Errorf("store: open %s for backup: %w", dbPath, err) } db.SetMaxOpenConns(1) return db, nil } // integrityCheck runs the pragma over db and demands the single canonical "ok" row; anything else (a list of // corruption reports, or a "file is not a database" open error surfaced here) is a loud failure. func integrityCheck(db *sql.DB, dbPath string) error { rows, err := db.Query("PRAGMA integrity_check") if err != nil { return fmt.Errorf("store: integrity_check %s: %w", dbPath, err) } defer rows.Close() var lines []string for rows.Next() { var s string if err := rows.Scan(&s); err != nil { return fmt.Errorf("store: integrity_check %s scan: %w", dbPath, err) } lines = append(lines, s) } if err := rows.Err(); err != nil { return fmt.Errorf("store: integrity_check %s: %w", dbPath, err) } if len(lines) != 1 || lines[0] != "ok" { return fmt.Errorf("store: integrity_check FAILED for %s: %v", dbPath, lines) } return nil }