Add msgvault backup: init, create, list, verify, and restore commands (#339)#428
Conversation
roborev: Combined Review (
|
roborev: Combined Review (
|
Two roborev findings on PR #428, both medium: LoadManifest trusted the JSON body as-is, so corrupted, hand-edited, or renamed manifest files were accepted by list, latest, and verify. It now recomputes the content-derived snapshot ID and rejects any manifest whose computed ID does not match both the requested filename and the embedded snapshot_id. A side effect is that forged parent cycles can no longer load at all (a cycle would need SHA-256 fixed-point IDs), so the mapChain cycle tests are replaced by rejected-at-load coverage and the depth-limit chain is built with genuine WriteManifest IDs; the verify geometry test now models a buggy writer with a self-consistent ID, the only way wrong geometry can still reach full verify. AttachmentRefs scanned MAX(size) into int64, but attachments.size is nullable: a content-bearing hash whose rows all lack size metadata made backup create fail. COALESCE to the same -1 sentinel thumbnails already use; capture resolves the real size from the file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
roborev: Combined Review (
|
7dc29e4 to
620a2e5
Compare
roborev: Combined Review (
|
roborev: Combined Review (
|
17be3ed to
f5473cc
Compare
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
Squashed branch commits: - fix: verify progress bar tracks blobs, not snapshots - feat: backup restore with layered self-proof - fix: address roborev findings on delta safety, storage paths, and progress - fix: gate namespaced-path snapshots behind manifest reader version 2 - fix: harden capture memory, extras placement, IDs, and map validation - fix: report and parallelize the changed-page pack phase of backup create - feat: show per-stage elapsed time and keep the restore proof line alive - feat: explain slow first TUI views when no analytics cache exists - fix: close extras traversal gap, stale ticker counts, cache-notice misfires - docs: correct when the daemon builds the analytics cache in the TUI guide - fix: report the daemon's analytics engine on /health for the TUI notice - fix: make backup restore and its tests portable to Windows - fix: resolve relative restore targets in the proof DSN, finish Windows lane - fix: allow backup freeze from a daemon bound to a non-loopback address - fix: honor cancellation in backup capture phases, refuse symlinked staging - test: skip the staging symlink test where symlink creation is denied Generated with Codex (GPT-5) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
879aaa9 to
25035c8
Compare
…ation TestRetireGeneration_DisablesStatementTimeout failed on CI: the test clamps statement_timeout=1ms on the pooled handle to prove the retire tx overrides it, but the verification COUNT afterwards can be handed the still-clamped pooled connection, and on a loaded runner even a COUNT exceeds 1ms (SQLSTATE 57014). Both timeout tests now pin the pool to one connection (the backfill timeout tests' existing pattern) and RESET the timeout after the operation under test, so verification never runs against the clamped session. Verified against a local pgvector/pgvector:pg16 container.
roborev: Combined Review (
|
Three review findings on the backup/restore path: - The restore-into-live-home guard only detected daemons compatible with this client, so a daemon left running across a CLI upgrade or downgrade — which still holds the archive database open — was invisible and --overwrite could clobber its msgvault.db. The guard now blocks on any responding daemon via findAnyDaemonRuntime. - The attachment predicates excluded storage paths with NOT LIKE 'http%', which also dropped valid local namespaced paths like http-cache/aa/<hash> from capture and restore while the stats proof still passed (both sides shared the predicate). Only genuine http:// and https:// URL schemes are excluded now, with predicate-level and end-to-end regression coverage. - Restored files were fsynced but the directories naming them were not (only the target root was synced), so a crash shortly after a successful restore could lose newly created nested paths. Restore now syncs every directory of the restored tree, deepest first, before reporting success — one sync per directory instead of per-file parent fsyncs in the already fsync-bound attachments phase.
roborev: Combined Review (
|
Two review findings on 76896f4: - The live-archive-home guard compared filepath.Abs strings, so a case-variant spelling on a case-insensitive filesystem (the macOS default) or a symlinked path to the same home slipped past it and could restore over a running daemon's database. When the string compare misses, the guard now resolves both paths with os.Stat and compares filesystem identity via os.SameFile; a symlink-alias case pins the behavior. - syncRestoredTree walked only under the target, so the target directory's own entry — and any missing ancestors os.MkdirAll created for it — were never fsynced in their parents; a crash after a successful restore into a brand-new path could still lose the whole tree. Restore now records the deepest pre-existing ancestor before creating the target and the durability pass climbs from the target's parent through that ceiling, keeping the deepest-first order.
roborev: Combined Review (
|
openPinnedSession built its SQLite DSN as dbPath+"?_busy_timeout=5000", so an archive path containing '?' (legal on POSIX filesystems) was cut at the first '?' and the driver opened — and created — a different, empty database file. The freeze/checkpoint session would then pin the wrong file while the raw page scan read opts.DBPath, producing an unfrozen or unrestorable snapshot. Extract restoredDBDSN's url.URL-based construction into sqliteURIDSN (absolute, slash-rooted file: URI immune to '?'/'#' in the path) and build the pinned-session DSN through it with _busy_timeout carried in the query. Regression test opens a frozen session on an "odd? archive#1.db" path and proves the pinned stats come from that file.
roborev: Combined Review (
|
Implements the backup and restore side of #339:
msgvault backup init / create / list / verify / restoreagainst an app-owned backup repository. Restore is included and proves itself before reporting success — see "Restore" below.Docs: usage guide · format reference
How this maps to the #339 proposal
Kept as proposed:
--include-config/--include-tokensflags, off by default. Added on top: capturing either into an unencrypted repository requires an explicit--allow-plaintext-secrets, so credentials can't leak by default.<aa>/<hash>layout).Where the design goes further than the proposal:
VACUUM INTO. Instead of a full-copy snapshot, the backup checkpoints the WAL and pins a read transaction on its own connection, coordinating with a running daemon through a short freeze window (loopback API, watchdog-protected). No double-disk requirement, no downtime; the daemon resumes writes while pages are scanned.backup createrefuses to run unfrozen against a live daemon.verify(full mode) re-derives every referenced blob's SHA-256 from its bytes, proves the page map covers the database exactly, and cross-checks manifest counts. Failures name the damaged blob and the pack file holding it.--quickdoes structural checks;--allcovers every snapshot, reading shared content once.min_reader_version, per-object magic+version on every binary format, per-snapshotmin_reader_version), so an older msgvault refuses a newer repository or snapshot with an explicit upgrade message instead of misreading it. Manifests are deterministic JSON; snapshot IDs are content-derived from the manifest bytes and re-checked on load, so a renamed or forged manifest is rejected.Restore
backup restore --target DIR [SNAPSHOT] [--overwrite]materializes any snapshot into a usable archive home and does not trust itself:PRAGMA integrity_checkand reproduce the manifest's recorded stats through exactly the queries capture ran inside the freeze; any mismatch fails the restore.--overwrite; overwrite merges but always removes the database and stale-wal/-shmsidecars first. Restoring into a running daemon's archive home is refused outright. Old backups go through normal schema migration on first open.Performance
Page scanning hashes on all CPUs while reads stay strictly sequential (safe for NAS/spinning disks with no flag). Verify, restore, attachment capture, and changed-page packing fan out workers with a
--jobsflag;--jobs 1serializes reads for spinning-disk repositories. Progress renders as in-place bars on a TTY and plain lines through pipes, with per-stage elapsed time that keeps counting through long quiet stages such as the restore proof'sintegrity_check.Changes
internal/pack(sealed pack container) andinternal/backup(repository, locking, incremental page capture, manifests, verify, restore).msgvault backup init | create | list | verify | restore;[backup]config section (repo,zstd_level);--jobson create/verify/restore;--progressrendering.backup createfrom the backup group — verify and restore always run locally.docs/usage/backup.md) and on-disk format reference (docs/architecture/backup-format.md), including the roadmap forverify --restore-check, encryption, and retention.Sequenced later (format hooks in place)
forget/pruneretention. Until prune ships, content purged from the archive persists in earlier snapshots — documented, with the privacy implications (including SQLite free-page remnants), in the usage guide.verify --restore-check: the full restore materialization proof against scratch space without writing a target.🤖 Generated with Claude Code