mirror of
https://github.com/velocitatem/cvfs.git
synced 2026-05-31 16:53:38 +00:00
Add paperless-ngx integration for document storage and share links
- dlib/integrations/paperless.py: sync HTTP client wrapping the paperless-ngx
REST API (upload doc, poll task, create/delete share links, delete document)
- config: PAPERLESS_ENABLED, PAPERLESS_BASE_URL, PAPERLESS_TOKEN, PAPERLESS_TAG_IDS
- PublicAsset model: paperless_document_id + paperless_share_slug columns
- publication service: after creating the asset, if paperless is enabled upload
the patched PDF and create a share link; stores doc id + share slug on the asset
- public routes: pass expires_at through to publish_version; new
POST /{slug}/share-links endpoint to (re)create expiring share links on demand
- schemas: PublishRequest.expires_at, PublicAssetResponse.paperless_share_url,
new ShareLinkRequest model
- frontend: paperless_share_url field on PublicAsset type, createShareLink()
and expiresAt param on publishVersion() in api.ts
- .env.example: documented paperless env vars
https://claude.ai/code/session_01YPVs6uBwCvcwVMvrfLBBdu
This commit is contained in:
88
dlib/integrations/paperless.py
Normal file
88
dlib/integrations/paperless.py
Normal file
@@ -0,0 +1,88 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import httpx
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.core.config import Settings
|
||||
|
||||
|
||||
class PaperlessClient:
|
||||
def __init__(self, base_url: str, token: str) -> None:
|
||||
self._base = base_url.rstrip("/")
|
||||
self._headers = {"Authorization": f"Token {token}"}
|
||||
|
||||
def _get(self, path: str, **params) -> dict:
|
||||
r = httpx.get(f"{self._base}{path}", headers=self._headers, params=params, timeout=30)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def _post(self, path: str, **kwargs) -> dict:
|
||||
r = httpx.post(f"{self._base}{path}", headers=self._headers, timeout=30, **kwargs)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def _delete(self, path: str) -> None:
|
||||
r = httpx.delete(f"{self._base}{path}", headers=self._headers, timeout=30)
|
||||
r.raise_for_status()
|
||||
|
||||
def upload_document(self, pdf_bytes: bytes, title: str, tags: list[int] | None = None) -> int:
|
||||
"""Upload PDF to paperless and return the created document_id (polls until task completes)."""
|
||||
files = {"document": (f"{title}.pdf", pdf_bytes, "application/pdf")}
|
||||
data: dict = {"title": title}
|
||||
if tags:
|
||||
data["tags"] = tags
|
||||
resp = self._post("/api/documents/post_document/", files=files, data=data)
|
||||
task_id = resp if isinstance(resp, str) else resp.get("task_id", resp)
|
||||
return self._poll_task(str(task_id))
|
||||
|
||||
def _poll_task(self, task_id: str, max_wait: int = 60) -> int:
|
||||
delay = 2
|
||||
elapsed = 0
|
||||
while elapsed < max_wait:
|
||||
time.sleep(delay)
|
||||
elapsed += delay
|
||||
result = self._get("/api/tasks/", task_id=task_id)
|
||||
tasks = result if isinstance(result, list) else result.get("results", [])
|
||||
if not tasks:
|
||||
delay = min(delay * 2, 10)
|
||||
continue
|
||||
task = tasks[0]
|
||||
if task.get("status") == "SUCCESS":
|
||||
return int(task["related_document"])
|
||||
if task.get("status") in ("FAILURE", "REVOKED"):
|
||||
raise RuntimeError(f"Paperless task {task_id} failed: {task.get('result')}")
|
||||
delay = min(delay * 2, 10)
|
||||
raise TimeoutError(f"Paperless task {task_id} did not complete within {max_wait}s")
|
||||
|
||||
def create_share_link(
|
||||
self, document_id: int, expiration: datetime | None = None
|
||||
) -> tuple[int, str]:
|
||||
"""Create a share link for document_id. Returns (share_link_id, full share URL)."""
|
||||
payload: dict = {"document": document_id}
|
||||
if expiration:
|
||||
payload["expiration_date"] = expiration.isoformat()
|
||||
resp = self._post("/api/share_links/", json=payload)
|
||||
slug = resp["slug"]
|
||||
link_id = int(resp["id"])
|
||||
return link_id, f"{self._base}/share/{slug}"
|
||||
|
||||
def get_share_links(self, document_id: int) -> list[dict]:
|
||||
return self._get(f"/api/documents/{document_id}/share_links/").get("results", [])
|
||||
|
||||
def delete_share_link(self, share_link_id: int) -> None:
|
||||
self._delete(f"/api/share_links/{share_link_id}/")
|
||||
|
||||
def delete_document(self, document_id: int) -> None:
|
||||
self._delete(f"/api/documents/{document_id}/")
|
||||
|
||||
|
||||
def get_paperless_client(settings: "Settings") -> PaperlessClient | None:
|
||||
if not settings.paperless_enabled:
|
||||
return None
|
||||
if not settings.paperless_base_url or not settings.paperless_token:
|
||||
return None
|
||||
return PaperlessClient(settings.paperless_base_url, settings.paperless_token)
|
||||
Reference in New Issue
Block a user