"""Attachment routes — upload (multipart), list, download, delete.""" from __future__ import annotations import os import uuid from fastapi import APIRouter, Depends, File, Form, HTTPException, Request, UploadFile, status from fastapi.responses import FileResponse from sqlalchemy.ext.asyncio import AsyncSession from app.core.db import get_db from app.core.rate_limit import RateLimitPolicy, check_rate_limit_policy from app.deps import require_permission 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( request: Request, file: UploadFile = File(...), entity_type: str = Form(...), entity_id: str = Form(...), db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("attachments:write")), ): """Upload a file attachment. Multipart form data.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) is_admin = current_user.get("is_system_admin", False) # Rate limit — UPLOAD policy await check_rate_limit_policy( f"rate:upload:attachments:{tenant_id}:{user_id}", RateLimitPolicy.UPLOAD, ) try: eid = uuid.UUID(entity_id) except ValueError: raise HTTPException(400, detail={"detail": "Invalid entity_id", "code": "invalid_id"}) from None mime_type = file.content_type or "application/octet-stream" try: return await attachment_service.save_attachment( db, tenant_id, user_id, entity_type, eid, file.filename or "unknown", file, mime_type, is_system_admin=is_admin, ) except PermissionError as e: raise HTTPException(status_code=403, detail=str(e)) from e @router.get("") async def list_attachments( entity_type: str, entity_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("attachments:read")), ): """List attachments for a specific entity.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) is_admin = current_user.get("is_system_admin", False) try: eid = uuid.UUID(entity_id) except ValueError: raise HTTPException(400, detail={"detail": "Invalid entity_id", "code": "invalid_id"}) from None try: return await attachment_service.list_attachments(db, tenant_id, entity_type, eid, user_id=user_id, is_system_admin=is_admin) except PermissionError as e: raise HTTPException(status_code=403, detail=str(e)) from e @router.get("/{attachment_id}") async def download_attachment( attachment_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("attachments:read")), ): """Download an attachment file.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) is_admin = current_user.get("is_system_admin", False) try: aid = uuid.UUID(attachment_id) except ValueError: raise HTTPException(400, detail={"detail": "Invalid attachment_id", "code": "invalid_id"}) from None try: data = await attachment_service.get_attachment(db, tenant_id, aid, user_id=user_id, is_system_admin=is_admin) except PermissionError as e: raise HTTPException(status_code=403, detail=str(e)) from e if data is None: raise HTTPException(404, detail={"detail": "Attachment not found", "code": "not_found"}) # Get storage path from DMS file via new unified service storage_path = await attachment_service.get_attachment_download_path( db, tenant_id, aid, user_id=user_id, is_system_admin=is_admin ) if storage_path is None: raise HTTPException(404, detail={"detail": "File not found in DMS", "code": "file_missing"}) from app.core.storage import get_storage_backend storage = get_storage_backend() try: file_bytes = await storage.read(storage_path) except Exception: raise HTTPException(404, detail={"detail": "File not found in storage", "code": "file_missing"}) from fastapi.responses import Response return Response( content=file_bytes, media_type=data["mime_type"], headers={"Content-Disposition": f'attachment; filename="{data["filename"]}"'}, ) @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(require_permission("attachments:write")), ): """Delete an attachment.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) is_admin = current_user.get("is_system_admin", False) try: aid = uuid.UUID(attachment_id) except ValueError: raise HTTPException(400, detail={"detail": "Invalid attachment_id", "code": "invalid_id"}) from None try: deleted = await attachment_service.delete_attachment(db, tenant_id, user_id, aid, is_system_admin=is_admin) except PermissionError as e: raise HTTPException(status_code=403, detail=str(e)) from e if not deleted: raise HTTPException(404, detail={"detail": "Attachment not found", "code": "not_found"})