Add authenticated PDF editor for Paperless scans
Test and deploy / test (push) Successful in 11s
Test and deploy / deploy (push) Failing after 14s

This commit is contained in:
2026-10-10 15:41:26 +02:00
parent e17d21c33a
commit 196286783a
6 changed files with 86 additions and 8 deletions
+7 -1
View File
@@ -24,9 +24,15 @@ If an ADF scan immediately says `Document feeder out of documents` and `0 pages
To merge separate Paperless documents imported from JPG scans, enable **Try to include archive version in merge for non-PDF files** in the merge dialog. Paperless's default merge tries to open the original JPG as a PDF and skips it. Enable **Delete original documents after successful merge** if you want only the merged entry in the active document list; the source entries go to Paperless's trash. A single feeder batch sent through the scanner action already becomes one document, without a later merge.
## Rotate or crop an existing document
Paperless's **PDF Editor** works only when the selected file version is a PDF. For a scanned JPG, download its archived PDF from the document's download menu; Paperless creates that PDF during OCR. Open <https://scanner.brunner.ninja/pdf/> to use the self-hosted Stirling PDF editor, protected by the same Authentik gate as ScanservJS. Its **Rotate** and **Crop** tools let you turn pages and visually select the area to keep. Download the edited PDF, then return to the original Paperless document and choose **Versions → Upload new version**. The document ID, title, tags, correspondent, document type, permissions, and custom fields stay with that Paperless entry; the previous file remains available in version history. Once the new PDF version has imported, Paperless's own PDF Editor can rotate or rearrange it too.
Paperless has no native crop tool or custom button that sends a document to another editor. Cropping a PDF changes its visible page boundary without resampling the scan image. For sensitive content outside the crop, use a redaction tool rather than relying on crop alone. Stirling PDF uses the pinned upstream Ultra-Lite image and temporary container storage; no document PVC or cloud account is involved.
## CI/CD
`./test.sh` checks syntax, the deployment manifest, and the scanner PDF handoff. `./deploy.sh` updates the existing workloads, services, ConfigMap, and ingress using `app.yaml`. The GitLab and Gitea workflows call these same scripts. Bootstrap once with `./create-ci-kubeconfig.sh`, then save its single output line as the protected CI variable / Gitea Actions secret `KUBE_CONFIG_BASE64`. Do not commit it. The deployer has named-object read and patch permissions and cannot read Secrets. Run `./install.sh` manually for first installation or changes to `storage.yaml`, `postgresql-values.yaml`, or Authentik setup.
`./test.sh` checks syntax, the deployment manifests, and the scanner PDF handoff. `./deploy.sh` updates the existing workloads, services, ConfigMap, and ingresses using `app.yaml` and `pdf-editor.yaml`. The GitLab and Gitea workflows call these same scripts. Bootstrap once with `./create-ci-kubeconfig.sh`, then save its single output line as the protected CI variable / Gitea Actions secret `KUBE_CONFIG_BASE64`. Do not commit it. The deployer has named-object read and patch permissions and cannot read Secrets. Run `./install.sh` manually for first installation or changes to `storage.yaml`, `postgresql-values.yaml`, or Authentik setup.
The Git remote is `gitea@brunner.ninja:feedc0de/paperless-ngx-deployment.git`. No credentials or generated kubeconfig are committed. To publish changes: `git add . && git commit -m 'Deploy Paperless-ngx' && git push origin main`.
+3 -3
View File
@@ -12,15 +12,15 @@ metadata:
rules:
- apiGroups: [apps]
resources: [deployments]
resourceNames: [paperless, valkey, scanner]
resourceNames: [paperless, valkey, scanner, pdf-editor]
verbs: [get, patch, watch]
- apiGroups: [""]
resources: [services]
resourceNames: [paperless, valkey, scanner]
resourceNames: [paperless, valkey, scanner, pdf-editor]
verbs: [get, patch]
- apiGroups: [networking.k8s.io]
resources: [ingresses]
resourceNames: [paperless, scanner]
resourceNames: [paperless, scanner, pdf-editor]
verbs: [get, patch]
- apiGroups: [""]
resources: [configmaps]
+2 -1
View File
@@ -2,7 +2,7 @@
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")"
./test.sh
apply_output=$(kubectl apply -f app.yaml)
apply_output=$(kubectl apply -f app.yaml -f pdf-editor.yaml)
printf '%s\n' "$apply_output"
if [[ $apply_output == *'configmap/scanner-config configured'* ]]; then
kubectl -n paperless-ngx rollout restart deployment/scanner
@@ -10,3 +10,4 @@ fi
kubectl -n paperless-ngx rollout status deployment/valkey --timeout=5m || { kubectl -n paperless-ngx describe deployment valkey; exit 1; }
kubectl -n paperless-ngx rollout status deployment/paperless --timeout=15m || { kubectl -n paperless-ngx describe deployment paperless; exit 1; }
kubectl -n paperless-ngx rollout status deployment/scanner --timeout=10m || { kubectl -n paperless-ngx describe deployment scanner; kubectl -n paperless-ngx logs deployment/scanner --tail=100; exit 1; }
kubectl -n paperless-ngx rollout status deployment/pdf-editor --timeout=10m || { kubectl -n paperless-ngx describe deployment pdf-editor; kubectl -n paperless-ngx logs deployment/pdf-editor --tail=100; exit 1; }
+3 -2
View File
@@ -21,8 +21,8 @@ fi
./setup-authentik.sh
./test.sh
helm upgrade --install postgresql postgres --repo https://groundhog2k.github.io/helm-charts/ --version 1.6.7 -n paperless-ngx -f postgresql-values.yaml --wait --timeout 10m
kubectl apply --dry-run=server -f storage.yaml -f app.yaml
apply_output=$(kubectl apply -f storage.yaml -f app.yaml)
kubectl apply --dry-run=server -f storage.yaml -f app.yaml -f pdf-editor.yaml
apply_output=$(kubectl apply -f storage.yaml -f app.yaml -f pdf-editor.yaml)
printf '%s\n' "$apply_output"
if [[ $apply_output == *'configmap/scanner-config configured'* ]]; then
kubectl -n paperless-ngx rollout restart deployment/scanner
@@ -30,4 +30,5 @@ fi
kubectl -n paperless-ngx rollout status deployment/valkey --timeout=5m
kubectl -n paperless-ngx rollout status deployment/paperless --timeout=15m
kubectl -n paperless-ngx rollout status deployment/scanner --timeout=10m
kubectl -n paperless-ngx rollout status deployment/pdf-editor --timeout=10m
kubectl -n paperless-ngx exec deployment/paperless -- python manage.py shell -c 'from django.contrib.auth.models import Group; [Group.objects.get_or_create(name=name) for name in ("Paperless Users", "Paperless Admins")]'
+65
View File
@@ -0,0 +1,65 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: pdf-editor
namespace: paperless-ngx
spec:
replicas: 1
selector:
matchLabels: {app: pdf-editor}
template:
metadata:
labels: {app: pdf-editor}
spec:
automountServiceAccountToken: false
containers:
- name: pdf-editor
image: docker.io/stirlingtools/stirling-pdf:3.1.0-ultra-lite@sha256:b19e480530e5812f96f1cddcf467bc0e652a252de98fdd36587b33ea04e75251
ports: [{name: http, containerPort: 8080}]
env:
- {name: SYSTEM_ROOTURIPATH, value: /pdf}
- {name: SYSTEM_ENABLEANALYTICS, value: "false"}
- {name: SECURITY_ENABLELOGIN, value: "false"}
startupProbe:
httpGet: {path: /pdf/, port: http}
periodSeconds: 5
failureThreshold: 60
readinessProbe:
httpGet: {path: /pdf/, port: http}
periodSeconds: 10
livenessProbe:
httpGet: {path: /pdf/, port: http}
periodSeconds: 30
resources:
requests: {cpu: 100m, memory: 512Mi}
limits: {memory: 2Gi}
---
apiVersion: v1
kind: Service
metadata:
name: pdf-editor
namespace: paperless-ngx
spec:
selector: {app: pdf-editor}
ports: [{name: http, port: 8080, targetPort: http}]
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: pdf-editor
namespace: paperless-ngx
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.middlewares: default-authentik@kubernetescrd
spec:
ingressClassName: traefik
rules:
- host: scanner.brunner.ninja
http:
paths:
- path: /pdf
pathType: Prefix
backend:
service:
name: pdf-editor
port: {name: http}
+6 -1
View File
@@ -10,12 +10,17 @@ from pathlib import Path
import yaml
documents = []
for path in ("storage.yaml", "app.yaml", "ci-deployer.yaml"):
for path in ("storage.yaml", "app.yaml", "pdf-editor.yaml", "ci-deployer.yaml"):
with open(path, encoding="utf-8") as stream:
documents.extend(doc for doc in yaml.safe_load_all(stream) if doc)
images = [container["image"] for doc in documents if doc["kind"] == "Deployment"
for container in doc["spec"]["template"]["spec"]["containers"]]
assert images and all(":" in image and not image.endswith(":latest") for image in images)
editor_ingress = next(doc for doc in documents if doc["kind"] == "Ingress"
and doc["metadata"]["name"] == "pdf-editor")
assert editor_ingress["spec"]["rules"][0]["host"] == "scanner.brunner.ninja"
assert editor_ingress["spec"]["rules"][0]["http"]["paths"][0]["path"] == "/pdf"
assert "authentik" in editor_ingress["metadata"]["annotations"]["traefik.ingress.kubernetes.io/router.middlewares"]
config = next(doc["data"]["config.local.js"] for doc in documents
if doc["kind"] == "ConfigMap" and doc["metadata"]["name"] == "scanner-config")
with tempfile.TemporaryDirectory() as temp: