kussssh/IPO-Analyzer
0
1"""2Supabase-backed durable file storage helpers.3 4When RUNTIME_STORAGE_BACKEND=supabase the backend keeps only temporary scratch5files on the local machine and stores PDFs / generated artifacts in Supabase6Storage.7"""8from __future__ import annotations9 10import json11import re12import tempfile13from functools import lru_cache14from pathlib import Path15from typing import Any, Optional16 17from supabase import create_client18from supabase.lib.client_options import SyncClientOptions19 20from backend.config import (21 RUNTIME_ROOT,22 SUPABASE_SERVICE_ROLE_KEY,23 SUPABASE_URL,24 USE_SUPABASE_RUNTIME,25)26 27 28SCRATCH_DIR = RUNTIME_ROOT / "scratch"29SCRATCH_DIR.mkdir(parents=True, exist_ok=True)30 31 32def _safe_slug(value: str, fallback: str = "file") -> str:33 cleaned = re.sub(r"[^\w\-.]+", "_", (value or "").strip()).strip("_")34 return cleaned or fallback35 36 37@lru_cache(maxsize=1)38def get_storage_client():39 if not USE_SUPABASE_RUNTIME:40 raise RuntimeError("Supabase storage client requested while runtime backend is local")41 if not SUPABASE_URL or not SUPABASE_SERVICE_ROLE_KEY:42 raise RuntimeError("Supabase storage is not configured")43 return create_client(44 SUPABASE_URL,45 SUPABASE_SERVICE_ROLE_KEY,46 options=SyncClientOptions(auto_refresh_token=False, persist_session=False),47 )48 49 50def build_document_storage_key(51 company_id: int,52 doc_type: str,53 pdf_sha256: str,54 filename: Optional[str] = None,55) -> str:56 base_name = _safe_slug(Path(filename or f"{doc_type}.pdf").name, fallback=f"{doc_type}.pdf")57 return f"documents/{company_id}/{doc_type}/{pdf_sha256}/{base_name}"58 59 60def build_result_storage_key(company_id: int, doc_type: str, company_name: str) -> str:61 safe_company = _safe_slug(company_name, fallback=f"company_{company_id}")62 return f"results/{company_id}/{doc_type}/{safe_company}_{doc_type}.json"63 64 65def upload_file(bucket: str, storage_key: str, local_path: str | Path, content_type: str) -> str:66 client = get_storage_client()67 with open(local_path, "rb") as handle:68 client.storage.from_(bucket).upload(69 path=storage_key,70 file=handle,71 file_options={"content-type": content_type, "upsert": "true"},72 )73 return storage_key74 75 76def upload_json(bucket: str, storage_key: str, payload: Any) -> str:77 client = get_storage_client()78 data = json.dumps(payload, indent=2, default=str).encode("utf-8")79 client.storage.from_(bucket).upload(80 path=storage_key,81 file=data,82 file_options={"content-type": "application/json", "upsert": "true"},83 )84 return storage_key85 86 87def download_to_temp(bucket: str, storage_key: str, *, suffix: str = "") -> str:88 client = get_storage_client()89 data = client.storage.from_(bucket).download(storage_key)90 tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix, dir=SCRATCH_DIR)91 try:92 tmp.write(data)93 tmp.flush()94 return tmp.name95 finally:96 tmp.close()97 98 99def stage_remote_pdf(storage_bucket: Optional[str], storage_key: Optional[str]) -> Optional[str]:100 if not storage_bucket or not storage_key:101 return None102 return download_to_temp(storage_bucket, storage_key, suffix=".pdf")103 104 105def cleanup_temp_file(path: Optional[str]) -> None:106 if not path:107 return108 try:109 candidate = Path(path)110 if candidate.exists():111 candidate.unlink(missing_ok=True)112 except Exception:113 pass114 