Add transactional file storage workflows, typed API contracts, recursive category handling, frontend component separation, and Files API coverage.
149 lines
5.2 KiB
Python
149 lines
5.2 KiB
Python
"""API routes for file management."""
|
|
|
|
from typing import NoReturn
|
|
|
|
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
|
|
from fastapi.responses import FileResponse
|
|
from sqlmodel import Session
|
|
|
|
from agenteval.config import get_settings
|
|
from agenteval.services.files import (
|
|
CategoryNotFoundError,
|
|
FileManagementError,
|
|
FileManagementService,
|
|
FileRecordNotFoundError,
|
|
FileValidationError,
|
|
PhysicalFileNotFoundError,
|
|
UploadTooLargeError,
|
|
)
|
|
from agenteval.storage.db import FILES_DIR
|
|
from agenteval.storage.file_storage import FileStorage
|
|
from agenteval.web.deps import get_db
|
|
from agenteval.web.file_schemas import (
|
|
CategoryCreate,
|
|
CategoryUpdate,
|
|
FileCategoryResponse,
|
|
FileRecordResponse,
|
|
FileUploadConfigResponse,
|
|
OkResponse,
|
|
)
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _allowed_extensions() -> set[str]:
|
|
raw = get_settings().allowed_extensions
|
|
return {extension.strip().lower() for extension in raw.split(",") if extension.strip()}
|
|
|
|
|
|
def get_file_service(session: Session = Depends(get_db)) -> FileManagementService:
|
|
settings = get_settings()
|
|
return FileManagementService(
|
|
session=session,
|
|
storage=FileStorage(FILES_DIR),
|
|
allowed_extensions=_allowed_extensions(),
|
|
max_upload_size_mb=settings.max_upload_size_mb,
|
|
)
|
|
|
|
|
|
def _raise_http_error(error: FileManagementError) -> NoReturn:
|
|
if isinstance(error, UploadTooLargeError):
|
|
raise HTTPException(
|
|
status_code=413,
|
|
detail=f"文件大小超过限制 ({get_settings().max_upload_size_mb}MB)",
|
|
) from error
|
|
if isinstance(error, FileValidationError):
|
|
raise HTTPException(status_code=400, detail=str(error)) from error
|
|
if isinstance(error, CategoryNotFoundError):
|
|
raise HTTPException(status_code=404, detail="分类不存在") from error
|
|
if isinstance(error, FileRecordNotFoundError):
|
|
raise HTTPException(status_code=404, detail="文件不存在") from error
|
|
if isinstance(error, PhysicalFileNotFoundError):
|
|
raise HTTPException(status_code=404, detail="物理文件不存在") from error
|
|
raise HTTPException(status_code=500, detail="文件操作失败") from error
|
|
|
|
|
|
@router.get("/config", response_model=FileUploadConfigResponse)
|
|
def get_upload_config() -> FileUploadConfigResponse:
|
|
return FileUploadConfigResponse(
|
|
allowed_extensions=sorted(_allowed_extensions()),
|
|
max_upload_size_mb=get_settings().max_upload_size_mb,
|
|
)
|
|
|
|
|
|
@router.get("/categories", response_model=list[FileCategoryResponse])
|
|
def list_categories(service: FileManagementService = Depends(get_file_service)):
|
|
return service.list_categories()
|
|
|
|
|
|
@router.post("/categories", response_model=FileCategoryResponse)
|
|
def create_category(body: CategoryCreate, service: FileManagementService = Depends(get_file_service)):
|
|
try:
|
|
return FileCategoryResponse.from_record(service.create_category(body.name, body.parent_id))
|
|
except FileManagementError as error:
|
|
_raise_http_error(error)
|
|
|
|
|
|
@router.put("/categories/{category_id}", response_model=FileCategoryResponse)
|
|
def update_category(
|
|
category_id: str,
|
|
body: CategoryUpdate,
|
|
service: FileManagementService = Depends(get_file_service),
|
|
):
|
|
try:
|
|
return FileCategoryResponse.from_record(service.update_category(category_id, body.name))
|
|
except FileManagementError as error:
|
|
_raise_http_error(error)
|
|
|
|
|
|
@router.delete("/categories/{category_id}", response_model=OkResponse)
|
|
def delete_category(category_id: str, service: FileManagementService = Depends(get_file_service)):
|
|
try:
|
|
service.delete_category(category_id)
|
|
return OkResponse()
|
|
except FileManagementError as error:
|
|
_raise_http_error(error)
|
|
|
|
|
|
@router.get("", response_model=list[FileRecordResponse])
|
|
def list_files(category_id: str | None = None, service: FileManagementService = Depends(get_file_service)):
|
|
try:
|
|
return [FileRecordResponse.from_record(record) for record in service.list_files(category_id)]
|
|
except FileManagementError as error:
|
|
_raise_http_error(error)
|
|
|
|
|
|
@router.post("/upload", response_model=FileRecordResponse)
|
|
async def upload_file(
|
|
file: UploadFile = File(...),
|
|
category_id: str | None = Form(default=None),
|
|
service: FileManagementService = Depends(get_file_service),
|
|
):
|
|
try:
|
|
record = await service.upload(file, category_id)
|
|
return FileRecordResponse.from_record(record)
|
|
except FileManagementError as error:
|
|
_raise_http_error(error)
|
|
|
|
|
|
@router.get("/{file_id}/download")
|
|
def download_file(file_id: str, service: FileManagementService = Depends(get_file_service)):
|
|
try:
|
|
record, file_path = service.get_download(file_id)
|
|
return FileResponse(
|
|
path=str(file_path),
|
|
filename=record.original_name,
|
|
media_type=record.mime_type or "application/octet-stream",
|
|
)
|
|
except FileManagementError as error:
|
|
_raise_http_error(error)
|
|
|
|
|
|
@router.delete("/{file_id}", response_model=OkResponse)
|
|
def delete_file(file_id: str, service: FileManagementService = Depends(get_file_service)):
|
|
try:
|
|
service.delete_file(file_id)
|
|
return OkResponse()
|
|
except FileManagementError as error:
|
|
_raise_http_error(error)
|