""" Where the raw copy of an uploaded audience file is kept. An upload is never just parsed and discarded, so the original file is stored as well. With SUPABASE_URL and SUPABASE_SERVICE_KEY set the file goes to a private Supabase Storage bucket (SUPABASE_BUCKET, default "uploads"). Without them it goes to a local folder, which is right for development and wrong for a host whose disk is wiped on restart. """ import os from pathlib import Path import httpx TIMEOUT_SECONDS = 30 CONTENT_TYPES = { "csv": "text/csv", "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", } class StorageError(Exception): """The file could not be stored. The upload is refused rather than kept only in memory.""" def _remote() -> tuple[str, str, str] | None: url = os.getenv("SUPABASE_URL", "").strip().rstrip("/") key = os.getenv("SUPABASE_SERVICE_KEY", "").strip() if not url or not key: return None return url, key, os.getenv("SUPABASE_BUCKET", "uploads").strip() or "uploads" def save_upload(org_id: str, database_id: str, ext: str, data: bytes) -> str: """Stores the original file as {org_id}/{database_id}.{ext}; returns where it went.""" name = f"{org_id}/{database_id}.{ext}" remote = _remote() if remote is None: path = Path(os.getenv("UPLOADS_DIR", "uploads")) / name path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(data) return str(path) url, key, bucket = remote try: res = httpx.post( f"{url}/storage/v1/object/{bucket}/{name}", content=data, headers={ "Authorization": f"Bearer {key}", "Content-Type": CONTENT_TYPES.get(ext.lower(), "application/octet-stream"), "x-upsert": "true", }, timeout=TIMEOUT_SECONDS, ) except httpx.HTTPError as err: raise StorageError(f"Could not reach file storage: {err.__class__.__name__}") from err if res.status_code >= 300: raise StorageError(f"File storage refused the file ({res.status_code})") return f"{bucket}/{name}"