"""Attachment routes — upload (multipart), list, download, delete.""" from __future__ import annotations import os import uuid from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile, status from fastapi.responses import FileResponse from sqlalchemy.ext.asyncio import AsyncSession from app.core.auth import check_permission from app.core.db import get_db from app.deps import get_current_user from app.services import attachment_service router = APIRouter(prefix="/api/v1/attachments", tags=["attachments"]) @router.post("", status_code=status.HTTP_201_CREATED) async def upload_attachment( file: UploadFile = File(...), entity_type: str = Form(...), entity_id: str = Form(...), db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Upload a file attachment. Multipart form data.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) role = current_user.get("role", "viewer") if not check_permission(role, "companies", "update"): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail={"detail": "Insufficient permissions", "code": "forbidden"}, ) try: eid = uuid.UUID(entity_id) except ValueError: raise HTTPException(400, detail={"detail": "Invalid entity_id", "code": "invalid_id"}) from None file_content = await file.read() mime_type = file.content_type or "application/octet-stream" return await attachment_service.save_attachment( db, tenant_id, user_id, entity_type, eid, file.filename or "unknown", file_content, mime_type, ) @router.get("") async def list_attachments( entity_type: str, entity_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """List attachments for a specific entity.""" tenant_id = uuid.UUID(current_user["tenant_id"]) try: eid = uuid.UUID(entity_id) except ValueError: raise HTTPException(400, detail={"detail": "Invalid entity_id", "code": "invalid_id"}) from None return await attachment_service.list_attachments(db, tenant_id, entity_type, eid) @router.get("/{attachment_id}") async def download_attachment( attachment_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Download an attachment file.""" tenant_id = uuid.UUID(current_user["tenant_id"]) try: aid = uuid.UUID(attachment_id) except ValueError: raise HTTPException(400, detail={"detail": "Invalid attachment_id", "code": "invalid_id"}) from None data = await attachment_service.get_attachment(db, tenant_id, aid) if data is None: raise HTTPException(404, detail={"detail": "Attachment not found", "code": "not_found"}) file_path = data["file_path"] if not os.path.isfile(file_path): raise HTTPException(404, detail={"detail": "File not found on disk", "code": "file_missing"}) return FileResponse( path=file_path, filename=data["filename"], media_type=data["mime_type"], ) @router.delete("/{attachment_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_attachment( attachment_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Delete an attachment.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) role = current_user.get("role", "viewer") if not check_permission(role, "companies", "delete"): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail={"detail": "Insufficient permissions", "code": "forbidden"}, ) try: aid = uuid.UUID(attachment_id) except ValueError: raise HTTPException(400, detail={"detail": "Invalid attachment_id", "code": "invalid_id"}) from None deleted = await attachment_service.delete_attachment(db, tenant_id, user_id, aid) if not deleted: raise HTTPException(404, detail={"detail": "Attachment not found", "code": "not_found"})