"""Database backups (pg_dump custom format, gzip-compressed by pg_dump) into AIA_DATA_DIR/backups. Off-node copies: scripts/backup-offnode.sh. The dataset is worth more than the code — keep 30 dailies.""" from __future__ import annotations import os import shutil import subprocess from datetime import UTC, datetime from pathlib import Path from urllib.parse import urlparse from aiatlas.config import settings def _pg_dump() -> str: for candidate in ("/opt/homebrew/opt/postgresql@17/bin/pg_dump", "/opt/homebrew/bin/pg_dump", "/usr/local/bin/pg_dump", "pg_dump"): if shutil.which(candidate) or os.path.exists(candidate): return candidate raise RuntimeError("pg_dump not found") def backup_database(keep: int = 30) -> Path: settings.ensure_dirs() url = urlparse(settings.sync_database_url) stamp = datetime.now(UTC).strftime("%Y%m%d-%H%M%S") out = settings.backups_dir / f"aiatlas-{stamp}.dump" env = dict(os.environ) if url.password: env["PGPASSWORD"] = url.password cmd = [_pg_dump(), "-Fc", "-Z", "6", "-f", str(out), "-h", url.hostname or "127.0.0.1", "-p", str(url.port or 5432), "-U", url.username or "aiatlas", (url.path or "/aiatlas").lstrip("/")] subprocess.run(cmd, check=True, env=env, timeout=3600) dumps = sorted(settings.backups_dir.glob("aiatlas-*.dump")) for old in dumps[:-keep]: old.unlink(missing_ok=True) return out __all__ = ["backup_database"]