fix: Ghostscript-Angebot log nicht mehr, fehlende Voraussetzungen dokumentiert (v0.7.1)

Befunde aus dem ersten echten Erstinstallations-Test auf frischen Debian-12-
und Debian-13-Containern. Der Weg selbst hat getragen (Basis-Install,
Instanz-Anlage, zweite Instanz, Update mit Rauchtest, Rollback) — diese
Stellen haben gelogen oder gefehlt:

- Das Ghostscript-Backports-Angebot auf Debian 12 war ein garantierter
  Leerlauf, der "aktualisiert ✓" meldete: bookworm-backports enthaelt gar
  kein ghostscript (am Paketindex verifiziert). Die Routine sucht jetzt den
  echten Kandidaten, vergleicht vorher/nachher und raeumt eine nur zur Probe
  angelegte Quelle wieder weg.
- Derselbe untaugliche Rat stand in der Preflight-Meldung, der
  pdfa_level-Warnung, config.example.toml und vier Doku-Dateien — ueberall
  ersetzt durch die echten Optionen.
- Mehrzeilige Log-Hinweise waren durch "echo -e" zerrissen und nicht
  kopierbar; log_* nutzt jetzt printf mit %s.
- pip-freeze.txt landete beim Rollback als /pip-freeze.txt im
  Wurzelverzeichnis, liegt jetzt unter opt/pdf-ocr-hotfolder/.
- git und sudo fehlen auf dem Proxmox-Debian-Template; "sudo ./install.sh"
  scheitert dort. Beide Wege dokumentiert, git als Voraussetzung ergaenzt,
  HTTPS-Clone als Normalfall.
- Rollback: systemctl start kann kein Glob. journald-Reparatur: Instanzen
  danach neu starten, sonst bleibt das Journal leer.

254 Tests gruen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-23 01:36:42 +02:00
parent cd803a3dfe
commit 465ff8873f
15 changed files with 611 additions and 101 deletions
+12 -5
View File
@@ -1,7 +1,7 @@
# AI Agent Briefing — PDF OCR Hotfolder
**Zuletzt aktualisiert:** 2026-09-23
**Version:** 0.7.0
**Version:** 0.7.1
**Status:** Multi-Instanz-Betrieb, Preflight-Checks (inkl. veraPDF-Binary), Fehlerzählung, Wiederaufnahme aus `working/`, Kollisionsschutz auf allen Schreibpfaden, Bewachung des Verzeichnis-Watches und ein abgesicherter Updater (venv-Rebuild, Backup, Verifikation, Versionssprung-Meldung, Rauchtest). `install.sh` und `update.sh` teilen sich `lib/common.sh`. Test-Suite grün (**254 pytest-Tests**). Ein Produktiv-Einsatz ist im Repo (README/CHANGELOG) nicht dokumentiert — die bisherigen Fixes stammen aus Issues #1–#6, aus Vorbereitungen auf Debian 13 und aus einer Durchsicht auf stille Datenverlust-Pfade (0.7.0), nicht aus einem belegten Dauerbetrieb.
> **Betriebsabläufe stehen nicht hier**, sondern in:
@@ -228,7 +228,9 @@ Vollständig: [docs/UPDATE.md](docs/UPDATE.md). Für die Arbeit am Skript wichti
und fasst nachinstallierte Sprachpakete nicht an (kein purge/autoremove).
Fehlschläge setzen nur `APT_WARN`, sie brechen nicht ab.
- **Backup** (`create_backup()`): Code, `/etc/pdf-ocr-hotfolder/`, Template-Unit,
alle Drop-ins und ein `pip-freeze.txt` der alten venv. **Ohne** venv und
alle Drop-ins und ein `pip-freeze.txt` der alten venv (im Archiv unter
`opt/pdf-ocr-hotfolder/`, damit es beim Entpacken nach `/` nicht im
Wurzelverzeichnis landet). **Ohne** venv und
**ohne** Datenverzeichnisse. `umask 077` + `chmod 600 root:root`, weil die
Configs Klartext-Passwörter enthalten. Rotation: letzte `BACKUP_KEEP` = 5.
- **LXC-Drop-in** wird beim Update aus dem Repo nachgezogen, **falls es
@@ -397,18 +399,22 @@ Der Service läuft in allen Fällen weiter (kein `exit 1` wie im alten Bash-Tool
- **ocrmypdf ≤ 16.x**: die Prüfung in `builtin_plugins/ghostscript.py::check_options()` läuft **bedingungslos**. `skip_text = true` allein reicht — `output_type` wird nicht geprüft. Auf Debian 12 scheitert damit **jede** Datei.
- **ocrmypdf ≥ 17.0**: derselbe Block steckt in einem `if options.output_type.startswith('pdfa'):`. Ohne PDF/A wird Ghostscript nicht angefasst.
`pdfa_level = ""` ist deshalb **kein** Schutz für sich genommen — es wirkt nur mit ocrmypdf ≥ 17. `requirements.txt` pinnt daher 17.x; ein Downgrade auf 16.x macht jede Debian-12-Instanz unbrauchbar, bei grünem `systemctl status`. Der Preflight bildet die reale Bedingung ab (`_gs_block_reason()`) und bricht mit Exit 2 ab, `--check-config` meldet denselben Zustand als Fehler. `redo_ocr` ist bewusst **nicht** in der Bedingung: die Config kennt keinen solchen Key. Abhilfe: Ghostscript ≥ 10.02.1 aus bookworm-backports (der Installer bietet das an) oder `skip_text = false`.
`pdfa_level = ""` ist deshalb **kein** Schutz für sich genommen — es wirkt nur mit ocrmypdf ≥ 17. `requirements.txt` pinnt daher 17.x; ein Downgrade auf 16.x macht jede Debian-12-Instanz unbrauchbar, bei grünem `systemctl status`. Der Preflight bildet die reale Bedingung ab (`_gs_block_reason()`) und bricht mit Exit 2 ab, `--check-config` meldet denselben Zustand als Fehler. `redo_ocr` ist bewusst **nicht** in der Bedingung: die Config kennt keinen solchen Key.
**Abhilfe — und die Falle dabei:** `pdfa_level = ""` lassen (Default), `skip_text = false` setzen, oder eine Distribution mit neuerem Ghostscript (**Debian 13: 10.05.1**). **Ein Ghostscript-Upgrade auf Debian 12 gibt es nicht** — `bookworm-backports` enthält kein Ghostscript-Paket (am echten Paketindex geprüft: 2606 Pakete, `ghostscript` nicht darunter). Bis einschließlich v0.7.0 haben Preflight-Meldung, Config-Warnung, `config.example.toml`, README und alle drei docs-Dateien genau dieses Upgrade empfohlen — ein Rat, der nie funktionieren konnte. Falls er irgendwo wieder auftaucht: **ersatzlos streichen**. Planungsaussage für den Admin: **wer auf Debian 12 PDF/A mit `skip_text = true` braucht, hat dort keinen Weg** — das muss *vor* der Installation entschieden werden, nicht beim Preflight-Abbruch.
- **`[ocr].timeout` ist ein Timeout PRO SEITE**, kein Gesamt-Timeout pro PDF. Der Wert geht als `tesseract_timeout` (ocrmypdf-Option `--tesseract-timeout`) durch; ocrmypdf kennt kein Dokument-Timeout. Wer noch den alten Default `1800` in einer Config stehen hat, gibt Tesseract 30 Minuten **je Seite** — Richtwert ist 300, ab 900 warnt `--check-config`. Ein durchgereichtes `0` würde ocrmypdf dazu bringen, OCR **still zu überspringen**, deshalb wird bei `0` (oder negativ) gar nichts übergeben und der ocrmypdf-Default greift.
- **`TimeoutStopSec=300` in der Unit ist Absicht.** Ein laufendes OCR soll beim Stoppen zu Ende laufen dürfen — ein `systemctl stop` kann deshalb pro Instanz bis zu 5 Minuten dauern, und `update.sh` (das nacheinander stoppt) entsprechend länger. Bei SIGKILL bliebe das Original in `working/` liegen; das wird zwar wiederaufgenommen, kostet aber den kompletten Durchlauf.
- **Die venv hängt an der Python-Version der Distribution.** Nach einem Debian-Major-Upgrade ist `venv/bin/python` tot (systemd: `203/EXEC`) oder eine andere Version als das System-Python. Der Weg dahin und zurück steht in [docs/OS-UPGRADE.md](docs/OS-UPGRADE.md); im Code prüfen beide Skripte das mit **demselben** `venv_is_healthy()` aus `lib/common.sh` (seit 0.7.0 — vorher hatte jedes eine eigene Fassung, und die in `install.sh` war die schwächere).
- **`incoming/` darf nicht auf CIFS/NFS liegen.** Der Hotfolder hängt vollständig an inotify, und inotify sieht nur Änderungen des lokalen Kernels. Schreibt ein anderer Rechner über SMB/NFS in ein gemountetes Verzeichnis, entsteht **gar kein Event** — der Dienst meldet `active (running)`, arbeitet beim Start-Scan den Bestand ab und bemerkt danach nichts mehr. Betriebsvorgabe: ext4, xfs oder zfs, `incoming/` lokal ([docs/INSTALLATION.md](docs/INSTALLATION.md#dateisystem-ext4-xfs-oder-zfs)).
- **Debian 13 in LXC auf Proxmox: journald scheitert mit `243/CREDENTIALS`.** systemd ≥ 255 (Debian 13 hat 257) setzt `ImportCredential=journal.*`; der Hilfsprozess `(sd-mkdcreds)` mountet dafür, und das AppArmor-Profil des Proxmox-Hosts blockiert das. Da der Dienst ausschließlich nach journald loggt, gibt es dann **keine** Logs. Betrifft jede Debian-13-LXC auf Proxmox 8.4 (Debian 12 mit systemd 252 nicht) und legt auch logind, networkd, console-getty und tmpfiles-setup lahm. Abhilfe und Hintergrund: [docs/INSTALLATION.md](docs/INSTALLATION.md#debian-13-in-lxc-auf-proxmox-journald-scheitert-243credentials). In 0.6.3 stand hier noch, das sei ein Schaden auf genau einer Maschine — das war falsch.
- **Debian 13 in LXC auf Proxmox: journald scheitert mit `243/CREDENTIALS`.** systemd ≥ 255 (Debian 13 hat 257) setzt `ImportCredential=journal.*`; der Hilfsprozess `(sd-mkdcreds)` mountet dafür, und das AppArmor-Profil des Proxmox-Hosts blockiert das. Da der Dienst ausschließlich nach journald loggt, gibt es dann **keine** Logs. Betrifft jede Debian-13-LXC auf Proxmox 8.4 (Debian 12 mit systemd 252 nicht) und legt auch logind, networkd, console-getty und tmpfiles-setup lahm. Abhilfe und Hintergrund: [docs/INSTALLATION.md](docs/INSTALLATION.md#debian-13-in-lxc-auf-proxmox-journald-scheitert-243credentials). In 0.6.3 stand hier noch, das sei ein Schaden auf genau einer Maschine — das war falsch. **Nach der Reparatur die Instanzen einmal neu starten** (`systemctl restart 'pdf-ocr-hotfolder@*'`): wer gestartet wurde, während journald tot war, hat danach ein leeres Journal (`-- No entries --`) — das sieht aus wie eine gescheiterte Reparatur, ist aber nur der fehlende Neustart.
- **Ein nicht aufrufbares veraPDF war bis 0.6.3 der gefährlichste Fehler des Dienstes.** `run_verapdf()` lieferte für ein fehlendes Binary, einen Timeout oder eine leere Ausgabe schlicht `False` — also ein inhaltliches FAIL-Urteil. Damit wanderte jedes OCR-Ergebnis nach `error/` und das Original wurde laut `original_on_success` entsorgt, beim Default `delete` also gelöscht. Scan für Scan, bei grünem `systemctl status`. Seit 0.7.0: Preflight-Prüfung (Exit 2) **und** `VeraPdfUnavailable` als eigene Ausnahme, die ausdrücklich kein Urteil über die Datei ist.
- **systemd-Hardening bricht in LXC-Containern** (`Error 226/NAMESPACE` durch `PrivateTmp`, `ProtectSystem` usw., Issue #4). Gegenmittel ist das Drop-in `systemd/lxc-compat.conf` nach `/etc/systemd/system/pdf-ocr-hotfolder@.service.d/`; der Installer erkennt Container via `systemd-detect-virt --container` und bietet es an, `update.sh` zieht ein vorhandenes Drop-in nach.
- **Das Paket wird nicht pip-installiert, sondern nach `/opt/pdf-ocr-hotfolder` kopiert.** Gestartet wird per `python -m pdf_ocr_hotfolder`, gefunden wird das Modul nur über das Arbeitsverzeichnis — `WorkingDirectory=/opt/pdf-ocr-hotfolder` in der Unit ist daher Pflicht, nicht Kosmetik (Issue #5). Auch `update.sh` ruft `--check-config` deshalb mit `cd "$INSTALL_DIR"` auf.
- **Relative Pfade in der Config waren still falsch.** Sie wurden gegen `WorkingDirectory=/opt/pdf-ocr-hotfolder` aufgelöst, nicht gegen das Verzeichnis der Config. Seit 0.7.0 `ConfigError` + Exit 2 für `[paths]`, `[output].archive_dir`, `[upload.folder].target`. **Das ist der einzige Fall, in dem ein Update von 0.6.x eine bisher „laufende" Instanz stoppt** — gewollt, siehe [docs/UPDATE.md](docs/UPDATE.md#relative-pfade--fehler-seit-070).
- **Klartext-Passwörter in der Instanz-Config**: SMTP-, Nextcloud- und SFTP-Zugangsdaten stehen unverschlüsselt in `/etc/pdf-ocr-hotfolder/<instanz>.toml`. Deshalb `chmod 640` und `chown root:<service-gruppe>`, und `/etc/pdf-ocr-hotfolder` selbst `750 root:pdfocr`. **Das Update-Backup enthält diese Configs** und ist deshalb `0600 root:root` in einem `700`-Verzeichnis. Beim Debuggen weder Config noch Backup in ein Ticket kopieren.
- **Das Update-Backup enthält die venv NICHT.** Ein Rollback per `tar -xzf … -C /` holt den Paketstand also nicht zurück, und `tar` löscht nichts, was neu hinzugekommen ist. Grenzen des Rollbacks: [docs/UPDATE.md](docs/UPDATE.md#grenzen-des-rollbacks).
- **Auf einem frischen Proxmox-Debian-Template (12 und 13) fehlen `sudo` und `git`.** `sudo ./install.sh` scheitert dort mit `sudo: command not found` — als `root` direkt (`./install.sh`) laeuft alles; die Skripte brauchen root-Rechte, nicht `sudo`. Und ohne `git` gibt es keinen Clone. Beides steht jetzt in den Voraussetzungen ([docs/INSTALLATION.md](docs/INSTALLATION.md#pakete-die-auf-einem-frischen-system-fehlen-können)). Wer Doku aendert: `sudo` **nicht** ueberall streichen — die meisten Admins haben es; beide Wege nennen.
- **`systemctl start` kann kein Glob.** `stop 'pdf-ocr-hotfolder@*'` trifft alle laufenden Instanzen (die Units sind geladen), `start 'pdf-ocr-hotfolder@*'` versucht eine Instanz namens `*` zu starten und scheitert. Nach einem Rollback muss deshalb **jede Instanz einzeln** gestartet werden ([docs/UPDATE.md](docs/UPDATE.md#rollback)). `restart` geht wieder mit Glob.
## 🛠️ Entwicklung
@@ -448,7 +454,8 @@ pytest # aktuell 254 Tests
- **Repo:** https://gitea.sonith.de/sonith_ug/pdf-ocr-hotfolder
- **Owner:** sonith_ug
- **SSH-User ist `gitea`, nicht `git`:** `gitea@gitea.sonith.de:sonith_ug/pdf-ocr-hotfolder.git`
- **Clone per HTTPS (Normalfall, ohne Credentials):** `https://gitea.sonith.de/sonith_ug/pdf-ocr-hotfolder.git` — das ist der Weg, den die Installations-Doku nennt und der im Erstinstallations-Test benutzt wurde.
- **SSH nur mit Deploy-Key — und der SSH-User ist `gitea`, nicht `git`:** `gitea@gitea.sonith.de:sonith_ug/pdf-ocr-hotfolder.git`
- **Versionierung:** Semver (PATCH bei jedem Build, MINOR bei Features, MAJOR manuell)
- **Tags:** `v{VERSION}`, automatischer Push nach Commit
+48
View File
@@ -1,5 +1,53 @@
# Changelog
## [0.7.1] - 2026-09-23
Gefunden beim ersten echten Erstinstallations-Test auf frischen Debian-12-
und Debian-13-Containern. Der Installationsweg selbst hat getragen; diese
drei Stellen haben gelogen oder gefehlt.
### Fixed
- **Das Ghostscript-Angebot auf Debian 12 war ein No-Op, der sich als Erfolg
meldete** (`Ghostscript aktualisiert: 10.00.0 -> 10.00.0 ✓`). Grund:
`bookworm-backports` enthaelt ueberhaupt kein `ghostscript` — am echten
Paketindex verifiziert (2606 Pakete, ghostscript nicht dabei, Kontroll-
pakete wie systemd/golang-go sehr wohl). Zurueck blieb eine nutzlose
`sources.list.d`-Quelle. Die Routine (jetzt `check_ghostscript` in
`lib/common.sh`) sucht nun erst den tatsaechlichen Kandidaten, vergleicht
die Version vorher/nachher, meldet "unveraendert" als Warnung statt als
Erfolg und entfernt eine nur zur Probe angelegte Quelle wieder. Eine
bereits vorhandene Backports-Quelle bleibt unangetastet.
- **Der Rat "Ghostscript aus bookworm-backports" ist ueberall raus** — er
stand auch in der Preflight-Fehlermeldung, der pdfa_level-Warnung,
config.example.toml und vier Doku-Dateien. Ersetzt durch die echten
Optionen: pdfa_level leer lassen (Default), skip_text = false, oder
Debian 13 (gs 10.05.1). Ein Test prueft negativ, dass der Rat nicht
zurueckkommt.
- **Mehrzeilige Log-Hinweise waren zerrissen und nicht kopierbar**: die
log-Funktionen nutzten `echo -e`, wodurch `\n` in Hinweistexten zu echten
Zeilenumbruechen ohne `[WARN]`-Praefix wurden. Jetzt `printf` mit `%s`
fuer den Text — das Problem kann strukturell nicht wiederkommen.
- **`pip-freeze.txt` landete beim Rollback im Wurzelverzeichnis.** Sie liegt
im Backup jetzt unter `opt/pdf-ocr-hotfolder/` und damit nach dem
Entpacken neben der Installation.
### Added (Doku)
- Voraussetzungen, die auf einem frischen Proxmox-Debian-Template fehlen:
**`git` und `sudo`** sind dort nicht installiert — der dokumentierte
Aufruf `sudo ./install.sh` scheitert mit `sudo: command not found`.
Beide Wege (root direkt / sudo) sind jetzt beschrieben.
- HTTPS- statt SSH-Clone als Normalfall (funktioniert ohne Credentials);
SSH-Variante mit dem Hinweis, dass der User `gitea` heisst, nicht `git`.
- Entscheidungstabelle zu PDF/A auf Debian 12 vs. 13. Praezisierung: die
Sackgasse ist PDF/A **zusammen mit** `skip_text = true`; mit
`skip_text = false` geht PDF/A auch auf Debian 12, nur langsamer.
- Rollback: `systemctl start` kann kein Glob (anders als `stop`), bei
mehreren Instanzen jede einzeln starten. Dazu, was ein Rollback
nachweislich zurueckholt und was nicht.
- journald-Reparatur: die Instanzen danach einmal neu starten, sonst bleibt
das Journal leer und die Reparatur sieht gescheitert aus.
- "So sieht ein Erstlauf aus" — die Reihenfolge der Abfragen.
## [0.7.0] - 2026-09-23
Schliesst die stillen Datenverlust-Pfade: gleichnamige Dateien werden nirgends
+21 -5
View File
@@ -19,7 +19,7 @@ Weiter: [CHANGELOG.md](CHANGELOG.md) · [AI_AGENT_BRIEFING.md](AI_AGENT_BRIEFING
- 🧠 **Stabilitäts-Erkennung**: wartet bis Scanner fertig geschrieben hat
- 🔁 **Parallelverarbeitung** mehrerer PDFs (ThreadPool, konfigurierbar)
- ♻️ **Wiederaufnahme aus `working/`** nach einem harten Stopp — keine Datei bleibt liegen
- ✅ **PDF/A-Output** (1, 2 oder 3) optional
- ✅ **PDF/A-Output** (1, 2 oder 3) optional — **aber nicht auf Debian 12**: dessen Ghostscript (10.0.0–10.02.0) hat einen Bug, ocrmypdf lehnt PDF/A zusammen mit `skip_text = true` ab, und ein neueres Ghostscript gibt es dort nicht (`bookworm-backports` enthält kein Ghostscript-Paket). Wer PDF/A braucht, installiert auf **Debian 13** (Ghostscript 10.05.1) oder setzt `skip_text = false` ([Details](docs/INSTALLATION.md#ghostscript-bug-auf-debian-12))
- 🛡️ **veraPDF-Validierung** optional — Binary wird im Preflight geprüft, eine Störung gilt nicht als FAIL
- 🚫 **Überschreibt nie eine gleichnamige Datei** — in `outgoing/`, `error/`, Archiv und Ordner-Upload weicht sie mit Zeitstempel aus
- ☁️ **Upload-Ziele**: lokaler Ordner, Nextcloud (WebDAV via Python), SFTP
@@ -38,11 +38,26 @@ Dateisystem **ext4, xfs oder zfs**; `incoming/` muss **lokal** liegen — auf
einem CIFS/NFS-Mount liefert inotify keine Events und der Hotfolder bemerkt
neue Dateien nur noch beim Start
([warum](docs/INSTALLATION.md#dateisystem-ext4-xfs-oder-zfs)).
Außerdem **`git`** (für den Clone) und, falls nicht als `root` gearbeitet wird,
**`sudo`** — beides fehlt im Proxmox-Debian-Standard-Template
([Details](docs/INSTALLATION.md#voraussetzungen)):
```bash
apt update && apt install -y git # als root; ggf. zusätzlich: sudo
```
```bash
# HTTPS — funktioniert ohne Credentials, das ist der Normalfall
git clone https://gitea.sonith.de/sonith_ug/pdf-ocr-hotfolder.git
cd pdf-ocr-hotfolder
./install.sh # als root; mit sudo: sudo ./install.sh
```
Wer einen Deploy-Key hinterlegt hat, klont per SSH — **der SSH-User heißt
`gitea`, nicht `git`**:
```bash
git clone gitea@gitea.sonith.de:sonith_ug/pdf-ocr-hotfolder.git
cd pdf-ocr-hotfolder
sudo ./install.sh
```
Der Installer legt einmalig Code, venv und die systemd-Template-Unit an und
@@ -62,7 +77,8 @@ Nach wenigen Sekunden liegt das OCR-PDF im `outgoing/`-Ordner der Instanz.
Alle Details zu den Abfragen, zum Multi-Instanz-Betrieb und zu den Fallstricken
(LXC, Ghostscript): **[docs/INSTALLATION.md](docs/INSTALLATION.md)**.
Update: `git pull && sudo ./update.sh` — siehe **[docs/UPDATE.md](docs/UPDATE.md)**.
Update: `git pull && ./update.sh` (als root; mit `sudo`:
`sudo ./update.sh`) — siehe **[docs/UPDATE.md](docs/UPDATE.md)**.
Nach einem Debian-Major-Upgrade: **[docs/OS-UPGRADE.md](docs/OS-UPGRADE.md)**.
## Verzeichnisse
@@ -180,5 +196,5 @@ MIT — © Sonith UG
---
**Version:** 0.7.0
**Version:** 0.7.1
**Repo:** https://gitea.sonith.de/sonith_ug/pdf-ocr-hotfolder
+1 -1
View File
@@ -1 +1 @@
0.7.0
0.7.1
+6
View File
@@ -30,6 +30,12 @@ oversample = 300
# ocrmypdf lehnt damit die Kombination pdfa_level + skip_text=true komplett ab.
# Nur auf "1"/"2"/"3" setzen, wenn gs >= 10.02.1 installiert ist.
#
# Auf Debian 12 lässt sich Ghostscript NICHT anheben: bookworm-backports
# enthält kein Ghostscript-Paket. Wer dort PDF/A braucht, hat auf dieser
# Distribution keinen Weg — es bleiben pdfa_level = "" (kein PDF/A),
# skip_text = false oder eine Distribution mit neuerem Ghostscript
# (Debian 13 liefert 10.05.1).
#
# pdfa_level = "" ist deshalb der Default — aber KEIN genereller Schutz gegen
# den Ghostscript-Bug: das gilt erst zusammen mit ocrmypdf >= 17. Bis
# ocrmypdf 16.x läuft dieselbe Prüfung auch ohne PDF/A, und dann scheitert mit
+134 -18
View File
@@ -14,10 +14,46 @@ Verwandte Dokumente: [README](../README.md) · [Update](UPDATE.md) · [Debian-Ma
| Python | 3.11+ (wegen `tomllib` aus der stdlib); kommt aus der Distribution |
| Arbeitsspeicher | **mindestens 2 GB** für den produktiven Betrieb — siehe [Systemanforderungen](#systemanforderungen) |
| Dateisystem | **ext4, xfs oder zfs**; `incoming/` **lokal**, kein CIFS/NFS — siehe [Dateisystem](#dateisystem-ext4-xfs-oder-zfs) |
| Rechte | `root` (`sudo ./install.sh`) |
| Rechte | `root` — als root direkt `./install.sh`, sonst `sudo ./install.sh` (s. [root oder sudo](#root-oder-sudo)) |
| `git` | zum Klonen des Repos — **nicht** vorinstalliert, s. [Pakete, die fehlen können](#pakete-die-auf-einem-frischen-system-fehlen-können) |
| Netz | apt-Zugriff für die System-Pakete, PyPI-Zugriff für die venv |
| Repo | muss dauerhaft liegen bleiben — `update.sh` kopiert daraus (s. [UPDATE.md](UPDATE.md)) |
### Pakete, die auf einem frischen System fehlen können
Auf dem **Proxmox-Debian-Standard-Template** (12 **und** 13) fehlen zwei Dinge,
die jede Anleitung stillschweigend voraussetzt — am frisch angelegten Container
verifiziert:
| Fehlt | Folge | Abhilfe |
|-------|-------|---------|
| **`git`** | `git clone …` schlägt mit `git: command not found` fehl — und ohne Clone gibt es kein Repo, aus dem `install.sh` läuft | `apt install git` |
| **`sudo`** | der überall dokumentierte Aufruf `sudo ./install.sh` schlägt mit `sudo: command not found` fehl | entweder `apt install sudo`, oder **einfach als `root` ohne `sudo` arbeiten** |
```bash
apt update
apt install -y git # zwingend
apt install -y sudo # nur, wenn nicht als root gearbeitet wird
```
### root oder sudo
Installer und Updater brauchen **root-Rechte** — *wie* man dahin kommt, ist
ihnen gleich. Beide Wege sind gleichwertig:
```bash
# (a) man ist bereits root — im frischen Container der Normalfall
./install.sh
# (b) man arbeitet als normaler Benutzer und hat sudo
sudo ./install.sh
```
In dieser Doku steht durchgehend die Variante mit `sudo`, weil die meisten
Systeme es haben. **Wer als `root` arbeitet, lässt das `sudo` bei jedem Befehl
einfach weg** — das gilt für alle Kommandos in diesem und den übrigen
Dokumenten. Ein fehlendes `sudo` ist kein Grund, es nachzuinstallieren.
Die System-Pakete installiert der Installer selbst. Die Liste steht als
einzige Quelle in `lib/common.sh` (Funktion `pdf_ocr_apt_packages()`, zwischen
den Marken `# --- BEGIN apt-packages` / `# --- END apt-packages`); `install.sh`
@@ -174,14 +210,51 @@ System mehr RAM bekommt.
## Installation
```bash
git clone gitea@gitea.sonith.de:sonith_ug/pdf-ocr-hotfolder.git
apt update && apt install -y git # fehlt im Proxmox-Standard-Template
git clone https://gitea.sonith.de/sonith_ug/pdf-ocr-hotfolder.git
cd pdf-ocr-hotfolder
sudo ./install.sh
sudo ./install.sh # als root: ./install.sh
```
#### Welche Clone-URL?
| Variante | URL | Wann |
|----------|-----|------|
| **HTTPS** (Normalfall) | `https://gitea.sonith.de/sonith_ug/pdf-ocr-hotfolder.git` | braucht **keine** Credentials und funktioniert auf einem frischen Server sofort — der Weg, der im Installations-Test benutzt wurde |
| **SSH** | `gitea@gitea.sonith.de:sonith_ug/pdf-ocr-hotfolder.git` | nur sinnvoll, wenn auf dem Zielsystem ein **Deploy-Key** hinterlegt ist (z.B. weil auch gepusht werden soll) |
> ⚠️ **Der SSH-User bei `gitea.sonith.de` heißt `gitea`, nicht `git`.**
> `git@gitea.sonith.de:…` ist der Default anderer Git-Hoster und hier **falsch**
> — der Clone scheitert dann mit `Permission denied (publickey)`.
`install.sh` ist **Installer und Instanz-Manager in einem** und idempotent —
jeder weitere Aufruf überspringt, was schon steht.
### So sieht ein Erstlauf aus
Verifiziert auf frischen Debian-12- und Debian-13-Containern. Die Reihenfolge
der Abfragen:
1. **LXC-Drop-in** — nur im Container: „systemd-Hardening-Drop-in für LXC
installieren?" → **ja**, sonst scheitert der Start mit `226/NAMESPACE`
([warum](#lxccontainer-error-226namespace)).
2. **Ghostscript-Hinweis** — nur auf Debian 12 mit betroffener Version. Kein
Abbruch: mit dem Default `pdfa_level = ""` ist die Installation
unproblematisch ([Hintergrund](#ghostscript-bug-auf-debian-12)).
3. **journald-Warnung** — nur, wenn `systemd-journald` nicht läuft. Typisch für
Debian 13 in LXC auf Proxmox. Hier **abbrechen**, journald reparieren und neu
anfangen — sonst hat der Dienst kein Log
([Abhilfe](#debian-13-in-lxc-auf-proxmox-journald-scheitert-243credentials)).
4. Dann die **fünf Fragen pro Instanz**: Instanz-Name → Basis-Pfad →
Service-User → OCR-Sprachen → Original-Behandlung (Archiv/löschen/liegen
lassen). Im Detail: [Die Abfragen pro Instanz](#die-abfragen-pro-instanz).
5. Zum Schluss `Weitere Instanz anlegen? [j/N]:` — hier entsteht die zweite
Instanz, ohne dass irgendetwas am Basis-Install noch einmal angefasst wird.
Danach läuft `pdf-ocr-hotfolder@<instanz>.service`. Gegenprobe: eine PDF nach
`incoming/` kopieren und `journalctl -u pdf-ocr-hotfolder@<instanz> -f`
mitlesen.
### Basis-Install vs. Instanz-Anlage
Der Installer unterscheidet zwei Ebenen:
@@ -424,21 +497,40 @@ ocrmypdf-Version:
### Abhilfe
**Weg 1 — Ghostscript anheben** (empfohlen). Der Installer erkennt betroffene
Versionen und bietet auf Debian 12 bookworm-backports an. Manuell:
> 🚫 **Es gibt auf Debian 12 kein neueres Ghostscript.** `bookworm-backports`
> führt **kein** Ghostscript-Paket — am Paketindex geprüft
> (`bookworm-backports/main/binary-amd64`: 2606 Pakete, `ghostscript` nicht
> darunter, `systemd`/`golang-go`/`linux-image-amd64` schon). Ältere Fassungen
> dieser Anleitung und frühere Versionen des Installers haben genau das
> empfohlen — der Rat konnte nie funktionieren.
>
> **Planungsaussage:** Wer auf Debian 12 **PDF/A in der schnellen Betriebsart**
> (`skip_text = true`) braucht, hat dort **keinen Weg** — weder über Backports
> noch sonst. Es bleibt: PDF/A aufgeben, `skip_text` aufgeben (Weg 2, kostet
> Laufzeit) oder Debian 13. **Das gehört vor die Installation**, nicht in den
> Moment, in dem der Preflight abbricht.
```bash
echo 'deb http://deb.debian.org/debian bookworm-backports main' | \
sudo tee /etc/apt/sources.list.d/bookworm-backports.list
sudo apt update && sudo apt install -t bookworm-backports ghostscript
```
Ab Ghostscript 10.02.1 ist alles in Ordnung; PDF/A kann dann eingeschaltet
werden.
**Weg 1 — `pdfa_level = ""` lassen** (Default, empfohlen). Ohne PDF/A-Ausgabe
fasst ocrmypdf ≥ 17 Ghostscript gar nicht an; die betroffene Version ist dann
völlig unproblematisch. Der Preis ist PDF/A — das Ergebnis ist eine normale
durchsuchbare PDF. Für die übliche Anwendung (Scan wird durchsuchbar) reicht
das.
**Weg 2 — `skip_text = false` setzen.** Dann wird vorhandener Text neu erkannt
statt übersprungen, und die Bedingung greift nicht mehr. Das kostet Laufzeit bei
PDFs, die bereits eine Textebene haben.
statt übersprungen, und die Bedingung greift nicht mehr — PDF/A ist damit auch
auf Debian 12 möglich. Das kostet Laufzeit bei PDFs, die bereits eine Textebene
haben, und OCRt sie ein zweites Mal.
**Weg 3 — Distribution mit neuerem Ghostscript.** **Debian 13 liefert
Ghostscript 10.05.1** und ist vom Bug nicht betroffen; dort ist PDF/A zusammen
mit `skip_text = true` ohne Einschränkung nutzbar. Wer PDF/A verbindlich
braucht, installiert von vornherein auf Debian 13.
| Ich brauche … | Debian 12 | Debian 13 |
|---------------|-----------|-----------|
| durchsuchbare PDF, kein PDF/A | ✅ Default (`pdfa_level = ""`) | ✅ |
| PDF/A **und** `skip_text = true` | ❌ kein Weg | ✅ |
| PDF/A mit `skip_text = false` | ✅ (langsamer) | ✅ |
---
@@ -762,6 +854,13 @@ Ist `systemd-journald.service` selbst `failed` (beobachtet mit
`status=243/CREDENTIALS`), gibt es schlicht kein journal, in das geschrieben
werden könnte.
Läuft journald dagegen `active (running)` und das Journal der Instanz ist
trotzdem leer (`-- No entries --`), ist meist journald **erst nach dem Dienst**
wieder hochgekommen: die Startmeldungen hatten in der Zwischenzeit kein Ziel und
sind weg. `sudo systemctl restart 'pdf-ocr-hotfolder@*'` schreibt sie neu —
siehe die Warnung im
[nächsten Abschnitt](#debian-13-in-lxc-auf-proxmox-journald-scheitert-243credentials).
**Einordnung.** Der Dienst loggt seit v0.4.1 **ausschließlich** nach journald —
es gibt bewusst kein eigenes Logfile und kein Logverzeichnis. Ein kaputtes
journald ist damit ein blinder Fleck: jede Fehlersuche läuft ins Leere, und der
@@ -843,9 +942,26 @@ sudo systemctl restart systemd-journald
sudo journalctl --flush
```
Danach `systemctl status systemd-journald` (muss `active (running)` sein) und
`journalctl -u pdf-ocr-hotfolder@<instanz>` gegenprüfen. Für die anderen
betroffenen Units gilt dasselbe Muster mit deren Unit-Namen.
Danach `systemctl status systemd-journald` gegenprüfen — muss `active
(running)` sein. Für die anderen betroffenen Units gilt dasselbe Muster mit
deren Unit-Namen.
> ⚠️ **Jetzt die Instanzen einmal neu starten — sonst steht man vor einem
> leeren Journal.** Wurde der Dienst gestartet, **während** journald tot war,
> sind seine Startmeldungen unwiederbringlich weg: sie hatten kein Ziel. Nach
> der Reparatur meldet `journalctl -u pdf-ocr-hotfolder@<instanz>` dann
> `-- No entries --` — und das sieht exakt so aus wie ein gescheiterter
> Reparaturversuch, obwohl journald längst wieder läuft. Der Neustart schreibt
> die Startmeldungen neu ins frische Journal:
>
> ```bash
> sudo systemctl restart 'pdf-ocr-hotfolder@*'
> journalctl -u pdf-ocr-hotfolder@<instanz> -n 20
> ```
>
> Erst wenn hier die Startmeldungen stehen, ist die Reparatur belegt. (`restart`
> kann das Glob, weil die Units geladen sind — `start` nicht, siehe
> [UPDATE.md](UPDATE.md#rollback).)
> **Der saubere Weg liegt host-seitig.** Das Drop-in kuriert das Symptom im
> Container. Richtig behoben wird es auf dem Proxmox-Host: Update von
+23 -3
View File
@@ -6,6 +6,14 @@ Verwandte Dokumente: [README](../README.md) · [Installation](INSTALLATION.md)
---
> **`sudo` oder direkt als `root`.** Alle Befehle dieser Seite brauchen
> root-Rechte, nicht `sudo`. Wer als `root` arbeitet — im
> Proxmox-Debian-Standard-Template der Normalfall, dort ist `sudo` gar nicht
> installiert —, lässt das `sudo` einfach weg. Siehe
> [INSTALLATION.md](INSTALLATION.md#root-oder-sudo).
---
## Warum das ein eigener Ablauf ist
Die venv unter `/opt/pdf-ocr-hotfolder/venv/` hängt an der **Python-Version der
@@ -179,11 +187,23 @@ Ghostscript) wirklich anfasst — `systemctl status` sagt darüber nichts.
gs --version
```
Auf Debian 13 ist der Ghostscript-Bug aus Debian 12 kein Thema mehr; ein
bookworm-backports-Eintrag unter `/etc/apt/sources.list.d/` gehört nach dem
Upgrade entfernt. Hintergrund:
Auf Debian 13 ist der Ghostscript-Bug aus Debian 12 kein Thema mehr — **Debian
13 liefert Ghostscript 10.05.1**. Damit ist PDF/A (`[ocr].pdfa_level = "1"`,
`"2"` oder `"3"`) zusammen mit `skip_text = true` erstmals ohne Einschränkung
nutzbar; auf Debian 12 gab es dafür **keinen** Weg (ein Upgrade aus
`bookworm-backports` existiert nicht, dort liegt kein Ghostscript-Paket). Genau
das ist oft der Grund für das Upgrade. Hintergrund:
[INSTALLATION.md](INSTALLATION.md#ghostscript-bug-auf-debian-12).
Hat jemand auf dem alten System nach der früheren, falschen Empfehlung einen
`bookworm-backports`-Eintrag unter `/etc/apt/sources.list.d/` angelegt, gehört
er nach dem Upgrade entfernt — Ghostscript kam ohnehin nie daher:
```bash
sudo rm -f /etc/apt/sources.list.d/bookworm-backports.list
sudo apt update
```
---
## Pins in `requirements.txt`
+58 -6
View File
@@ -24,6 +24,13 @@ sudo ./update.sh --rebuild-venv # venv zwingend neu bauen (nach dist-upgrade)
sudo ./update.sh --no-smoke-test # ohne Rauchtest durchlaufen
```
> **`sudo` oder direkt als `root`.** `update.sh` braucht root-Rechte, nicht
> `sudo`. Wer als `root` arbeitet — im Proxmox-Debian-Standard-Template der
> Normalfall, dort ist `sudo` gar nicht installiert —, lässt das `sudo` bei
> jedem Befehl dieser Seite einfach weg: `./update.sh`. Ebenso setzt `git pull`
> ein installiertes **`git`** voraus; siehe
> [INSTALLATION.md](INSTALLATION.md#pakete-die-auf-einem-frischen-system-fehlen-können).
`update.sh` muss aus dem Repo laufen. Findet es sich nicht selbst im Repo, liest
es den gespeicherten Pfad aus `/opt/pdf-ocr-hotfolder/.repo_path` — **das Repo
muss also liegen bleiben**, das Tool kopiert daraus.
@@ -126,10 +133,14 @@ Vor dem ersten Eingriff auf der Platte schreibt `update.sh` ein Archiv:
| `/etc/pdf-ocr-hotfolder/` (alle Instanz-Configs) | die **Datenverzeichnisse** `/var/lib/pdf-ocr-hotfolder/` |
| Template-Unit `pdf-ocr-hotfolder@.service` | `__pycache__`, `*.pyc` |
| alle Drop-in-Verzeichnisse `…@*.service.d` | |
| `pip-freeze.txt` — `pip freeze` der **alten** venv plus Zeitstempel und Versionssprung | |
| `opt/pdf-ocr-hotfolder/pip-freeze.txt` — `pip freeze` der **alten** venv plus Zeitstempel und Versionssprung | |
`pip-freeze.txt` ist die Versicherung für den Fall, dass ein neuer Pin Ärger
macht: man sieht schwarz auf weiß, welche Paketversionen vorher liefen.
macht: man sieht schwarz auf weiß, welche Paketversionen vorher liefen. Sie
liegt im Archiv unter `opt/pdf-ocr-hotfolder/` und landet beim Entpacken nach
`/` folglich als `/opt/pdf-ocr-hotfolder/pip-freeze.txt` — also neben der
Installation statt im Wurzelverzeichnis. Ein Code-Tausch beim nächsten Update
löscht sie nicht (dort fliegen nur `pdf_ocr_hotfolder/` und `lib/`).
**Rechte:** Das Archiv enthält die Instanz-Configs und damit **Klartext-Passwörter**
(SMTP, Nextcloud, SFTP). Es wird deshalb mit `umask 077` erzeugt und danach auf
@@ -153,24 +164,65 @@ Das Backup-Archiv ist wurzelrelativ gepackt und lässt sich direkt zurückspiele
sudo systemctl stop 'pdf-ocr-hotfolder@*'
sudo tar -xzf /var/backups/pdf-ocr-hotfolder/backup-YYYYmmdd-HHMMSS.tar.gz -C /
sudo systemctl daemon-reload
sudo systemctl start 'pdf-ocr-hotfolder@<instanz>'
sudo systemctl start pdf-ocr-hotfolder@kunde-a
sudo systemctl start pdf-ocr-hotfolder@kunde-b # jede Instanz einzeln!
```
Das Skript nennt diesen Befehl mit dem konkreten Archivnamen selbst — sowohl
beim Abbruch als auch bei einer erkannten Regression.
> ⚠️ **`start` kann kein Glob.** `systemctl stop 'pdf-ocr-hotfolder@*'` trifft
> alle laufenden Instanzen, weil systemd dafür die **bereits geladenen** Units
> auflösen kann. Beim Starten gibt es nichts aufzulösen:
> `systemctl start 'pdf-ocr-hotfolder@*'` startet eine Instanz mit dem
> wörtlichen Namen `*` — und scheitert. **Bei mehreren Instanzen muss jede
> einzeln gestartet werden.** Welche es sind:
> `ls /etc/pdf-ocr-hotfolder/*.toml`. Am Stück:
>
> ```bash
> for f in /etc/pdf-ocr-hotfolder/*.toml; do
> n=$(basename "$f" .toml)
> sudo systemctl start "pdf-ocr-hotfolder@$n"
> done
> systemctl status 'pdf-ocr-hotfolder@*' --no-pager
> ```
### Was ein Rollback nachweislich zurückholt
Einmal real durchgespielt (Debian 12 **und** 13, Update auf den neuen Stand,
danach Rollback auf das Backup). Zurück kamen korrekt:
- **der Code** unter `/opt/pdf-ocr-hotfolder/` inklusive `lib/` — die alte
Version lief danach wieder,
- **alle Instanz-Configs** unter `/etc/pdf-ocr-hotfolder/` — **mit den
`640`-Rechten und dem `root:<service-gruppe>`-Eigentum**; die
Klartext-Passwörter bleiben also geschützt, `tar` stellt Modus und Eigentümer
mit her,
- die **Template-Unit** `pdf-ocr-hotfolder@.service` und
- die **Drop-ins** unter `…@*.service.d/` (LXC-Kompat, User-Drop-in).
Nach `daemon-reload` und dem Einzelstart liefen die Instanzen wieder mit dem
alten Stand. Was dabei **nicht** zurückkommt, steht im nächsten Abschnitt.
### Grenzen des Rollbacks
Ein Rollback ist ein **Overlay**, kein exaktes Zurücksetzen:
Ein Rollback ist ein **Overlay**, kein exaktes Zurücksetzen — beides im Test
bestätigt:
- **Die venv ist nicht im Backup.** Wurde sie beim Update neu gebaut oder
hat `pip install --upgrade` Pakete angehoben, holt das Rollback den alten
Stand der Pakete **nicht** zurück. Dafür ist `pip-freeze.txt` aus dem Archiv
da: die dort genannten Versionen lassen sich von Hand wiederherstellen
da — nach dem Entpacken unter `/opt/pdf-ocr-hotfolder/pip-freeze.txt`: die
dort genannten Versionen lassen sich von Hand wiederherstellen
(`venv/bin/pip install -r …`).
Im Test lief nach dem Rollback die **alte** Code-Version in der **neuen**
venv — was hier gutging, weil sich die Pins nicht geändert hatten. Verlassen
darf man sich darauf nicht: nach einem Update mit Versionssprung gehört die
venv nach dem Rollback von Hand auf den alten Paketstand gebracht.
- **Dateien, die es vorher nicht gab, bleiben liegen.** `tar -x` legt nur an und
überschreibt; es löscht nichts. Eine mit dem neuen Stand hinzugekommene Datei
im Code-Verzeichnis überlebt das Rollback. Sauberer ist deshalb
im Code-Verzeichnis überlebt das Rollback — im Test nachgestellt und
bestätigt. Sauberer ist deshalb
`rm -rf /opt/pdf-ocr-hotfolder/pdf_ocr_hotfolder` **vor** dem Entpacken.
- **Die Datenverzeichnisse sind nicht im Backup** — gewollt. Ein Rollback
verändert keine PDFs, weder in `incoming/` noch in `error/`.
+9 -38
View File
@@ -51,42 +51,11 @@ install_base() {
log_info "System-Pakete ok ✓"
# Ghostscript-Versions-Check (Issue #3 + Issue #6)
if command -v gs >/dev/null 2>&1; then
GS_VER="$(gs --version 2>/dev/null || echo 0.0)"
log_info "Ghostscript: $GS_VER"
case "$GS_VER" in
10.0.0|10.00.0|10.01.*|10.02.0)
echo
log_warn "═══════════════════════════════════════════════════════════════"
log_warn "Ghostscript $GS_VER ist vom PDF/A-Bug betroffen (10.0.0–10.02.0)."
log_warn "Mit pdfa_level + skip_text=true kann ocrmypdf KEINE PDFs verarbeiten."
log_warn "═══════════════════════════════════════════════════════════════"
echo
# Prüfe ob Debian bookworm (12) — Backports anbieten
if grep -q 'bookworm' /etc/os-release 2>/dev/null; then
read -r -p "Ghostscript via bookworm-backports upgraden? [J/n]: " UPGRADE_GS
UPGRADE_GS="${UPGRADE_GS:-J}"
if [[ "$UPGRADE_GS" =~ ^[JjYy]$ ]]; then
log_info "Aktiviere bookworm-backports..."
if ! grep -q 'bookworm-backports' /etc/apt/sources.list /etc/apt/sources.list.d/*.list 2>/dev/null; then
echo 'deb http://deb.debian.org/debian bookworm-backports main' \
> /etc/apt/sources.list.d/bookworm-backports.list
apt-get update -qq
fi
apt-get install -y -t bookworm-backports ghostscript
GS_VER_NEW="$(gs --version 2>/dev/null || echo '?')"
log_info "Ghostscript aktualisiert: $GS_VER → $GS_VER_NEW ✓"
else
log_warn "Workaround: In der Config [ocr].pdfa_level = \"\" setzen (Default ab v0.2.2)"
fi
else
log_warn "Kein Debian bookworm erkannt — manuelles Upgrade nötig."
log_warn "Workaround: In der Config [ocr].pdfa_level = \"\" setzen (Default ab v0.2.2)"
fi
echo
;;
esac
fi
# Die eigentliche Logik steckt in lib/common.sh (check_ghostscript): sie
# prueft nur, was wirklich verfuegbar ist, meldet Erfolg ausschliesslich
# nach einem Versionsvergleich und laesst keine nutzlose apt-Quelle
# zurueck. Ein betroffenes Ghostscript ist kein Abbruchgrund.
check_ghostscript
# LXC/Container-Erkennung (Issue #4)
if systemd-detect-virt --container -q 2>/dev/null; then
@@ -114,9 +83,11 @@ install_base() {
log_warn "Pruefen: systemctl status systemd-journald"
log_warn "Scheitert es mit 243/CREDENTIALS (Debian 13 in LXC auf"
log_warn "Proxmox), hilft ein Drop-in im Container:"
# Jede Zeile ein eigener log_warn und ohne '\n'-Escapes, damit der
# Block zeilenweise mit [WARN]-Praefix herauskommt und sich sauber
# kopieren laesst (das Schreiben der Datei bewusst als Einzeiler).
log_warn " mkdir -p /etc/systemd/system/systemd-journald.service.d"
log_warn " printf '[Service]\\nImportCredential=\\n' > \\"
log_warn " /etc/systemd/system/systemd-journald.service.d/no-credentials.conf"
log_warn " { echo '[Service]'; echo 'ImportCredential='; } > /etc/systemd/system/systemd-journald.service.d/no-credentials.conf"
log_warn " systemctl daemon-reload && systemctl restart systemd-journald"
log_warn "Details: docs/INSTALLATION.md, Abschnitt Troubleshooting."
read -r -p "Trotzdem fortfahren? [J/n]: " JOURNAL_GO
+210 -4
View File
@@ -27,10 +27,17 @@ PDF_OCR_COMMON_LOADED=1
# ============================================================
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
log_step() { echo -e "\n${BLUE}==>${NC} $*"; }
# Die Meldung selbst geht als %s durch, NICHT als %b (frueher: echo -e). Sonst
# wird ein '\n' im Text — etwa in einem Befehl, den wir zum Kopieren anzeigen —
# zu einem echten Zeilenumbruch: die Folgezeilen haetten dann kein [WARN] davor
# und wer den Block kopiert, schleppt das Praefix mit. Nur die Farbcodes
# brauchen %b. Eine Meldung pro Zeile — mehrzeilige Hinweise bitte als mehrere
# Aufrufe schreiben.
log_info() { printf '%b[INFO]%b %s\n' "$GREEN" "$NC" "$*"; }
log_warn() { printf '%b[WARN]%b %s\n' "$YELLOW" "$NC" "$*"; }
log_error() { printf '%b[ERROR]%b %s\n' "$RED" "$NC" "$*"; }
log_step() { printf '\n%b==>%b %s\n' "$BLUE" "$NC" "$*"; }
# Bricht ab, wenn nicht root. $1 = der Aufruf, der gemeint ist.
require_root() {
@@ -50,6 +57,12 @@ require_root() {
: "${SYSTEMD_DIR:=/etc/systemd/system}"
: "${DEFAULT_USER:=pdfocr}"
# Systempfade, die nur die Ghostscript-Pruefung braucht. Ueberschreibbar,
# damit sich die Pruefung gegen eine Fake-Umgebung testen laesst.
: "${OS_RELEASE_FILE:=/etc/os-release}"
: "${APT_SOURCES_LIST:=/etc/apt/sources.list}"
: "${APT_SOURCES_DIR:=/etc/apt/sources.list.d}"
SERVICE_TEMPLATE="pdf-ocr-hotfolder@.service"
LXC_DROPIN_DIR="$SYSTEMD_DIR/${SERVICE_TEMPLATE}.d"
# shellcheck disable=SC2034 # wird von install.sh und update.sh genutzt
@@ -88,6 +101,199 @@ PKGLIST
}
# --- END apt-packages ---
# ============================================================
# Ghostscript
# ============================================================
# Ghostscript 10.0.0 bis 10.02.0 erzeugt fehlerhafte PDF/A-Ausgaben: zusammen
# mit [ocr].pdfa_level UND skip_text = true scheitert ocrmypdf an jeder Datei.
# Mit leerem pdfa_level (der Default) ist so ein Ghostscript voellig
# unproblematisch — die Pruefung ist deshalb ein Hinweis, kein Abbruchgrund.
# Betroffen? $1 = Ausgabe von 'gs --version'.
gs_version_affected() {
case "$1" in
10.0.0|10.00.0|10.01.*|10.02.0) return 0 ;;
*) return 1 ;;
esac
}
# Version, die gs meldet; leer, wenn gs nicht aufrufbar ist.
gs_version() {
command -v gs >/dev/null 2>&1 || return 0
gs --version 2>/dev/null || true
}
# Codename der Distribution (VERSION_CODENAME aus os-release); leer, wenn
# unbekannt.
os_codename() {
[ -r "$OS_RELEASE_FILE" ] || return 0
sed -n 's/^VERSION_CODENAME=//p' "$OS_RELEASE_FILE" | tr -d '"' | head -n1
}
# Ist $1 (z.B. bookworm-backports) schon irgendwo als apt-Quelle eingetragen?
apt_release_configured() {
grep -RqsF -- "$1" "$APT_SOURCES_LIST" "$APT_SOURCES_DIR" 2>/dev/null
}
# Installierte Paketversion laut dpkg; leer, wenn das Paket nicht da ist.
apt_installed_version() {
command -v dpkg-query >/dev/null 2>&1 || return 0
dpkg-query -W -f '${Version}' "$1" 2>/dev/null || true
}
# Hoechste Version von Paket $1, die aus Release $2 (z.B. bookworm-backports)
# zu haben ist — gelesen aus der Versionstabelle von 'apt-cache policy'.
# Leer, wenn das Release das Paket nicht fuehrt. Bewusst nicht hart verdrahtet:
# liefert Debian spaeter doch ein Backports-Ghostscript, greift der Pfad.
apt_version_in_release() {
local pkg="$1" release="$2"
command -v apt-cache >/dev/null 2>&1 || return 0
LC_ALL=C apt-cache policy "$pkg" 2>/dev/null | awk -v rel="$release" '
$1 == "***" { ver = $2; next }
NF == 2 && $1 ~ /^[0-9]/ { ver = $1; next }
ver != "" && index($0, rel) { print ver; exit }
'
}
# Entfernt eine Quelle, die NUR fuer die Pruefung angelegt wurde ($2 = 1).
# War sie vorher schon da, bleibt sie unangetastet — sie kann von etwas
# anderem stammen.
gs_drop_probe_source() {
local list_file="$1" added="$2"
[ "$added" = "1" ] || return 0
rm -f "$list_file" 2>/dev/null || true
log_info "Quelle wieder entfernt: $list_file (sie hat nichts gebracht)"
apt-get update -qq || log_warn "'apt-get update' nach dem Aufraeumen schlug fehl."
}
# Versucht, Ghostscript aus Release $1 zu aktualisieren. $2 = bisherige
# gs-Version. Rueckgabe 0 NUR, wenn hinterher nachweislich eine andere,
# nicht mehr betroffene Version laeuft.
gs_try_release_upgrade() {
local release="$1" old_ver="$2"
local list_file="$APT_SOURCES_DIR/${release}.list"
local added=0 cand installed new_ver new_installed
# Ohne apt-cache/dpkg laesst sich nicht feststellen, was dort ueberhaupt
# liegt — dann wird auch nichts eingetragen.
if ! command -v apt-cache >/dev/null 2>&1 || ! command -v dpkg >/dev/null 2>&1; then
log_warn "apt-cache oder dpkg fehlt — der Paketstand von $release ist nicht pruefbar."
return 1
fi
if apt_release_configured "$release"; then
log_info "$release ist bereits eingetragen — die Quelle bleibt, wie sie ist."
else
log_info "Trage $release voruebergehend ein, um den Paketstand zu pruefen..."
mkdir -p "$APT_SOURCES_DIR" 2>/dev/null || true
if ! printf 'deb http://deb.debian.org/debian %s main\n' "$release" > "$list_file" 2>/dev/null; then
log_warn "$list_file liess sich nicht schreiben — Pruefung nicht moeglich."
return 1
fi
added=1
if ! apt-get update -qq; then
log_warn "'apt-get update' schlug fehl — $release nicht auswertbar."
gs_drop_probe_source "$list_file" "$added"
return 1
fi
fi
installed="$(apt_installed_version ghostscript)"
cand="$(apt_version_in_release ghostscript "$release")"
if [ -z "$cand" ]; then
log_warn "$release fuehrt gar kein Paket 'ghostscript' — von dort kommt nichts."
gs_drop_probe_source "$list_file" "$added"
return 1
fi
if [ -n "$installed" ] && ! dpkg --compare-versions "$cand" gt "$installed"; then
log_warn "$release hat ghostscript $cand — nicht neuer als das installierte $installed."
gs_drop_probe_source "$list_file" "$added"
return 1
fi
log_info "$release bietet ghostscript $cand (installiert: ${installed:-unbekannt}) — installiere..."
if ! apt-get install -y -t "$release" ghostscript; then
log_warn "Installation aus $release schlug fehl — Ghostscript bleibt, wie es war."
gs_drop_probe_source "$list_file" "$added"
return 1
fi
# Erfolg wird NUR gemeldet, wenn sich die Version wirklich geaendert hat.
new_ver="$(gs_version)"
new_installed="$(apt_installed_version ghostscript)"
if [ "$new_ver" = "$old_ver" ] && [ "$new_installed" = "$installed" ]; then
log_warn "Ghostscript unveraendert: $old_ver (Paket ${installed:-unbekannt})."
log_warn "Das Upgrade aus $release hat nichts bewirkt."
gs_drop_probe_source "$list_file" "$added"
return 1
fi
log_info "Ghostscript: $old_ver -> $new_ver (Paket ${installed:-unbekannt} -> ${new_installed:-unbekannt})"
if gs_version_affected "$new_ver"; then
log_warn "Auch $new_ver liegt noch im betroffenen Bereich (10.0.0-10.02.0)."
return 1
fi
log_info "Ghostscript ist jetzt frei vom PDF/A-Fehler ✓"
return 0
}
# Sagt dem Admin, was wirklich zur Auswahl steht. Kein Verweis auf Wege, die
# es nicht gibt.
gs_report_options() {
log_warn "Es bleibt bei der betroffenen Ghostscript-Version. Echte Optionen:"
log_warn " 1. Nichts tun: [ocr].pdfa_level leer lassen (Default). Dann ist der"
log_warn " Fehler folgenlos — die Ausgabe ist nur kein PDF/A."
log_warn " 2. PDF/A wirklich noetig? Dann [ocr].skip_text = false setzen."
log_warn " 3. Neuere Distribution: Debian 13 (trixie) liefert Ghostscript 10.05.1."
log_warn "Die Installation laeuft normal weiter."
}
# Vollstaendige Pruefung fuer den Installer. Gibt immer 0 zurueck: ein
# betroffenes Ghostscript ist ein Hinweis, kein Abbruch.
check_ghostscript() {
local ver codename release answer
ver="$(gs_version)"
if [ -z "$ver" ]; then
log_warn "Ghostscript ist nicht aufrufbar — Versionspruefung uebersprungen."
return 0
fi
log_info "Ghostscript: $ver"
gs_version_affected "$ver" || return 0
echo
log_warn "═══════════════════════════════════════════════════════════════"
log_warn "Ghostscript $ver ist vom PDF/A-Fehler betroffen (10.0.0-10.02.0)."
log_warn "Betroffen sind nur Instanzen mit [ocr].pdfa_level UND skip_text = true;"
log_warn "mit leerem pdfa_level (Default) ist die Version unauffaellig."
log_warn "═══════════════════════════════════════════════════════════════"
echo
codename="$(os_codename)"
if [ "$codename" = "bookworm" ]; then
release="bookworm-backports"
# Bewusst als Frage nach dem PRUEFEN formuliert: ob dort ueberhaupt ein
# neueres Ghostscript liegt, steht erst nach 'apt-cache policy' fest.
read -r -p "In $release nach einem neueren Ghostscript suchen? [J/n]: " answer || answer=""
answer="${answer:-J}"
if [[ "$answer" =~ ^[JjYy]$ ]]; then
if gs_try_release_upgrade "$release" "$ver"; then
echo
return 0
fi
else
log_info "Uebersprungen."
fi
else
log_warn "Kein Debian 12 (bookworm) erkannt${codename:+ (Codename: $codename)} — es gibt hier keinen Backports-Weg."
fi
gs_report_options
echo
return 0
}
# ============================================================
# Python / venv
# ============================================================
+1 -1
View File
@@ -1,3 +1,3 @@
"""PDF OCR Hotfolder — Scanner-PDFs automatisch durchsuchbar machen."""
__version__ = "0.7.0"
__version__ = "0.7.1"
+7 -1
View File
@@ -32,6 +32,9 @@ class OcrConfig:
# blockiert dann JEDE Datei — deshalb pinnt requirements.txt 17.x und der
# Preflight prüft die installierte ocrmypdf-Version mit (siehe
# service._gs_block_reason).
# Auf Debian 12 gibt es KEIN neueres Ghostscript (bookworm-backports führt
# kein Ghostscript-Paket) — PDF/A ist dort mit skip_text schlicht nicht zu
# haben.
pdfa_level: str = ""
deskew: bool = True
clean: bool = False
@@ -292,7 +295,10 @@ def legacy_warnings(cfg: Config) -> list[str]:
"Bug, wegen dem ocrmypdf die Kombination mit skip_text ablehnt "
"(Issue #3). Der Preflight bricht ab, falls die installierte "
"Ghostscript-Version betroffen ist; ab 10.02.1 ist alles in "
"Ordnung."
"Ordnung. Auf Debian 12 lässt sich Ghostscript nicht anheben — "
"bookworm-backports enthält kein Ghostscript. Dort bleiben nur "
"pdfa_level = \"\" (kein PDF/A), skip_text = false oder eine "
"Distribution mit neuerem Ghostscript (Debian 13: 10.05.1)."
)
return out
+27 -9
View File
@@ -254,19 +254,37 @@ def _gs_block_reason(pdfa_level: str, skip_text: bool) -> str | None:
"einzelne PDF würde in error/ landen"
)
if pdfa_level:
abhilfe = (
"Abhilfe — eines davon: "
"(1) [ocr].pdfa_level = \"\" setzen (der Default; ohne PDF/A-Ausgabe "
"fasst ocrmypdf >= 17 Ghostscript gar nicht an, die betroffene "
"Version ist dann unproblematisch) — kostet allerdings PDF/A; "
"(2) [ocr].skip_text = false setzen (dann wird vorhandener Text neu "
"erkannt statt übersprungen, das kostet Laufzeit); "
"(3) eine Distribution mit neuerem Ghostscript einsetzen — "
"Debian 13 liefert 10.05.1."
)
else:
abhilfe = (
"Abhilfe — eines davon: "
"(1) ocrmypdf >= 17 einsetzen (requirements.txt pinnt 17.x; dort "
"wird Ghostscript ohne PDF/A-Ausgabe gar nicht angefasst) — nach "
"einem Downgrade also die venv neu bauen; "
"(2) [ocr].skip_text = false setzen (dann wird vorhandener Text neu "
"erkannt statt übersprungen, das kostet Laufzeit); "
"(3) eine Distribution mit neuerem Ghostscript einsetzen — "
"Debian 13 liefert 10.05.1."
)
return (
f"Ghostscript {gs_version} ist von einem bekannten Fehler betroffen "
"(10.0.0–10.02.0, der Debian-12-Standard) und wird von ocrmypdf "
f"abgelehnt: {ursache}. "
"Abhilfe — eines von beidem: "
"(1) Ghostscript >= 10.02.1 aus bookworm-backports installieren "
"(install.sh bietet das an): "
"echo 'deb http://deb.debian.org/debian bookworm-backports main' | "
"sudo tee /etc/apt/sources.list.d/bookworm-backports.list && "
"sudo apt update && sudo apt install -t bookworm-backports ghostscript — "
"oder (2) in der Config [ocr].skip_text = false setzen "
"(dann wird vorhandener Text neu erkannt statt übersprungen)"
+ (" bzw. [ocr].pdfa_level = \"\"." if pdfa_level else ".")
+ abhilfe
+ " Ein Ghostscript-Upgrade auf Debian 12 gibt es NICHT: "
"bookworm-backports enthält kein Ghostscript-Paket. Wer auf Debian 12 "
"PDF/A zusammen mit skip_text = true braucht, hat dort keinen Weg."
)
+30 -4
View File
@@ -137,13 +137,39 @@ def test_healthy_gs_passes_on_ocrmypdf_16() -> None:
# ---------------- Meldungstext ----------------
def test_error_message_names_both_remedies() -> None:
"""Der Admin muss aus der Meldung heraus handeln können."""
"""Der Admin muss aus der Meldung heraus handeln können.
Die Wege sind die **echten**: `pdfa_level = ""` (Default), `skip_text =
false` oder eine Distribution mit neuerem Ghostscript (Debian 13: 10.05.1).
Bis v0.7.0 stand hier „Ghostscript aus bookworm-backports" als Weg 1 — das
konnte nie funktionieren, denn bookworm-backports führt gar kein
Ghostscript-Paket (am echten Paketindex verifiziert: 2606 Pakete,
`ghostscript` nicht darunter). Der Rat darf nicht zurückkommen.
"""
# Fall A: ocrmypdf 16.x ohne PDF/A — Weg 1 ist hier, ocrmypdf anzuheben.
with _env("10.0.0", "16.13.0"):
with pytest.raises(PreflightError) as exc_info:
check_preflight(pdfa_level="", skip_text=True)
msg = str(exc_info.value)
assert "backports" in msg, "Weg 1: Ghostscript aus bookworm-backports"
assert "skip_text = false" in msg, "Weg 2: skip_text abschalten"
msg_no_pdfa = str(exc_info.value)
assert "ocrmypdf >= 17" in msg_no_pdfa, "Weg 1: ocrmypdf anheben"
assert "skip_text = false" in msg_no_pdfa, "Weg 2: skip_text abschalten"
assert "Debian 13" in msg_no_pdfa, "Weg 3: neuere Distribution"
# Fall B: PDF/A gewünscht — Weg 1 ist hier, PDF/A aufzugeben.
with _env("10.0.0", "17.4.1"):
with pytest.raises(PreflightError) as exc_info:
check_preflight(pdfa_level="2", skip_text=True)
msg_pdfa = str(exc_info.value)
assert 'pdfa_level = ""' in msg_pdfa, "Weg 1: PDF/A abschalten"
assert "skip_text = false" in msg_pdfa, "Weg 2: skip_text abschalten"
assert "Debian 13" in msg_pdfa, "Weg 3: neuere Distribution"
# Und in keiner der beiden Meldungen der widerlegte Backports-Rat.
for msg in (msg_no_pdfa, msg_pdfa):
assert "apt install -t bookworm-backports" not in msg
assert "sources.list.d/bookworm-backports.list" not in msg
# Erwähnt werden darf es — aber nur als ausdrückliche Absage.
assert "NICHT" in msg and "kein Ghostscript-Paket" in msg
def test_default_config_pdfa_level_is_empty() -> None:
+24 -6
View File
@@ -901,14 +901,24 @@ strip_root() {
# Das Archiv enthaelt Klartext-Passwoerter aus den Instanz-Configs und wird
# deshalb root-only (0600) abgelegt.
create_backup() {
local stage tmp d
local stage tmp tar_tmp d freeze_rel
local -a items=()
log_step "Backup erstellen"
mkdir -p "$BACKUP_DIR" || die "Backup-Verzeichnis $BACKUP_DIR nicht anlegbar."
chmod 700 "$BACKUP_DIR" 2>/dev/null || true
# Die pip-freeze-Liste liegt im Archiv UNTER dem Installationsverzeichnis.
# Der dokumentierte Rollback (tar -xzf ... -C /) legt sie damit nach
# $INSTALL_DIR/pip-freeze.txt statt als /pip-freeze.txt in die Wurzel.
# Dort ueberlebt sie auch den naechsten Update-Lauf: der raeumt nur
# $INSTALL_DIR/pdf_ocr_hotfolder und $INSTALL_DIR/lib weg.
freeze_rel="$(strip_root "$INSTALL_DIR")/pip-freeze.txt"
stage="$(mktemp -d)" || die "mktemp -d fehlgeschlagen."
if ! mkdir -p "$stage/$(dirname "$freeze_rel")"; then
rm -rf "$stage"
die "Backup-Zwischenverzeichnis nicht anlegbar."
fi
{
echo "# pip freeze der alten venv vor dem Update"
echo "# Zeitpunkt: $(date -Is)"
@@ -918,7 +928,7 @@ create_backup() {
else
echo "# keine funktionierende venv vorhanden"
fi
} > "$stage/pip-freeze.txt"
} > "$stage/$freeze_rel"
[ -d "$INSTALL_DIR" ] && items+=("$(strip_root "$INSTALL_DIR")")
[ -d "$CONFIG_DIR" ] && items+=("$(strip_root "$CONFIG_DIR")")
@@ -935,17 +945,24 @@ create_backup() {
fi
tmp="$BACKUP_DIR/backup-$(date +%Y%m%d-%H%M%S).tar.gz"
tar_tmp="${tmp%.gz}"
local old_umask; old_umask="$(umask)"
umask 077
if ! tar -czf "$tmp" \
# Zwei tar-Aufrufe statt einem: --exclude wirkt global, wuerde also auch
# die frische Liste aus $stage verschlucken. So faellt eine von einem
# frueheren Rollback liegengebliebene pip-freeze.txt im ersten Aufruf raus,
# und der zweite haengt die frische an — genau ein Eintrag im Archiv.
if ! tar -cf "$tar_tmp" \
--exclude="$(strip_root "$INSTALL_DIR")/venv" \
--exclude="$(strip_root "$INSTALL_DIR")/venv.old-*" \
--exclude="$freeze_rel" \
--exclude='*/__pycache__' \
--exclude='*.pyc' \
-C "$TAR_ROOT" "${items[@]}" \
-C "$stage" pip-freeze.txt; then
|| ! tar -rf "$tar_tmp" -C "$stage" "$freeze_rel" \
|| ! gzip -n "$tar_tmp"; then
umask "$old_umask"
rm -f "$tmp"
rm -f "$tar_tmp" "$tmp"
rm -rf "$stage"
die "Backup fehlgeschlagen (volle Platte?). Es wurde NICHTS veraendert."
fi
@@ -956,7 +973,8 @@ create_backup() {
BACKUP_FILE="$tmp"
log_info "Backup: $tmp ($(du -h "$tmp" 2>/dev/null | awk '{print $1}'))"
log_info " Enthalten: Code, $CONFIG_DIR, Template-Unit, Drop-ins, pip-freeze.txt"
log_info " Enthalten: Code, $CONFIG_DIR, Template-Unit, Drop-ins"
log_info " Dazu die pip-Liste der alten venv: $INSTALL_DIR/pip-freeze.txt"
log_info " NICHT enthalten: venv und die Datenverzeichnisse ($DATA_ROOT)"
log_warn " Achtung: die Instanz-Configs enthalten Klartext-Passwoerter"
log_warn " (SMTP/Nextcloud/SFTP) — Archiv ist deshalb root-only (0600)."