| |
| """Verify or restore a DocDoe PostgreSQL custom-format backup. |
| |
| Restore is deliberately locked behind both --execute and an exact database |
| name confirmation. Without them the command only verifies the archive and |
| prints the intended target. Credentials never appear in the command line. |
| """ |
| from __future__ import annotations |
|
|
| import argparse |
| import json |
| import os |
| import shutil |
| import subprocess |
| import sys |
| from pathlib import Path |
|
|
| from dotenv import load_dotenv |
|
|
| from database_backup import BACKEND_DIR, file_sha256, parse_postgres_url |
|
|
|
|
| def verify_manifest(backup_path: Path) -> None: |
| manifest_path = backup_path.with_suffix(".manifest.json") |
| if not manifest_path.exists(): |
| raise ValueError(f"Backup manifest is missing: {manifest_path}") |
| manifest = json.loads(manifest_path.read_text(encoding="utf-8")) |
| expected = str(manifest.get("sha256") or "") |
| actual = file_sha256(backup_path) |
| if not expected or expected != actual: |
| raise ValueError("Backup checksum does not match its manifest") |
|
|
|
|
| def restore_backup( |
| *, |
| backup_path: Path, |
| database_url: str, |
| execute: bool, |
| confirm_database: str | None, |
| replace_existing: bool, |
| ) -> None: |
| target = parse_postgres_url(database_url) |
| backup_path = backup_path.expanduser().resolve() |
| if not backup_path.is_file(): |
| raise ValueError(f"Backup file does not exist: {backup_path}") |
| verify_manifest(backup_path) |
|
|
| pg_restore = shutil.which("pg_restore") |
| if not pg_restore: |
| raise RuntimeError("pg_restore is not installed or not available on PATH") |
|
|
| env = target.process_environment() |
| verification = subprocess.run( |
| [pg_restore, "--list", str(backup_path)], |
| env=env, |
| stdout=subprocess.DEVNULL, |
| stderr=subprocess.PIPE, |
| text=True, |
| check=False, |
| ) |
| if verification.returncode != 0: |
| detail = (verification.stderr or "unknown error").strip().splitlines()[-1] |
| raise RuntimeError(f"Backup archive verification failed: {detail[:240]}") |
|
|
| print(f"[OK] Archive and checksum verified: {backup_path.name}") |
| print(f"[TARGET] {target.host}:{target.port}/{target.database}") |
| if not execute: |
| print("[VERIFY ONLY] Add --execute and --confirm-database to restore.") |
| return |
| if confirm_database != target.database: |
| raise ValueError("--confirm-database must exactly match the target database name") |
|
|
| command = [ |
| pg_restore, |
| "--exit-on-error", |
| "--no-owner", |
| "--no-privileges", |
| "--dbname", |
| target.database, |
| ] |
| if replace_existing: |
| command.extend(["--clean", "--if-exists"]) |
| command.append(str(backup_path)) |
| restored = subprocess.run( |
| command, |
| env=env, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE, |
| text=True, |
| check=False, |
| ) |
| if restored.returncode != 0: |
| detail = (restored.stderr or "unknown error").strip().splitlines()[-1] |
| raise RuntimeError(f"Database restore failed: {detail[:240]}") |
| print("[OK] Restore completed. Run application smoke tests before reopening traffic.") |
|
|
|
|
| def main(argv: list[str] | None = None) -> int: |
| load_dotenv(BACKEND_DIR / ".env", override=False) |
| parser = argparse.ArgumentParser(description="Verify or restore a DocDoe database backup.") |
| parser.add_argument("backup", type=Path) |
| parser.add_argument("--database-url", default=os.getenv("RESTORE_DATABASE_URL")) |
| parser.add_argument("--execute", action="store_true") |
| parser.add_argument("--confirm-database") |
| parser.add_argument("--replace-existing", action="store_true") |
| args = parser.parse_args(argv) |
| try: |
| restore_backup( |
| backup_path=args.backup, |
| database_url=args.database_url or "", |
| execute=args.execute, |
| confirm_database=args.confirm_database, |
| replace_existing=args.replace_existing, |
| ) |
| except (OSError, RuntimeError, ValueError, json.JSONDecodeError) as exc: |
| print(f"[ERROR] {exc}", file=sys.stderr) |
| return 1 |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| raise SystemExit(main()) |
|
|