textmachine/backend/cmd/tmctl/backup.go

66 lines
3.1 KiB
Go

package main
// backup.go: the F4 backup/integrity command and the automatic pre-flight guard for the PAID boevoy
// commands (backlog row 83). The guard lives at the boevoy COMMAND dispatch (run()), NOT inside translate()
// or the pipeline Runner — so the "is this a real run?" discriminator carries NO provider-name magic string
// (generality §0: Go must not branch on a fake-vs-real provider literal). A fake-provider test drives the
// Runner or translate() directly and never enters run()'s dispatch, so the golden/fixture paths create zero
// backup files without any special-casing here.
import (
"fmt"
"io"
"os"
"path/filepath"
"time"
"textmachine/backend/internal/config"
"textmachine/backend/internal/store"
)
// backupStamp formats a UTC timestamp for a backup filename — sortable and filesystem-safe.
func backupStamp(t time.Time) string { return t.UTC().Format("20060102T150405Z") }
// backupDirFor returns the durable backup directory for a book: backups/ next to its project DB, which for
// the stand book lands under ~/books/<book>/… — the ratified durable home ("persist, not scratch").
func backupDirFor(dbPath string) string { return filepath.Join(filepath.Dir(dbPath), "backups") }
// backupCmd is the `tmctl backup` verb: after a green PRAGMA integrity_check it VACUUM-INTO-copies the book's
// SQLite file to a fresh timestamped restore point (F4). $0, no provider keys — an operator safety command,
// like status/report. It refuses loudly if the database is missing or fails integrity.
func backupCmd(cfgPath string, w io.Writer) error {
book, err := config.LoadBook(cfgPath)
if err != nil {
return err
}
if _, err := os.Stat(book.ProjectDB); err != nil {
return fmt.Errorf("tmctl backup: project database %s does not exist yet — run `tmctl translate` first, there is nothing to back up", book.ProjectDB)
}
path, err := store.BackupSQLite(book.ProjectDB, backupDirFor(book.ProjectDB), backupStamp(time.Now()))
if err != nil {
return err
}
fmt.Fprintf(w, "backup OK: %s (integrity_check green, VACUUM INTO)\n", path)
return nil
}
// preflightBackup is the automatic pre-run guard for the PAID boevoy commands (translate, non-dry-run
// redrive): a paid run of a book must not start without a fresh restore point and a green PRAGMA
// integrity_check (F4 — the SPOF is a silent loss of the owner's signed bank and checkpoints). A missing DB
// is a fresh book with no signed bank to lose (skip, no error); an integrity failure is a LOUD refusal to
// start, so a paid run never writes on top of a corrupt file.
func preflightBackup(cfgPath string, w io.Writer) error {
book, err := config.LoadBook(cfgPath)
if err != nil {
return err
}
if _, err := os.Stat(book.ProjectDB); err != nil {
return nil // first run of a new book: no signed bank exists yet, nothing to back up
}
path, err := store.BackupSQLite(book.ProjectDB, backupDirFor(book.ProjectDB), backupStamp(time.Now()))
if err != nil {
return fmt.Errorf("pre-flight guard: refusing to start a paid run that could lose the signed bank: %w", err)
}
fmt.Fprintf(w, "pre-flight: backed up the project DB to %s (integrity_check green)\n", path)
return nil
}