Files
erp-nutzfahrzeuge/backend/app/routers/files.py
T

210 lines
6.0 KiB
Python

"""Files router: upload, list, download, and delete files for vehicles."""
import os
import uuid
from fastapi import APIRouter, Depends, File as FastAPIFile, HTTPException, UploadFile, status
from fastapi.responses import FileResponse as FastAPIFileResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.dependencies import get_current_user, get_pagination
from app.models.user import User
from app.models.vehicle import Vehicle
from app.schemas.file import (
FileDeleteResponse,
FileListResponse,
FileResponse,
FileUploadResponse,
)
from app.services import file_service, vehicle_service
router = APIRouter(prefix="/vehicles", tags=["files"])
async def _verify_vehicle_exists(
db: AsyncSession, vehicle_id: uuid.UUID
) -> Vehicle:
"""Verify that a vehicle exists, raising 404 if not."""
vehicle = await vehicle_service.get_vehicle_by_id(db, vehicle_id)
if vehicle is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={
"error": {
"code": "VEHICLE_NOT_FOUND",
"message": "Vehicle not found",
}
},
)
return vehicle
@router.get(
"/{vehicle_id}/files",
response_model=FileListResponse,
status_code=status.HTTP_200_OK,
)
async def list_files(
vehicle_id: uuid.UUID,
db: AsyncSession = Depends(get_db),
pagination: dict = Depends(get_pagination),
current_user: User = Depends(get_current_user),
):
"""List all files for a vehicle with pagination."""
await _verify_vehicle_exists(db, vehicle_id)
files, total = await file_service.list_files(
db,
vehicle_id=vehicle_id,
page=pagination["page"],
page_size=pagination["page_size"],
)
return FileListResponse(
items=[FileResponse.model_validate(f) for f in files],
total=total,
page=pagination["page"],
page_size=pagination["page_size"],
)
@router.post(
"/{vehicle_id}/files",
response_model=FileUploadResponse,
status_code=status.HTTP_201_CREATED,
)
async def upload_file(
vehicle_id: uuid.UUID,
file: UploadFile = FastAPIFile(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Upload a file for a vehicle (multipart/form-data).
Accepts images (jpg, png, webp), documents (pdf, doc, docx).
Max file size: 20MB.
"""
await _verify_vehicle_exists(db, vehicle_id)
# Read file content
file_content = await file.read()
file_size = len(file_content)
# Check file size (20MB limit)
if not file_service.validate_file_size(file_size, max_size_mb=20):
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail={
"error": {
"code": "FILE_TOO_LARGE",
"message": f"File size {file_size} bytes exceeds 20MB limit",
}
},
)
# Validate MIME type
mime_type = file.content_type or ""
if not file_service.validate_mime_type(mime_type, file.filename or ""):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail={
"error": {
"code": "INVALID_MIME_TYPE",
"message": f"Unsupported file type: {mime_type}. Allowed: jpg, png, webp, pdf, doc, docx",
}
},
)
try:
file_record = await file_service.upload_file(
db,
vehicle_id=vehicle_id,
file_content=file_content,
original_filename=file.filename or "unnamed",
mime_type=mime_type,
)
except ValueError as exc:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail={
"error": {
"code": "UPLOAD_FAILED",
"message": str(exc),
}
},
)
return FileUploadResponse.model_validate(file_record)
@router.get(
"/{vehicle_id}/files/{file_id}",
status_code=status.HTTP_200_OK,
)
async def download_file(
vehicle_id: uuid.UUID,
file_id: uuid.UUID,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Download a file by its ID."""
await _verify_vehicle_exists(db, vehicle_id)
file_record = await file_service.get_file(db, vehicle_id, file_id)
if file_record is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={
"error": {
"code": "FILE_NOT_FOUND",
"message": "File not found",
}
},
)
if not os.path.exists(file_record.file_path):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={
"error": {
"code": "FILE_NOT_FOUND",
"message": "File not found on disk",
}
},
)
return FastAPIFileResponse(
path=file_record.file_path,
filename=file_record.original_filename,
media_type=file_record.mime_type,
)
@router.delete(
"/{vehicle_id}/files/{file_id}",
response_model=FileDeleteResponse,
status_code=status.HTTP_200_OK,
)
async def delete_file(
vehicle_id: uuid.UUID,
file_id: uuid.UUID,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Delete a file by its ID."""
await _verify_vehicle_exists(db, vehicle_id)
file_record = await file_service.delete_file(db, vehicle_id, file_id)
if file_record is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={
"error": {
"code": "FILE_NOT_FOUND",
"message": "File not found",
}
},
)
return FileDeleteResponse(message="File deleted", id=file_record.id)