"""Validate a privacy-safe public-avatar migration manifest.""" from __future__ import annotations import argparse import asyncio import json import os import sys from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent API_ROOT = REPO_ROOT / "apps" / "api" if str(API_ROOT) not in sys.path: sys.path.insert(0, str(API_ROOT)) os.chdir(API_ROOT) from app.config import settings # noqa: E402 from app.upload_storage import ( # noqa: E402 validate_current_avatar_references, validate_upload_manifest, ) from public_runtime_database_identity import ( # noqa: E402 connected_database_target_sha256, ) async def _load_current_runtime_db_state() -> tuple[list[str], str]: import asyncpg connection = await asyncpg.connect(settings.database_url) try: async with connection.transaction(isolation="repeatable_read", readonly=True): target_sha256 = await connected_database_target_sha256(connection) rows = await connection.fetch( """ SELECT avatar_url FROM app.app_user WHERE btrim(avatar_url) LIKE '/uploads/%' ORDER BY avatar_url """ ) return [str(row["avatar_url"]) for row in rows], target_sha256 finally: await connection.close() def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--upload-root", required=True) parser.add_argument("--manifest-path", required=True) parser.add_argument("--expected-manifest-sha256", required=True) parser.add_argument("--expected-write-freeze-path", required=True) args = parser.parse_args() try: current_urls, database_target = asyncio.run(_load_current_runtime_db_state()) proof = validate_upload_manifest( upload_root=Path(args.upload_root), manifest_path=Path(args.manifest_path), expected_manifest_sha256=args.expected_manifest_sha256, expected_write_freeze_path=Path(args.expected_write_freeze_path), expected_database_target_sha256=database_target, verify_preserved_objects=True, reject_unbound_extras=False, ) current = validate_current_avatar_references( upload_root=Path(args.upload_root), avatar_urls=current_urls, initial_manifest=proof, expected_database_target_sha256=database_target, ) except (OSError, ValueError): print( json.dumps( {"status": "failed", "reason": "upload_storage_contract_failed"}, ensure_ascii=True, separators=(",", ":"), ) ) return 1 except Exception: print( json.dumps( {"status": "failed", "reason": "current_db_inventory_unavailable"}, ensure_ascii=True, separators=(",", ":"), ) ) return 1 print( json.dumps( { "status": "passed", "manifest_sha256": proof.manifest_sha256, "root_path_sha256": proof.root_path_sha256, "required_object_count": proof.required_object_count, "preserved_object_count": proof.preserved_object_count, "preserved_total_size_bytes": proof.preserved_total_size_bytes, "preserved_decode_valid_count": ( proof.preserved_decode_valid_count ), "preserved_decode_invalid_count": ( proof.preserved_decode_invalid_count ), "required_decode_invalid_object_count": ( proof.required_decode_invalid_object_count ), "required_decode_invalid_reference_count": ( proof.required_decode_invalid_reference_count ), "preserved_object_set_sha256": proof.preserved_object_set_sha256, "required_reference_count": proof.required_reference_count, "reference_set_sha256": proof.reference_set_sha256, "write_freeze_token_sha256": proof.write_freeze_token_sha256, "current_object_count": current.object_count, "current_reference_count": current.reference_count, "current_reference_set_sha256": current.reference_set_sha256, "current_decode_invalid_object_count": ( current.decode_invalid_object_count ), "current_decode_invalid_reference_count": ( current.decode_invalid_reference_count ), "database_target_sha256": database_target, }, ensure_ascii=True, separators=(",", ":"), ) ) return 0 if __name__ == "__main__": raise SystemExit(main())