Download app/api/documents.py from nvikou/Pagewise: direct link, hf CLI and curl.
- Browser
- Download file 4.99 kB
-
https://huggingface.co/spaces/nvikou/Pagewise/resolve/main/app/api/documents.py
- Command line
-
hf download hf://spaces/nvikou/Pagewise/app/api/documents.py
-
curl -L -o documents.py https://huggingface.co/spaces/nvikou/Pagewise/resolve/main/app/api/documents.py
4.99 kB
| """Document upload, list, delete, and file serving.""" | |
| from __future__ import annotations | |
| import logging | |
| from pathlib import Path | |
| from fastapi import ( | |
| APIRouter, | |
| Depends, | |
| File, | |
| Form, | |
| HTTPException, | |
| UploadFile, | |
| ) | |
| from fastapi.responses import FileResponse | |
| from sqlalchemy import func, select | |
| from sqlalchemy.ext.asyncio import AsyncSession | |
| from app.config import get_settings | |
| from app.db import get_session | |
| from app.models import Document | |
| from app.schemas import DocumentOut, FolderOut, IngestResponse | |
| from app.services.rag import ( | |
| SUPPORTED_SUFFIXES, | |
| document_file_path, | |
| ingest_file, | |
| ) | |
| from app.services.store import get_document_store | |
| logger = logging.getLogger(__name__) | |
| router = APIRouter(prefix="/api/documents", tags=["documents"]) | |
| async def list_documents( | |
| folder: str | None = None, | |
| session: AsyncSession = Depends(get_session), | |
| ) -> list[Document]: | |
| """List indexed documents.""" | |
| stmt = select(Document).order_by(Document.created_at.desc()) | |
| if folder: | |
| stmt = stmt.where(Document.folder == folder) | |
| result = await session.execute(stmt) | |
| return list(result.scalars().all()) | |
| async def list_folders( | |
| session: AsyncSession = Depends(get_session), | |
| ) -> list[FolderOut]: | |
| """List folders with document counts.""" | |
| result = await session.execute( | |
| select(Document.folder, func.count(Document.id)) | |
| .group_by(Document.folder) | |
| .order_by(Document.folder) | |
| ) | |
| return [ | |
| FolderOut(name=row[0], document_count=row[1]) | |
| for row in result.all() | |
| ] | |
| async def get_document( | |
| document_id: str, | |
| session: AsyncSession = Depends(get_session), | |
| ) -> Document: | |
| """Fetch one document metadata row.""" | |
| doc = await session.get(Document, document_id) | |
| if doc is None: | |
| raise HTTPException(404, "Document not found") | |
| return doc | |
| async def download_document( | |
| document_id: str, | |
| session: AsyncSession = Depends(get_session), | |
| ) -> FileResponse: | |
| """Serve the original file for the PDF viewer.""" | |
| path = await document_file_path(session, document_id) | |
| if path is None or not path.exists(): | |
| raise HTTPException(404, "File not found") | |
| doc = await session.get(Document, document_id) | |
| media = doc.media_type if doc else "application/octet-stream" | |
| return FileResponse( | |
| path, | |
| media_type=media, | |
| filename=doc.original_name if doc else path.name, | |
| ) | |
| async def upload_document( | |
| file: UploadFile = File(...), | |
| folder: str = Form("general"), | |
| session: AsyncSession = Depends(get_session), | |
| ) -> IngestResponse: | |
| """Upload and index a document.""" | |
| settings = get_settings() | |
| name = file.filename or "upload.bin" | |
| suffix = Path(name).suffix.lower() | |
| if suffix not in SUPPORTED_SUFFIXES: | |
| raise HTTPException( | |
| 400, | |
| f"Unsupported type {suffix}. " | |
| f"Allowed: {', '.join(sorted(SUPPORTED_SUFFIXES))}", | |
| ) | |
| raw = await file.read() | |
| max_bytes = settings.max_upload_mb * 1024 * 1024 | |
| if len(raw) > max_bytes: | |
| raise HTTPException( | |
| 400, | |
| f"File exceeds {settings.max_upload_mb} MB limit", | |
| ) | |
| tmp_dir = Path(settings.upload_path) / "_tmp" | |
| tmp_dir.mkdir(parents=True, exist_ok=True) | |
| tmp_path = tmp_dir / name | |
| tmp_path.write_bytes(raw) | |
| try: | |
| doc, chunks = await ingest_file( | |
| session, | |
| source_path=tmp_path, | |
| original_name=name, | |
| folder=(folder or "general").strip() or "general", | |
| media_type=file.content_type | |
| or "application/octet-stream", | |
| ) | |
| finally: | |
| if tmp_path.exists(): | |
| tmp_path.unlink() | |
| return IngestResponse( | |
| document=DocumentOut.model_validate(doc), | |
| chunks_added=chunks, | |
| ) | |
| async def delete_document( | |
| document_id: str, | |
| session: AsyncSession = Depends(get_session), | |
| ) -> dict[str, str]: | |
| """Delete document metadata, chunks, and file.""" | |
| doc = await session.get(Document, document_id) | |
| if doc is None: | |
| raise HTTPException(404, "Document not found") | |
| store = get_document_store() | |
| store.delete_by_document(document_id) | |
| settings = get_settings() | |
| folder = Path(settings.upload_path) / document_id | |
| if folder.exists(): | |
| for child in folder.iterdir(): | |
| child.unlink(missing_ok=True) | |
| folder.rmdir() | |
| await session.delete(doc) | |
| await session.commit() | |
| return {"status": "deleted", "id": document_id} | |