Files
kefu/im/deploy/baota_publish_test_avatar_thumbnails.py
2026-09-03 08:38:17 +08:00

118 lines
4.6 KiB
Python

#!/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:])