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:
Claude
2026-04-09 09:27:26 +00:00
parent 61430317f4
commit f5621f120f
11 changed files with 214 additions and 18 deletions

View File

@@ -47,6 +47,11 @@ class Settings(BaseSettings):
)
publish_domain: str = Field(default="cv.alves.world", alias="CV_PUBLIC_DOMAIN")
paperless_enabled: bool = Field(default=False, alias="PAPERLESS_ENABLED")
paperless_base_url: str | None = Field(default=None, alias="PAPERLESS_BASE_URL")
paperless_token: str | None = Field(default=None, alias="PAPERLESS_TOKEN")
paperless_tag_ids: list[int] = Field(default_factory=list, alias="PAPERLESS_TAG_IDS")
class Config:
env_file = ".env"
extra = "ignore"
@@ -67,13 +72,20 @@ class Settings(BaseSettings):
return [origin.strip() for origin in value.split(",") if origin.strip()]
return value
@field_validator("storage_endpoint_url", mode="before")
@field_validator("storage_endpoint_url", "paperless_base_url", "paperless_token", mode="before")
@classmethod
def _empty_endpoint_to_none(cls, value):
def _empty_str_to_none(cls, value):
if isinstance(value, str) and not value.strip():
return None
return value
@field_validator("paperless_tag_ids", mode="before")
@classmethod
def _parse_tag_ids(cls, value):
if isinstance(value, str):
return [int(v.strip()) for v in value.split(",") if v.strip()]
return value
@lru_cache(maxsize=1)
def get_settings() -> Settings: