#!/usr/bin/env python3 """Create a consistent SQLite backup from the running Docker service.""" import argparse import subprocess import sys from datetime import datetime, timedelta, timezone from pathlib import Path from uuid import uuid4 CONTAINER_BACKUP = """import sqlite3 source = sqlite3.connect('/data/eternity.db') destination = sqlite3.connect('/tmp/eternity-backup.db') with destination: source.backup(destination) destination.close() source.close() """ def run(command, **kwargs): return subprocess.run(command, check=True, text=True, **kwargs) def main(): parser = argparse.ArgumentParser(description="Back up the live Eternity Project SQLite database.") parser.add_argument("--output-dir", default="backups", help="Directory for timestamped backup files") parser.add_argument("--keep-days", type=int, default=30, help="Delete backups older than this many days; use 0 to keep all") args = parser.parse_args() output_dir = Path(args.output_dir).resolve() output_dir.mkdir(parents=True, exist_ok=True) timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") destination = output_dir / f"eternity-{timestamp}.db" temporary_path = f"/tmp/eternity-backup-{uuid4().hex}.db" try: container_id = run(["docker", "compose", "ps", "-q", "eternityproject"], capture_output=True).stdout.strip() if not container_id: raise RuntimeError("The eternityproject Docker service must be running.") run(["docker", "exec", container_id, "python", "-c", CONTAINER_BACKUP.replace("/tmp/eternity-backup.db", temporary_path)]) run(["docker", "cp", f"{container_id}:{temporary_path}", str(destination)]) except (OSError, subprocess.CalledProcessError, RuntimeError) as error: print(f"DATABASE_BACKUP_FAILED: {error}", file=sys.stderr) return 1 finally: if "container_id" in locals() and container_id: subprocess.run(["docker", "exec", container_id, "rm", "-f", temporary_path], check=False) print(f"DATABASE_BACKUP_CREATED={destination}") if args.keep_days > 0: cutoff = datetime.now(timezone.utc) - timedelta(days=args.keep_days) removed = 0 for backup in output_dir.glob("eternity-*.db"): if backup != destination and datetime.fromtimestamp(backup.stat().st_mtime, timezone.utc) < cutoff: backup.unlink() removed += 1 print(f"DATABASE_BACKUP_RETENTION: removed={removed} keep_days={args.keep_days}") return 0 if __name__ == "__main__": raise SystemExit(main())