kamau1's picture
feat: file naming
b26895a
"""
Supabase Integration
Handles Supabase Storage for document uploads (PDFs, DOCX, etc.)
"""
from fastapi import UploadFile, HTTPException
from app.config import settings
from supabase import create_client, Client
from typing import Dict, Any
import logging
import uuid
logger = logging.getLogger(__name__)
class SupabaseStorageService:
"""Service for uploading files to Supabase Storage"""
@staticmethod
def get_client() -> Client:
"""Get Supabase client"""
return create_client(settings.SUPABASE_URL, settings.SUPABASE_SERVICE_KEY)
@staticmethod
def get_bucket_name(entity_type: str) -> str:
"""
Get Supabase storage bucket based on entity type
Bucket structure:
- documents-users - User documents (IDs, contracts, etc.)
- documents-tickets - Ticket documents
- documents-projects - Project documents
- documents-general - Everything else
"""
bucket_map = {
'user': 'documents-users',
'ticket': 'documents-tickets',
'project': 'documents-projects',
'client': 'documents-clients',
'contractor': 'documents-contractors'
}
return bucket_map.get(entity_type, 'documents-general')
@staticmethod
def get_file_path(entity_type: str, entity_id: str, file_name: str) -> str:
"""
Generate file path in bucket
Format: {entity_type}/{entity_id}/{filename}
Note: filename should already be contextual and unique from FileNamingService
"""
return f"{entity_type}/{entity_id}/{file_name}"
@staticmethod
async def upload(
file: UploadFile,
entity_type: str,
entity_id: str,
document_type: str,
contextual_filename: str = None
) -> Dict[str, Any]:
"""
Upload file to Supabase Storage
Args:
file: The file to upload
entity_type: Type of entity (user, ticket, etc.)
entity_id: ID of the entity
document_type: Type of document
contextual_filename: Optional contextual filename to use instead of original
Returns:
Dict containing upload response with public URL
Raises:
HTTPException: If upload fails
"""
try:
client = SupabaseStorageService.get_client()
# Read file content
file_content = await file.read()
# Use contextual filename if provided, otherwise use original
filename_to_use = contextual_filename if contextual_filename else file.filename
# Get bucket and file path
bucket_name = SupabaseStorageService.get_bucket_name(entity_type)
file_path = SupabaseStorageService.get_file_path(entity_type, entity_id, filename_to_use)
# Upload to Supabase Storage
response = client.storage.from_(bucket_name).upload(
path=file_path,
file=file_content,
file_options={
"content-type": file.content_type,
"upsert": "false" # Don't overwrite existing files
}
)
# Store permanent reference (bucket + path)
# Don't store signed URLs as they expire
# Instead, store a reference URL that can be used to generate signed URLs on-demand
permanent_url = f"supabase://{bucket_name}/{file_path}"
logger.info(f"Uploaded file to Supabase Storage: {permanent_url}")
return {
'public_url': permanent_url, # Permanent reference, not a signed URL
'bucket': bucket_name,
'path': file_path,
'size': len(file_content),
'content_type': file.content_type,
'storage_type': 'supabase'
}
except Exception as e:
logger.error(f"Supabase Storage upload failed: {str(e)}")
raise HTTPException(
status_code=500,
detail=f"Failed to upload file to Supabase Storage: {str(e)}"
)
@staticmethod
def get_signed_url(bucket: str, file_path: str, expires_in: int = 3600) -> str:
"""
Generate a signed URL for accessing a private file
Args:
bucket: Bucket name
file_path: Path to file in bucket
expires_in: URL validity in seconds (default: 1 hour)
Returns:
Signed URL string
"""
try:
client = SupabaseStorageService.get_client()
response = client.storage.from_(bucket).create_signed_url(file_path, expires_in)
if isinstance(response, dict) and 'signedURL' in response:
return response['signedURL']
elif hasattr(response, 'signed_url'):
return response.signed_url
else:
# Fallback: return authenticated URL
return f"{settings.SUPABASE_URL}/storage/v1/object/authenticated/{bucket}/{file_path}"
except Exception as e:
logger.error(f"Failed to generate signed URL: {str(e)}")
# Return authenticated URL as fallback
return f"{settings.SUPABASE_URL}/storage/v1/object/authenticated/{bucket}/{file_path}"
@staticmethod
def delete(bucket: str, file_path: str) -> bool:
"""
Delete file from Supabase Storage
Args:
bucket: Bucket name
file_path: Path to file in bucket
Returns:
True if successful, False otherwise
"""
try:
client = SupabaseStorageService.get_client()
client.storage.from_(bucket).remove([file_path])
return True
except Exception as e:
logger.error(f"Supabase Storage delete failed: {str(e)}")
return False