notepilot / src /r2_artifacts.py
jasonmdong's picture
Sync from GitHub via hub-sync
c057aea verified
Raw
History Blame Contribute Delete
5.84 kB
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}"