"""Blob storage router: upload and retrieve binary assets in the database.""" import uuid from fastapi import APIRouter, Depends, HTTPException, Response, UploadFile, status from app.auth import get_current_admin_user from app.database import get_session from app.models import StorageBlob from app.user_models import User from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession router = APIRouter() ALLOWED_MIME_TYPES = {"image/png", "image/jpeg", "image/webp", "image/avif"} MAX_FILE_SIZE = 5 * 1024 * 1024 # 5 MB # Magic byte signatures → MIME type MAGIC_BYTES = { b'\x89PNG\r\n\x1a\n': 'image/png', b'\xff\xd8\xff': 'image/jpeg', b'\x00\x00\x00\x1cftypavif': 'image/avif', b'\x00\x00\x00\x20ftypwebp': 'image/webp', } @router.post("/upload") async def upload_blob( file: UploadFile, session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_admin_user), ): """Upload an image blob. Admin only. Validates magic bytes, stores binary in PostgreSQL, returns a URL path. """ # Read and validate size content = await file.read() if len(content) > MAX_FILE_SIZE: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="File size exceeds 5 MB limit", ) # Validate magic bytes detected_type = None for magic, mime_type in MAGIC_BYTES.items(): if content.startswith(magic): detected_type = mime_type break if detected_type is None: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="File type not supported. Only PNG, JPEG, WebP, and AVIF are allowed.", ) # Generate unique blob_id and insert blob_id = uuid.uuid4().hex blob = StorageBlob( blob_id=blob_id, content_type=detected_type, size=len(content), data=content, ) session.add(blob) await session.commit() return {"url": f"/api/storage/{blob_id}"} @router.get("/{blob_id}") async def get_blob(blob_id: str, session: AsyncSession = Depends(get_session)): """Retrieve a stored blob by blob_id. Public endpoint.""" result = await session.execute( select(StorageBlob).where(StorageBlob.blob_id == blob_id) ) blob = result.scalar_one_or_none() if blob is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Blob not found", ) return Response( content=blob.data, media_type=blob.content_type, headers={"Content-Length": str(blob.size)}, )