72 lines
3.4 KiB
Go
72 lines
3.4 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 (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"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
|
|
}
|
|
// Only ABSENT means "fresh book". Any other stat error read that way silently skips the guard.
|
|
if _, err := os.Stat(book.ProjectDB); err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil // first run of a new book: no signed bank exists yet, nothing to back up
|
|
}
|
|
return fmt.Errorf("pre-flight guard: cannot stat the project database %s: %w", book.ProjectDB, err)
|
|
}
|
|
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
|
|
}
|