"""Document endpoints: upload, list, detail, page images, re-process, delete.""" from __future__ import annotations import uuid from datetime import datetime, timezone from fastapi import APIRouter, BackgroundTasks, File, HTTPException, UploadFile from fastapi.responses import FileResponse from app.core.config import settings from app.core.logging import get_logger from app.schemas.documents import DocStatus, DocumentDetail, DocumentMeta from app.services import storage, vectorstore from app.services.pipeline import process_document log = get_logger(__name__) router = APIRouter(prefix="/api/documents", tags=["documents"]) ALLOWED = { ".pdf", ".docx", ".doc", ".pptx", ".xlsx", ".html", ".md", ".txt", ".png", ".jpg", ".jpeg", ".tiff", ".bmp", } @router.post("", response_model=DocumentMeta) async def upload(background: BackgroundTasks, file: UploadFile = File(...)): ext = "." + (file.filename or "").rsplit(".", 1)[-1].lower() if "." in (file.filename or "") else "" if ext not in ALLOWED: raise HTTPException(400, f"Unsupported file type '{ext}'. Allowed: {sorted(ALLOWED)}") doc_id = uuid.uuid4().hex[:12] dest = settings.uploads_path / f"{doc_id}{ext}" data = await file.read() dest.write_bytes(data) detail = DocumentDetail( id=doc_id, filename=file.filename or dest.name, content_type=file.content_type or "application/octet-stream", size_bytes=len(data), status=DocStatus.uploaded, created_at=datetime.now(timezone.utc).isoformat(), ) storage.save(detail) # kick off async processing background.add_task(process_document, doc_id, dest) log.info("Uploaded %s (%d bytes) -> %s", file.filename, len(data), doc_id) return DocumentMeta.model_validate(detail.model_dump()) @router.get("", response_model=list[DocumentMeta]) async def list_documents(): return storage.list_all() @router.get("/{doc_id}", response_model=DocumentDetail) async def get_document(doc_id: str): detail = storage.get(doc_id) if not detail: raise HTTPException(404, "Document not found") return detail @router.get("/{doc_id}/pages/{page}.png") async def get_page_image(doc_id: str, page: int): path = settings.renders_path / doc_id / f"page-{page}.png" if not path.exists(): raise HTTPException(404, "Page image not found") return FileResponse(path, media_type="image/png") @router.delete("/{doc_id}") async def delete_document(doc_id: str): if not storage.get(doc_id): raise HTTPException(404, "Document not found") vectorstore.delete(doc_id) storage.delete(doc_id) return {"ok": True}