Spaces:
Runtime error
Runtime error
| import hashlib | |
| import hmac | |
| import os | |
| from datetime import datetime, timezone | |
| from urllib.parse import quote | |
| import requests | |
| from fastapi import HTTPException | |
| R2_REGION = "auto" | |
| R2_SERVICE = "s3" | |
| R2_SIGNED_URL_EXPIRES_SECONDS = int(os.getenv("R2_SIGNED_URL_EXPIRES_SECONDS", "600")) | |
| def _env(name: str) -> str: | |
| return os.getenv(name, "").strip() | |
| def r2_configured() -> bool: | |
| return all( | |
| _env(name) | |
| for name in ( | |
| "R2_ACCOUNT_ID", | |
| "R2_BUCKET_NAME", | |
| "R2_ACCESS_KEY_ID", | |
| "R2_SECRET_ACCESS_KEY", | |
| ) | |
| ) | |
| def require_r2_configured(): | |
| if not r2_configured(): | |
| raise HTTPException(status_code=503, detail="R2 artifact storage is not configured.") | |
| def get_score_storage_keys(user_id: str, score_id: str) -> dict: | |
| base = f"users/{user_id}/scores/{score_id}" | |
| return { | |
| "original_musicxml": f"{base}/original.musicxml", | |
| "full_html": f"{base}/renders/full.html", | |
| "fingered_html": f"{base}/renders/fingered.html", | |
| "full_svg": f"{base}/renders/full.svg", | |
| "full_pdf": f"{base}/exports/full.pdf", | |
| "cache_prefix": f"{base}/cache", | |
| } | |
| def _endpoint_host() -> str: | |
| return f"{_env('R2_ACCOUNT_ID')}.r2.cloudflarestorage.com" | |
| def _canonical_uri(bucket: str, key: str) -> str: | |
| parts = [quote(part, safe="-_.~") for part in key.split("/")] | |
| return f"/{quote(bucket, safe='-_.~')}/{'/'.join(parts)}" | |
| def _object_url(key: str) -> str: | |
| return f"https://{_endpoint_host()}{_canonical_uri(_env('R2_BUCKET_NAME'), key)}" | |
| def _signing_key(date_stamp: str) -> bytes: | |
| secret = _env("R2_SECRET_ACCESS_KEY").encode("utf-8") | |
| k_date = hmac.new(b"AWS4" + secret, date_stamp.encode("utf-8"), hashlib.sha256).digest() | |
| k_region = hmac.new(k_date, R2_REGION.encode("utf-8"), hashlib.sha256).digest() | |
| k_service = hmac.new(k_region, R2_SERVICE.encode("utf-8"), hashlib.sha256).digest() | |
| return hmac.new(k_service, b"aws4_request", hashlib.sha256).digest() | |
| def _signature(string_to_sign: str, date_stamp: str) -> str: | |
| return hmac.new(_signing_key(date_stamp), string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest() | |
| def _credential_scope(date_stamp: str) -> str: | |
| return f"{date_stamp}/{R2_REGION}/{R2_SERVICE}/aws4_request" | |
| def _amz_dates() -> tuple[str, str]: | |
| now = datetime.now(timezone.utc) | |
| return now.strftime("%Y%m%dT%H%M%SZ"), now.strftime("%Y%m%d") | |
| def upload_r2_artifact(*, key: str, body: str | bytes, content_type: str) -> str: | |
| require_r2_configured() | |
| body_bytes = body.encode("utf-8") if isinstance(body, str) else body | |
| payload_hash = hashlib.sha256(body_bytes).hexdigest() | |
| amz_date, date_stamp = _amz_dates() | |
| host = _endpoint_host() | |
| canonical_uri = _canonical_uri(_env("R2_BUCKET_NAME"), key) | |
| signed_headers = "content-type;host;x-amz-content-sha256;x-amz-date" | |
| canonical_headers = ( | |
| f"content-type:{content_type}\n" | |
| f"host:{host}\n" | |
| f"x-amz-content-sha256:{payload_hash}\n" | |
| f"x-amz-date:{amz_date}\n" | |
| ) | |
| canonical_request = "\n".join( | |
| [ | |
| "PUT", | |
| canonical_uri, | |
| "", | |
| canonical_headers, | |
| signed_headers, | |
| payload_hash, | |
| ] | |
| ) | |
| credential_scope = _credential_scope(date_stamp) | |
| string_to_sign = "\n".join( | |
| [ | |
| "AWS4-HMAC-SHA256", | |
| amz_date, | |
| credential_scope, | |
| hashlib.sha256(canonical_request.encode("utf-8")).hexdigest(), | |
| ] | |
| ) | |
| authorization = ( | |
| "AWS4-HMAC-SHA256 " | |
| f"Credential={_env('R2_ACCESS_KEY_ID')}/{credential_scope}, " | |
| f"SignedHeaders={signed_headers}, " | |
| f"Signature={_signature(string_to_sign, date_stamp)}" | |
| ) | |
| try: | |
| response = requests.put( | |
| _object_url(key), | |
| data=body_bytes, | |
| headers={ | |
| "Authorization": authorization, | |
| "Content-Type": content_type, | |
| "Host": host, | |
| "x-amz-content-sha256": payload_hash, | |
| "x-amz-date": amz_date, | |
| }, | |
| timeout=30, | |
| ) | |
| except requests.exceptions.RequestException as exc: | |
| raise HTTPException(status_code=503, detail="R2 upload failed. Try again.") from exc | |
| if response.status_code >= 400: | |
| raise HTTPException(status_code=503, detail=f"R2 upload failed: {response.text[:200]}") | |
| return key | |
| def get_r2_artifact_download_url(key: str, *, expires_in: int | None = None) -> str: | |
| require_r2_configured() | |
| expires = int(expires_in or R2_SIGNED_URL_EXPIRES_SECONDS) | |
| amz_date, date_stamp = _amz_dates() | |
| credential_scope = _credential_scope(date_stamp) | |
| credential = f"{_env('R2_ACCESS_KEY_ID')}/{credential_scope}" | |
| host = _endpoint_host() | |
| signed_headers = "host" | |
| query_params = { | |
| "X-Amz-Algorithm": "AWS4-HMAC-SHA256", | |
| "X-Amz-Credential": credential, | |
| "X-Amz-Date": amz_date, | |
| "X-Amz-Expires": str(expires), | |
| "X-Amz-SignedHeaders": signed_headers, | |
| } | |
| canonical_query = "&".join( | |
| f"{quote(k, safe='-_.~')}={quote(v, safe='-_.~')}" | |
| for k, v in sorted(query_params.items()) | |
| ) | |
| canonical_request = "\n".join( | |
| [ | |
| "GET", | |
| _canonical_uri(_env("R2_BUCKET_NAME"), key), | |
| canonical_query, | |
| f"host:{host}\n", | |
| signed_headers, | |
| "UNSIGNED-PAYLOAD", | |
| ] | |
| ) | |
| string_to_sign = "\n".join( | |
| [ | |
| "AWS4-HMAC-SHA256", | |
| amz_date, | |
| credential_scope, | |
| hashlib.sha256(canonical_request.encode("utf-8")).hexdigest(), | |
| ] | |
| ) | |
| signature = _signature(string_to_sign, date_stamp) | |
| return f"{_object_url(key)}?{canonical_query}&X-Amz-Signature={signature}" | |