""" Supabase Storage utility for PolicyEye. Handles uploading and downloading policy PDFs to/from Supabase Storage. This replaces local disk storage with cloud-hosted, CDN-served file storage. Bucket: policy-pdfs (public, created via Supabase dashboard) Bucket: grievance-reports (private, for generated PDF reports) """ import os import logging import uuid from datetime import datetime logger = logging.getLogger(__name__) SUPABASE_URL = os.getenv("SUPABASE_URL", "") SUPABASE_KEY = os.getenv("SUPABASE_KEY", "") BUCKET_NAME = "policy-pdfs" def _get_client(): """Lazy-init Supabase client.""" from supabase import create_client if not SUPABASE_URL or not SUPABASE_KEY: raise RuntimeError( "SUPABASE_URL and SUPABASE_KEY must be set in .env" ) return create_client(SUPABASE_URL, SUPABASE_KEY) def upload_pdf(pdf_bytes: bytes, original_filename: str, bucket_name: str = BUCKET_NAME) -> dict: """ Upload a PDF to Supabase Storage. Args: pdf_bytes: Raw PDF content original_filename: Original name of the uploaded file bucket_name: Supabase bucket name (default: policy-pdfs) Returns: { "storage_path": str, # Path within the bucket "public_url": str, # CDN-served public URL "size_kb": float, } """ client = _get_client() # Generate a unique filename to avoid collisions timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") safe_name = original_filename.replace(" ", "_").lower() storage_path = f"{timestamp}_{uuid.uuid4().hex[:8]}_{safe_name}" # Upload to Supabase Storage client.storage.from_(bucket_name).upload( path=storage_path, file=pdf_bytes, file_options={"content-type": "application/pdf"}, ) # Get public URL public_url = client.storage.from_(bucket_name).get_public_url(storage_path) size_kb = len(pdf_bytes) / 1024 logger.info( f"[Storage] Uploaded {safe_name} to Supabase " f"({size_kb:.1f}KB) → {storage_path}" ) return { "storage_path": storage_path, "public_url": public_url, "size_kb": round(size_kb, 1), } def download_pdf(storage_path: str, bucket_name: str = BUCKET_NAME) -> bytes: """ Download a PDF from Supabase Storage. Args: storage_path: Path within the bucket (from upload_pdf return value) bucket_name: Supabase bucket name Returns: Raw PDF bytes """ client = _get_client() data = client.storage.from_(bucket_name).download(storage_path) logger.info(f"[Storage] Downloaded {storage_path} from {bucket_name} ({len(data)/1024:.1f}KB)") return data def delete_pdf(storage_path: str, bucket_name: str = BUCKET_NAME) -> bool: """Delete a PDF from Supabase Storage.""" try: client = _get_client() client.storage.from_(bucket_name).remove([storage_path]) logger.info(f"[Storage] Deleted {storage_path} from {bucket_name}") return True except Exception as e: logger.error(f"[Storage] Delete failed for {storage_path} in {bucket_name}: {e}") return False def upload_report(local_filepath: str) -> dict: """ Upload a generated report PDF from local disk to Supabase. Args: local_filepath: Path to the local PDF file Returns: dict with storage_path and public_url (same format as upload_pdf) """ if not os.path.exists(local_filepath): raise FileNotFoundError(f"Report not found: {local_filepath}") filename = os.path.basename(local_filepath) with open(local_filepath, "rb") as f: pdf_bytes = f.read() return upload_pdf(pdf_bytes, filename, bucket_name="grievance-reports")