diff --git a/upload_to_s3.py b/upload_to_s3.py index c2ef960..d66d540 100644 --- a/upload_to_s3.py +++ b/upload_to_s3.py @@ -1,17 +1,43 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +import hashlib +import json import os import sys from pathlib import Path import boto3 from botocore.config import Config +from botocore.exceptions import ClientError S3_PREFIX = "World of Warcraft" MANIFEST_S3_KEY = "manifest.json" +def compute_build_hash(files: list[dict]) -> str: + lines = [ + f'{item["path"]}:{item["size"]}:{item["sha256"]}' + for item in sorted(files, key=lambda item: item["path"]) + ] + return hashlib.sha256("\n".join(lines).encode("utf-8")).hexdigest() + + +def merge_manifests(base_manifest: dict, staging_manifest: dict) -> dict: + files_by_path = { + item["path"]: item + for item in base_manifest.get("files", []) + } + for item in staging_manifest.get("files", []): + files_by_path[item["path"]] = item + + files = [files_by_path[path] for path in sorted(files_by_path)] + return { + "buildHash": compute_build_hash(files), + "files": files, + } + + def load_dotenv(env_path: Path) -> dict[str, str]: values: dict[str, str] = {} if not env_path.exists(): @@ -61,6 +87,38 @@ def main() -> int: config=Config(s3={"addressing_style": "path"}) if use_path_style else None, ) + manifest_path = project_root / "manifest.json" + if not manifest_path.exists(): + print("ERROR: manifest.json not found", file=sys.stderr) + return 1 + + try: + staging_manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as error: + print(f"ERROR: unable to read local manifest.json: {error}", file=sys.stderr) + return 1 + + try: + response = s3.get_object(Bucket=bucket, Key=MANIFEST_S3_KEY) + production_manifest = json.loads(response["Body"].read()) + except ClientError as error: + error_code = error.response.get("Error", {}).get("Code") + if error_code not in {"NoSuchKey", "404"}: + print(f"ERROR: unable to read production manifest: {error}", file=sys.stderr) + return 1 + production_manifest = {"files": []} + except (UnicodeDecodeError, json.JSONDecodeError) as error: + print(f"ERROR: invalid production manifest: {error}", file=sys.stderr) + return 1 + + merged_manifest = merge_manifests(production_manifest, staging_manifest) + print( + "Manifest merge: " + f"production={len(production_manifest.get('files', []))}, " + f"staging={len(staging_manifest.get('files', []))}, " + f"result={len(merged_manifest['files'])}" + ) + dist_dir = project_root / "dist" if not dist_dir.exists(): print(f"ERROR: dist/ directory not found. Run build first.", file=sys.stderr) @@ -87,18 +145,26 @@ def main() -> int: errors = True # Загрузка манифеста - manifest_path = project_root / "manifest.json" - if not manifest_path.exists(): - print(f"ERROR: manifest.json not found", file=sys.stderr) + if errors: + print("\nFinished with errors; production manifest was not changed.") return 1 + manifest_path.write_text( + json.dumps(merged_manifest, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + newline="\n", + ) + print(f"Uploading manifest.json -> s3://{bucket}/{MANIFEST_S3_KEY}") try: s3.upload_file( str(manifest_path), bucket, MANIFEST_S3_KEY, - ExtraArgs={"ContentType": "application/json"}, + ExtraArgs={ + "ContentType": "application/json", + "CacheControl": "no-cache, no-store, must-revalidate", + }, ) print(f" OK") except Exception as e: