feat(api): add POST/GET /v1/files with auth, error envelope, and request-id middleware
Why: - Wires the ADR-0008 error envelope, per-request correlation id, and the /v1/files routes into the app. Changes: - Extend AppResources/lifespan with the ingestion CapacityLimiter and ObjectStorage adapter. Impact: - /v1 now exposes routes for the first time.
This commit is contained in:
74
src/api/routers/files.py
Normal file
74
src/api/routers/files.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""`POST /v1/files`, `GET /v1/files/{file_id}` (ADR-0008).
|
||||
|
||||
Routes adapt HTTP to `application/files` calls; they do not parse, hash,
|
||||
touch MinIO/Qdrant, or otherwise carry ingestion business logic (ADR-0015).
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from typing import Annotated
|
||||
|
||||
from anyio import CapacityLimiter
|
||||
from fastapi import APIRouter, Depends, Form, HTTPException, Response, UploadFile, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
|
||||
from src.api.dependencies.auth import require_scope
|
||||
from src.api.schemas.files import FileStatusResponse, FileUploadResponse
|
||||
from src.application.auth.context import AuthContext
|
||||
from src.application.files.status import get_file_status
|
||||
from src.application.files.upload import upload_source_file
|
||||
from src.application.ports.object_storage import ObjectStorage
|
||||
from src.bootstrap.dependencies import (
|
||||
get_ingestion_limiter,
|
||||
get_object_storage,
|
||||
get_sessionmaker,
|
||||
get_settings,
|
||||
)
|
||||
from src.config import Settings
|
||||
|
||||
router = APIRouter(prefix="/files", tags=["files"])
|
||||
|
||||
_RequireFilesWrite = Annotated[AuthContext, Depends(require_scope("files:write"))]
|
||||
_SessionmakerDep = Annotated[async_sessionmaker[AsyncSession], Depends(get_sessionmaker)]
|
||||
_ObjectStorageDep = Annotated[ObjectStorage, Depends(get_object_storage)]
|
||||
_SettingsDep = Annotated[Settings, Depends(get_settings)]
|
||||
_IngestionLimiterDep = Annotated[CapacityLimiter, Depends(get_ingestion_limiter)]
|
||||
|
||||
|
||||
@router.post("", status_code=status.HTTP_201_CREATED)
|
||||
async def upload_file(
|
||||
response: Response,
|
||||
file: UploadFile,
|
||||
domain: Annotated[str, Form()],
|
||||
auth: _RequireFilesWrite,
|
||||
sessionmaker: _SessionmakerDep,
|
||||
storage: _ObjectStorageDep,
|
||||
settings: _SettingsDep,
|
||||
limiter: _IngestionLimiterDep,
|
||||
) -> FileUploadResponse:
|
||||
data = await file.read()
|
||||
result = await upload_source_file(
|
||||
sessionmaker=sessionmaker,
|
||||
storage=storage,
|
||||
auth=auth,
|
||||
domain=domain,
|
||||
filename=file.filename or "",
|
||||
data=data,
|
||||
max_upload_size_bytes=settings.ingestion.max_upload_size_bytes,
|
||||
chunking_strategy=settings.chunking.strategy,
|
||||
validation_limiter=limiter,
|
||||
)
|
||||
if not result.is_new_attempt:
|
||||
response.status_code = status.HTTP_200_OK
|
||||
return FileUploadResponse.from_result(result)
|
||||
|
||||
|
||||
@router.get("/{file_id}")
|
||||
async def get_file(
|
||||
file_id: uuid.UUID,
|
||||
auth: _RequireFilesWrite,
|
||||
sessionmaker: _SessionmakerDep,
|
||||
) -> FileStatusResponse:
|
||||
result = await get_file_status(sessionmaker, tenant_id=auth.tenant_id, source_file_id=file_id)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="file not found")
|
||||
return FileStatusResponse.from_result(result)
|
||||
Reference in New Issue
Block a user