"""Restore FabledCurator from its off-site bucket, from a shell in the container. The Backup screen does the same; this is for an install whose page you can't reach yet, or for scripting a rebuild. python -m backend.app.scripts.restore list python -m backend.app.scripts.restore database --dump latest --yes python -m backend.app.scripts.restore files On a brand-new install with no destination saved, name it on the command line: --endpoint, --bucket and --access-key, with the secret key in the FC_BACKUP_SECRET_KEY environment variable (or typed at the prompt). It is checked by listing the bucket, then saved as this install's destination, sealed, exactly as the settings page saves it. `database` replaces the database with the chosen dump here, then hands the files to the worker; their progress shows on the Backup screen. `files` fetches only what is missing from disk and leaves the database alone. """ from __future__ import annotations import argparse import getpass import os import sys from ..models import ImportSettings from ..services import backup_destination, backup_restore from ..services.backup_s3 import S3Error from ..tasks._sync_engine import sync_session_factory def _human(n: int) -> str: for unit in ("B", "KB", "MB", "GB", "TB"): if n < 1000 or unit == "TB": return f"{n:.0f} {unit}" if unit == "B" else f"{n:.1f} {unit}" n /= 1000 return str(n) def _destination(args) -> object: """The target to restore from: the one named on the command line, saved first, or the one already saved.""" session_factory = sync_session_factory() if args.endpoint or args.bucket or args.access_key: fields, why = backup_destination.clean_fields({ "endpoint": args.endpoint, "bucket": args.bucket, "access_key": args.access_key, "region": args.region, }) if fields is None: raise SystemExit(f"Destination not usable: {why}.") secret = os.environ.get("FC_BACKUP_SECRET_KEY") or getpass.getpass("Secret key: ") target = backup_destination.target_for(fields, secret.strip() or None) if target is None: raise SystemExit("Name the address, bucket, access key and secret key.") check = backup_destination.check_target(target) print(check["message"]) if not check["ok"]: raise SystemExit(1) with session_factory() as session: settings = ImportSettings.load_sync(session) limit = settings.backup_upload_limit_mbps backup_restore.keep_destination(session, target, limit) print("Saved as this install's off-site destination.") return target with session_factory() as session: target = backup_destination.saved_target(ImportSettings.load_sync(session)) if target is None: raise SystemExit("No off-site destination is saved; name one with " "--endpoint, --bucket and --access-key.") return target def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(prog="python -m backend.app.scripts.restore", description=__doc__.split("\n\n")[0]) parser.add_argument("action", choices=("list", "database", "files")) parser.add_argument("--dump", default="latest", help="the dump's key from `list`, or latest (the default)") parser.add_argument("--yes", action="store_true", help="confirm replacing this install's database") parser.add_argument("--endpoint") parser.add_argument("--bucket") parser.add_argument("--access-key") parser.add_argument("--region", default=None) args = parser.parse_args(argv) try: target = _destination(args) if args.action == "list": for d in backup_restore.list_dumps(target): print(f"{d.key} {_human(d.size_bytes)}") return 0 from ..tasks.backup import restore_offsite_task if args.action == "files": restore_offsite_task.delay(files_only=True) print("Queued: the worker fetches every missing file; watch the Backup screen.") return 0 dump = backup_restore.find_dump(target, args.dump) if not args.yes: print(f"This replaces this install's database with {dump.key} " f"({_human(dump.size_bytes)}). Run again with --yes to go ahead.") return 2 print(f"Restoring {dump.key}; this takes a while for a large library…") result = restore_offsite_task(dump_key=dump.key) if result.get("skipped"): print(f"Not started: {result['skipped']}.") return 1 print("The database is restored. The worker is fetching the library's files; " "watch the Backup screen.") return 0 except S3Error as exc: print(f"Couldn't restore: {exc}", file=sys.stderr) return 1 if __name__ == "__main__": sys.exit(main())