import base64 import hashlib import hmac from time import time from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit from .config import get_settings SIGNED_QUERY_KEYS = {"OSSAccessKeyId", "Expires", "Signature"} MEDIA_URL_EXPIRES_SECONDS = 3600 def normalized_oss_host(endpoint: str, bucket: str) -> tuple[str, str]: raw = endpoint.strip().rstrip("/") if "://" not in raw: raw = f"https://{raw}" parsed = urlsplit(raw) if parsed.scheme not in {"http", "https"} or not parsed.netloc: raise ValueError("invalid OSS endpoint") host = parsed.netloc if not host.lower().startswith(f"{bucket.lower()}."): host = f"{bucket}.{host}" return parsed.scheme, host def sign_oss_get_url( url: str, *, access_key_id: str, access_key_secret: str, endpoint: str, bucket: str, expires_at: int, ) -> str: scheme, expected_host = normalized_oss_host(endpoint, bucket) parsed = urlsplit(url) if parsed.scheme != scheme or parsed.netloc.lower() != expected_host.lower(): return url path = parsed.path or "/" canonical_resource = f"/{bucket}{path}" string_to_sign = f"GET\n\n\n{expires_at}\n{canonical_resource}" signature = base64.b64encode( hmac.new( access_key_secret.strip().encode("utf-8"), string_to_sign.encode("utf-8"), hashlib.sha1, ).digest() ).decode("ascii") query = [ (key, value) for key, value in parse_qsl(parsed.query, keep_blank_values=True) if key not in SIGNED_QUERY_KEYS ] query.extend( [ ("OSSAccessKeyId", access_key_id.strip()), ("Expires", str(expires_at)), ("Signature", signature), ] ) return urlunsplit((scheme, expected_host, path, urlencode(query), "")) def resolve_media_url(url: str | None) -> str | None: if not url or not isinstance(url, str): return url settings = get_settings() values = ( settings.oss_access_key_id, settings.oss_access_key_secret, settings.oss_endpoint, settings.oss_bucket_name, ) if not all(value and value.strip() for value in values): return url try: return sign_oss_get_url( url, access_key_id=settings.oss_access_key_id, access_key_secret=settings.oss_access_key_secret, endpoint=settings.oss_endpoint, bucket=settings.oss_bucket_name, expires_at=int(time()) + MEDIA_URL_EXPIRES_SECONDS, ) except (TypeError, ValueError): return url