Files
pdf/gateway/app/routers/documents/export.py
T
2026-08-22 16:08:33 +05:30

138 lines
5.1 KiB
Python

import httpx
import logging
import uuid
from fastapi import APIRouter, HTTPException, Response, status
from pydantic import BaseModel
from app.services import engine
from app.services.store import document_store
router = APIRouter(tags=["documents"])
logger = logging.getLogger(__name__)
class ExportRemoteRequest(BaseModel):
fresh_token: str | None = None
@router.get("/{document_id}/export")
def export_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.",
)
d = document_store.get_document(document_id)
if not d:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
perms = d.get("permissions") or {}
if perms.get("canCopy", True) is False:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Exporting is not permitted by this document's restrictions (canCopy).",
)
try:
doc = d["doc_instance"]
bytes_data = doc.save_full_for_export()
filename = d["filename"]
if not filename.endswith(".pdf"):
filename += ".pdf"
return Response(
content=bytes_data,
media_type="application/pdf",
headers={
"Content-Disposition": f'attachment; filename="{filename}"',
"Content-Length": str(len(bytes_data)),
},
)
except Exception as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
@router.post("/{document_id}/export-remote")
async def export_remote_document(document_id: str, body: ExportRemoteRequest | None = None):
if not engine.is_available():
raise HTTPException(status_code=501, detail="Engine bridge not available.")
d = document_store.get_document(document_id)
if not d:
raise HTTPException(status_code=404, detail="Document not found")
perms = d.get("permissions") or {}
if perms.get("canCopy", True) is False:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Exporting is not permitted by this document's restrictions (canCopy).",
)
ctx = d.get("remote_context")
if not ctx or not ctx.get("upload_url"):
print(f"[export-remote] FAILING with 400: ctx={ctx}", flush=True)
raise HTTPException(
status_code=400,
detail="Document does not have a registered remote upload context.",
)
try:
# ALWAYS use the stored bytes_data, which accurately reflects the current state (including encryption).
# doc.save_full_for_export() drops encryption or corrupts the output when exporting a protected PDF.
bytes_data = d.get("bytes_data")
if not bytes_data:
doc = d["doc_instance"]
bytes_data = doc.save_full_for_export()
filename = d["filename"]
if not filename.endswith(".pdf"):
filename += ".pdf"
headers = {}
# Prefer fresh_token sent at save-time over the potentially-stale stored token
token_val = (body.fresh_token if body and body.fresh_token else None) or ctx.get("auth_token")
if token_val:
headers["Authorization"] = f"Bearer {token_val}"
csrf_token = str(uuid.uuid4())
headers["Cookie"] = f"csrf_token={csrf_token}"
headers["X-CSRF-Token"] = csrf_token
# Resolve folder_id from remote context or default to 0
data = {"folder_id": str(ctx.get("folder_id", "0"))}
if ctx.get("resource_id"):
data["file_id"] = str(ctx["resource_id"])
upload_url = ctx["upload_url"]
print(f"[export-remote] POST {upload_url} | file_id={data.get('file_id')} | filename={filename} | size={len(bytes_data)}", flush=True)
files = {"upload": (filename, bytes_data, "application/pdf")}
async with httpx.AsyncClient(timeout=120.0, follow_redirects=True) as client:
res = await client.post(upload_url, headers=headers, data=data, files=files)
print(f"[export-remote] Response status={res.status_code} body={res.text[:500]}", flush=True)
if res.status_code not in (200, 201):
logger.error("[export-remote] upstream %s: %s", res.status_code, res.text[:500])
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail=f"Remote upload failed (upstream status {res.status_code}).",
)
try:
remote_response = res.json() if res.content else {}
except ValueError:
remote_response = {}
return {
"success": True,
"message": "Successfully exported updated document to remote host!",
"remoteResponse": remote_response,
}
except Exception as e:
if isinstance(e, HTTPException):
raise e
logger.error(f"[export-remote] Unexpected error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Remote export failed.")