Files
ctms/backend/app/api/v1/attachments.py
T
2025-12-16 17:02:11 +08:00

114 lines
3.7 KiB
Python

import os
import uuid
from pathlib import Path
import aiofiles
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
from fastapi.responses import FileResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.deps import get_current_user, get_db_session, require_study_member
from app.crud import attachment as attachment_crud
from app.crud import audit as audit_crud
from app.crud import study as study_crud
from app.schemas.attachment import AttachmentRead
router = APIRouter()
UPLOAD_ROOT = Path(__file__).resolve().parent.parent.parent / "uploads"
async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID):
study = await study_crud.get(db, study_id)
if not study:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found")
return study
@router.post(
"/",
response_model=AttachmentRead,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(require_study_member())],
)
async def upload_attachment(
study_id: uuid.UUID,
entity_type: str,
entity_id: uuid.UUID,
file: UploadFile = File(...),
db: AsyncSession = Depends(get_db_session),
current_user=Depends(get_current_user),
) -> AttachmentRead:
await _ensure_study_exists(db, study_id)
dest_dir = UPLOAD_ROOT / f"study_{study_id}" / f"{entity_type}_{entity_id}"
dest_dir.mkdir(parents=True, exist_ok=True)
unique_name = f"{uuid.uuid4()}{Path(file.filename).suffix}"
dest_path = dest_dir / unique_name
content = await file.read()
async with aiofiles.open(dest_path, "wb") as out_file:
await out_file.write(content)
attachment = await attachment_crud.create_attachment(
db,
study_id=study_id,
entity_type=entity_type,
entity_id=entity_id,
filename=file.filename,
file_path=str(dest_path),
file_size=len(content),
content_type=file.content_type,
uploaded_by=current_user.id,
)
await audit_crud.log_action(
db,
study_id=study_id,
entity_type=entity_type,
entity_id=entity_id,
action="UPLOAD_FILE",
detail=f"File uploaded: {file.filename}",
operator_id=current_user.id,
operator_role=current_user.role,
)
return attachment
@router.get(
"/",
response_model=list[AttachmentRead],
dependencies=[Depends(require_study_member())],
)
async def list_attachments(
study_id: uuid.UUID,
entity_type: str,
entity_id: uuid.UUID,
db: AsyncSession = Depends(get_db_session),
) -> list[AttachmentRead]:
await _ensure_study_exists(db, study_id)
attachments = await attachment_crud.list_attachments(db, study_id, entity_type, entity_id)
return list(attachments)
@router.get(
"/{attachment_id}/download",
dependencies=[Depends(require_study_member())],
)
async def download_attachment(
study_id: uuid.UUID,
entity_type: str,
entity_id: uuid.UUID,
attachment_id: uuid.UUID,
db: AsyncSession = Depends(get_db_session),
) -> FileResponse:
await _ensure_study_exists(db, study_id)
attachment = await attachment_crud.get_attachment(db, attachment_id)
if not attachment or attachment.study_id != study_id or attachment.entity_id != entity_id or attachment.entity_type != entity_type:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Attachment not found")
if not os.path.exists(attachment.file_path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File missing on server")
return FileResponse(
path=attachment.file_path,
filename=attachment.filename,
media_type=attachment.content_type or "application/octet-stream",
)