Source code for mmm_framework.platform.backup
"""Online backup / restore for the SQLite sessions + auth store.
The entire platform state (sessions, auth users/tokens, run_metrics, langgraph
checkpoints) lives in one SQLite file with no backup tooling — a single disk loss
destroys everything. This adds a CONSISTENT online backup (SQLite's backup API,
safe while the app is running, WAL-aware) and a restore, plus a CLI::
python -m mmm_framework.platform.backup backup /backups/sessions-2026-06-25.db
python -m mmm_framework.platform.backup restore /backups/sessions-2026-06-25.db
This is the stop-gap for the single-SQLite risk until the Postgres migration
(action plan P1a). See technical-docs/disaster-recovery.md.
"""
from __future__ import annotations
import argparse
import sqlite3
from pathlib import Path
from . import sessions as sessions_store
def _default_db_path() -> Path:
return Path(sessions_store.DB_PATH)
def _sqlite_copy(src: Path, dest: Path) -> None:
"""Consistent online copy of ``src`` -> ``dest`` via the SQLite backup API."""
dest.parent.mkdir(parents=True, exist_ok=True)
src_conn = sqlite3.connect(str(src))
try:
dst_conn = sqlite3.connect(str(dest))
try:
with dst_conn:
src_conn.backup(dst_conn)
finally:
dst_conn.close()
finally:
src_conn.close()
[docs]
def backup_db(dest: str | Path, *, db_path: str | Path | None = None) -> Path:
"""Write a consistent snapshot of the live DB to ``dest``. Returns ``dest``."""
src = Path(db_path) if db_path else _default_db_path()
if not src.exists():
raise FileNotFoundError(f"Source database not found: {src}")
dest = Path(dest)
_sqlite_copy(src, dest)
return dest
[docs]
def restore_db(src: str | Path, *, db_path: str | Path | None = None) -> Path:
"""Restore the DB from a backup at ``src`` into the live path. Returns target."""
src = Path(src)
if not src.exists():
raise FileNotFoundError(f"Backup not found: {src}")
target = Path(db_path) if db_path else _default_db_path()
_sqlite_copy(src, target)
return target
[docs]
def main(argv: list[str] | None = None) -> None:
ap = argparse.ArgumentParser(description="Backup/restore the MMM sessions DB")
sub = ap.add_subparsers(dest="cmd", required=True)
b = sub.add_parser("backup", help="snapshot the live DB to a file")
b.add_argument("dest")
b.add_argument("--db", default=None, help="source DB path (default: sessions.db)")
r = sub.add_parser("restore", help="restore the DB from a backup file")
r.add_argument("src")
r.add_argument("--db", default=None, help="target DB path (default: sessions.db)")
args = ap.parse_args(argv)
if args.cmd == "backup":
out = backup_db(args.dest, db_path=args.db)
print(f"Backed up to {out}")
else:
out = restore_db(args.src, db_path=args.db)
print(f"Restored to {out}")
if __name__ == "__main__": # pragma: no cover
main()