#!/usr/bin/env python3 """Publish only the missing immutable thumbnails for the fixed test-user batch.""" import fcntl import hashlib import json import os import re import subprocess import sys import urllib.request from pathlib import Path sys.path.insert(0, "/www/server/xingyu-im/ops") from baota_publish_test_users import database, env_values, query BATCH = "cn-adults-20260831-v1" BUCKET = "gz-1349751149" BACKUP_ROOT = Path("/www/backup") LOCK = Path("/www/server/xingyu-im/ops/application-release.lock") def digest(path): return hashlib.sha256(Path(path).read_bytes()).hexdigest() def profile_marker(conn): rows = query( conn, "SELECT p.user_id,p.avatar_url FROM user_profiles p JOIN users u ON u.id=p.user_id " "WHERE u.is_test=1 AND u.test_batch=%s ORDER BY p.user_id", (BATCH,), ) return hashlib.sha256(repr(rows).encode()).hexdigest(), rows def thumbnail_url(source): assert "/media/test-users/" + BATCH + "/" in source return re.sub(r"\.(?:jpeg|jpg|png|webp)$", "-thumb.jpg", source, flags=re.I) def publish(binary_name, expected_sha): assert os.geteuid() == 0 assert re.fullmatch(r"[a-f0-9]{64}", expected_sha) binary = Path(binary_name).resolve() assert binary.parent == Path("/tmp") and binary.is_file() assert digest(binary) == expected_sha and binary.read_bytes()[:4] == b"\x7fELF" os.chmod(binary, 0o700) with LOCK.open("a") as release_lock: fcntl.flock(release_lock, fcntl.LOCK_EX | fcntl.LOCK_NB) values = env_values() assert values["IM_ENV"] == "production" environment = dict(os.environ, **values) conn, _ = database(values) try: before_hash, rows = profile_marker(conn) assert len(rows) == 100 and len({url for _, url in rows}) == 10 assert query(conn, "SELECT config_value FROM system_configs WHERE config_key='storage.provider'")[0][0] == "tencent_cos" base_args = [ str(binary), "--confirm-database", "im", "--confirm-bucket", BUCKET, "--media-dir", "/www/wwwroot/xingyu-data/uploads", ] dry_run = subprocess.run(base_args, env=environment, text=True, capture_output=True, timeout=180, check=True) preview = json.loads(dry_run.stdout.strip().splitlines()[-1]) assert preview["dryRun"] is True and preview["users"] == 100 and len(preview["objects"]) == 10 stamp = __import__("time").strftime("%Y%m%d-%H%M%S") # The migration binary intentionally accepts only this audited backup prefix. backup = BACKUP_ROOT / ("xingyu-cos-avatars-thumbnails-" + stamp) completed = subprocess.run( base_args + ["--apply", "--backup-dir", str(backup)], env=environment, text=True, capture_output=True, timeout=600, check=True, ) result = json.loads(completed.stdout.strip().splitlines()[-1]) assert result["updated"] == 0 and result["users"] == 100 assert result["objects"] == 20 and result["publicURLsVerified"] == 20 after_hash, after_rows = profile_marker(conn) assert after_hash == before_hash and after_rows == rows verified = [] for source in sorted({url for _, url in rows}): url = thumbnail_url(source) request = urllib.request.Request(url, headers={"User-Agent": "xingyu-thumbnail-verify/1"}) with urllib.request.urlopen(request, timeout=30) as response: body = response.read(128 * 1024) assert response.status == 200 assert response.headers.get_content_type() == "image/jpeg" assert body.startswith(b"\xff\xd8") and body.endswith(b"\xff\xd9") assert 0 < len(body) < 128 * 1024 verified.append({"url": url, "bytes": len(body)}) record = { "backup": str(backup), "binarySha256": expected_sha, "databaseProfilesUnchanged": True, "verifiedObjects": result["objects"], "newThumbnailObjects": len(verified), "thumbnails": verified, } verification = backup / "thumbnail-verification.json" verification.write_text(json.dumps(record, ensure_ascii=False, indent=2)) os.chmod(verification, 0o600) print("PUBLISHED=" + json.dumps(record, ensure_ascii=False), flush=True) finally: conn.close() if __name__ == "__main__": publish(*sys.argv[1:])