118 lines
4.6 KiB
Python
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:])
|