Files
pdf/gateway/app/routers/documents/crud.py
T

188 lines
7.1 KiB
Python

import re
import httpx
from urllib.parse import urlparse
from fastapi import APIRouter, File, HTTPException, UploadFile, status
from pydantic import BaseModel
from app.schemas.document import (
DocumentInfoResponse,
PageInfoResponse,
PermissionsResponse,
)
from app.services import engine
from app.services.store import document_store
router = APIRouter(prefix="/documents", tags=["documents"])
class RemoteImportRequest(BaseModel):
stream_url: str
upload_url: str
auth_token: str | None = None
resource_id: str | None = None
@router.post("/import-remote", response_model=DocumentInfoResponse, status_code=status.HTTP_201_CREATED)
async def import_remote_document(req: RemoteImportRequest) -> DocumentInfoResponse:
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
stream_parsed = urlparse(req.stream_url)
upload_parsed = urlparse(req.upload_url)
if stream_parsed.scheme not in ("http", "https") or upload_parsed.scheme not in ("http", "https"):
raise HTTPException(status_code=400, detail="Invalid URL scheme. Only http/https are allowed.")
headers = {}
if req.auth_token:
headers["Authorization"] = f"Bearer {req.auth_token}"
MAX_SIZE = 50 * 1024 * 1024 # 50 MB limit
async with httpx.AsyncClient(timeout=60.0, follow_redirects=True) as client:
try:
async with client.stream("GET", req.stream_url, headers=headers) as res:
if res.status_code != 200:
await res.aread()
raise HTTPException(
status_code=res.status_code,
detail=f"Failed to stream remote document: {res.text[:200]}",
)
content_length = res.headers.get("Content-Length")
if content_length and int(content_length) > MAX_SIZE:
raise HTTPException(status_code=400, detail="Document exceeds maximum allowed size (50MB).")
bytes_data_arr = bytearray()
async for chunk in res.aiter_bytes():
bytes_data_arr.extend(chunk)
if len(bytes_data_arr) > MAX_SIZE:
raise HTTPException(status_code=400, detail="Document exceeds maximum allowed size (50MB).")
bytes_data = bytes(bytes_data_arr)
except Exception as err:
if isinstance(err, HTTPException):
raise err
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Failed to connect to remote stream_url: {err!s}",
)
cd = res.headers.get("Content-Disposition", "")
match = re.search(r'filename="?([^";]+)"?', cd)
filename = match.group(1) if match else f"remote_document_{req.resource_id or 'file'}.pdf"
try:
pdfengine = engine.require()
doc = pdfengine.PdfDocument.load_from_memory(bytes_data, "")
info = document_store.add_document(filename, bytes_data, doc)
info["remote_context"] = {
"stream_url": req.stream_url,
"upload_url": req.upload_url,
"auth_token": req.auth_token,
"resource_id": req.resource_id,
}
return make_document_response(info)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Failed to load remote PDF into engine: {e!s}",
)
def make_document_response(d: dict) -> DocumentInfoResponse:
pages_list = []
if "doc_instance" in d:
doc = d["doc_instance"]
for i in range(doc.page_count):
try:
page = doc.get_page(i)
pages_list.append(PageInfoResponse(index=i, width=page.width, height=page.height))
except Exception:
pass
perms = d.get("permissions")
return DocumentInfoResponse(
id=d["id"],
filename=d["filename"],
sizeBytes=d["sizeBytes"],
totalPages=d["totalPages"],
pageWidth=d.get("pageWidth", 612.0),
pageHeight=d.get("pageHeight", 792.0),
uploadedAt=d["uploadedAt"],
status=d["status"],
pages=pages_list,
permissions=PermissionsResponse(**perms) if perms else PermissionsResponse(),
)
@router.post("", response_model=DocumentInfoResponse, status_code=status.HTTP_201_CREATED)
async def upload_document(file: UploadFile = File(...), password: str = "") -> DocumentInfoResponse:
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
bytes_data = await file.read()
try:
pdfengine = engine.require()
doc = pdfengine.PdfDocument.load_from_memory(bytes_data, password)
info = document_store.add_document(file.filename, bytes_data, doc)
return make_document_response(info)
except ValueError as e:
detail = str(e)
if "Password required" in detail:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Password required"
)
elif "Invalid password" in detail:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid password")
else:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=detail)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=f"Failed to load PDF: {e!s}"
)
@router.get("", response_model=list[DocumentInfoResponse])
def list_documents() -> list[DocumentInfoResponse]:
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
docs = document_store.list_documents()
return [make_document_response(d) for d in docs]
@router.get("/{document_id}", response_model=DocumentInfoResponse)
def get_document(document_id: str) -> DocumentInfoResponse:
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
d = document_store.get_document(document_id)
if not d:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
return make_document_response(d)
@router.delete("/{document_id}")
def delete_document(document_id: str):
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
deleted = document_store.delete_document(document_id)
if not deleted:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
return {"success": True}