Files

215 lines
6.1 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)