"""`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 collections.abc import Sequence from typing import Annotated from anyio import CapacityLimiter, Semaphore 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.embedding import DenseEmbedder, SparseEmbedder from src.application.ports.object_storage import ObjectStorage from src.application.ports.point_storage import PointStorage from src.bootstrap.dependencies import ( get_dense_embedders, get_ingestion_concurrency_limiter, get_ingestion_limiter, get_object_storage, get_point_storage, get_sessionmaker, get_settings, get_sparse_embedder, ) 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)] _PointStorageDep = Annotated[PointStorage, Depends(get_point_storage)] _SettingsDep = Annotated[Settings, Depends(get_settings)] _IngestionLimiterDep = Annotated[CapacityLimiter, Depends(get_ingestion_limiter)] _ConcurrencyLimiterDep = Annotated[Semaphore, Depends(get_ingestion_concurrency_limiter)] _DenseEmbeddersDep = Annotated[Sequence[DenseEmbedder], Depends(get_dense_embedders)] _SparseEmbedderDep = Annotated[SparseEmbedder, Depends(get_sparse_embedder)] @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, point_storage: _PointStorageDep, settings: _SettingsDep, limiter: _IngestionLimiterDep, concurrency_limiter: _ConcurrencyLimiterDep, dense_embedders: _DenseEmbeddersDep, sparse_embedder: _SparseEmbedderDep, ) -> FileUploadResponse: data = await file.read() result = await upload_source_file( sessionmaker=sessionmaker, storage=storage, point_storage=point_storage, auth=auth, domain=domain, filename=file.filename or "", data=data, ingestion_settings=settings.ingestion, chunking_settings=settings.chunking, qdrant_settings=settings.qdrant, thread_limiter=limiter, concurrency_limiter=concurrency_limiter, dense_embedders=dense_embedders, sparse_embedder=sparse_embedder, ) 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)