AgentEvalTool/backend/agenteval/web/routers/files.py
sinohqb d7514f4e65 refactor(files): harden storage and split frontend
Add transactional file storage workflows, typed API contracts, recursive category handling, frontend component separation, and Files API coverage.
2026-07-17 17:41:19 +08:00

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)