아바타 저장소 승격 계약을 완성

This commit is contained in:
Yun Chan 2026-08-29 23:58:33 +09:00
parent ac9b702688
commit ccdcfcd2f5
36 changed files with 14734 additions and 222 deletions

View file

@ -0,0 +1,136 @@
"""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())