2 Commits

Author SHA1 Message Date
techadmin 1b67c846a2 fix: Updater meldet den LXC-Drop-in nur noch, wenn sich wirklich etwas aendert (v0.7.2)
Nachlese aus der Verifikation von v0.7.1 auf einem frischen Debian 12.

- update.sh meldete "LXC-Drop-in nachgezogen ✓" auch bei unveraenderter
  Datei — dieselbe Klasse Unwahrheit wie das Ghostscript-Haekchen, das in
  v0.7.1 behoben wurde. Jetzt wird verglichen und ehrlich gemeldet.
- "Quelle wieder entfernt" ist Teil der schlechten Nachricht und kommt als
  WARN statt INFO.

254 Tests gruen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 02:14:27 +02:00
techadmin 465ff8873f 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>
2026-09-23 01:36:42 +02:00
15 changed files with 633 additions and 104 deletions
+12 -5
View File
@@ -1,7 +1,7 @@
# AI Agent Briefing — PDF OCR Hotfolder # AI Agent Briefing — PDF OCR Hotfolder
**Zuletzt aktualisiert:** 2026-09-23 **Zuletzt aktualisiert:** 2026-09-23
**Version:** 0.7.0 **Version:** 0.7.2
**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. **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: > **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). und fasst nachinstallierte Sprachpakete nicht an (kein purge/autoremove).
Fehlschläge setzen nur `APT_WARN`, sie brechen nicht ab. Fehlschläge setzen nur `APT_WARN`, sie brechen nicht ab.
- **Backup** (`create_backup()`): Code, `/etc/pdf-ocr-hotfolder/`, Template-Unit, - **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 **ohne** Datenverzeichnisse. `umask 077` + `chmod 600 root:root`, weil die
Configs Klartext-Passwörter enthalten. Rotation: letzte `BACKUP_KEEP` = 5. Configs Klartext-Passwörter enthalten. Rotation: letzte `BACKUP_KEEP` = 5.
- **LXC-Drop-in** wird beim Update aus dem Repo nachgezogen, **falls es - **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 ≤ 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. - **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. - **`[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. - **`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). - **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)). - **`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. - **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. - **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. - **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). - **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. - **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). - **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 ## 🛠️ Entwicklung
@@ -448,7 +454,8 @@ pytest # aktuell 254 Tests
- **Repo:** https://gitea.sonith.de/sonith_ug/pdf-ocr-hotfolder - **Repo:** https://gitea.sonith.de/sonith_ug/pdf-ocr-hotfolder
- **Owner:** sonith_ug - **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) - **Versionierung:** Semver (PATCH bei jedem Build, MINOR bei Features, MAJOR manuell)
- **Tags:** `v{VERSION}`, automatischer Push nach Commit - **Tags:** `v{VERSION}`, automatischer Push nach Commit
+60
View File
@@ -1,5 +1,65 @@
# Changelog # Changelog
## [0.7.2] - 2026-09-23
Nachlese aus der Verifikation von 0.7.1 auf einem frischen Debian-12-Container.
### Fixed
- `update.sh` meldete "LXC-Drop-in nachgezogen ✓" auch dann, wenn die Datei
bereits identisch war — dieselbe Sorte Unwahrheit wie das
Ghostscript-Haekchen in 0.7.0. Es wird jetzt verglichen und nur gemeldet,
was tatsaechlich passiert ist ("bereits aktuell" vs. "nachgezogen").
- Die Zeile "Quelle wieder entfernt" gehoert zur schlechten Nachricht und
kommt jetzt als WARN statt als INFO.
## [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 ## [0.7.0] - 2026-09-23
Schliesst die stillen Datenverlust-Pfade: gleichnamige Dateien werden nirgends 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 - 🧠 **Stabilitäts-Erkennung**: wartet bis Scanner fertig geschrieben hat
- 🔁 **Parallelverarbeitung** mehrerer PDFs (ThreadPool, konfigurierbar) - 🔁 **Parallelverarbeitung** mehrerer PDFs (ThreadPool, konfigurierbar)
- ♻️ **Wiederaufnahme aus `working/`** nach einem harten Stopp — keine Datei bleibt liegen - ♻️ **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 - 🛡️ **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 - 🚫 **Ü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 - ☁️ **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 einem CIFS/NFS-Mount liefert inotify keine Events und der Hotfolder bemerkt
neue Dateien nur noch beim Start neue Dateien nur noch beim Start
([warum](docs/INSTALLATION.md#dateisystem-ext4-xfs-oder-zfs)). ([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 ```bash
git clone gitea@gitea.sonith.de:sonith_ug/pdf-ocr-hotfolder.git 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 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 Alle Details zu den Abfragen, zum Multi-Instanz-Betrieb und zu den Fallstricken
(LXC, Ghostscript): **[docs/INSTALLATION.md](docs/INSTALLATION.md)**. (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)**. Nach einem Debian-Major-Upgrade: **[docs/OS-UPGRADE.md](docs/OS-UPGRADE.md)**.
## Verzeichnisse ## Verzeichnisse
@@ -180,5 +196,5 @@ MIT — © Sonith UG
--- ---
**Version:** 0.7.0 **Version:** 0.7.2
**Repo:** https://gitea.sonith.de/sonith_ug/pdf-ocr-hotfolder **Repo:** https://gitea.sonith.de/sonith_ug/pdf-ocr-hotfolder
+1 -1
View File
@@ -1 +1 @@
0.7.0 0.7.2
+6
View File
@@ -30,6 +30,12 @@ oversample = 300
# ocrmypdf lehnt damit die Kombination pdfa_level + skip_text=true komplett ab. # 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. # 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 # pdfa_level = "" ist deshalb der Default — aber KEIN genereller Schutz gegen
# den Ghostscript-Bug: das gilt erst zusammen mit ocrmypdf >= 17. Bis # 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 # 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 | | Python | 3.11+ (wegen `tomllib` aus der stdlib); kommt aus der Distribution |
| Arbeitsspeicher | **mindestens 2 GB** für den produktiven Betrieb — siehe [Systemanforderungen](#systemanforderungen) | | 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) | | 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 | | 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)) | | 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 Die System-Pakete installiert der Installer selbst. Die Liste steht als
einzige Quelle in `lib/common.sh` (Funktion `pdf_ocr_apt_packages()`, zwischen einzige Quelle in `lib/common.sh` (Funktion `pdf_ocr_apt_packages()`, zwischen
den Marken `# --- BEGIN apt-packages` / `# --- END apt-packages`); `install.sh` den Marken `# --- BEGIN apt-packages` / `# --- END apt-packages`); `install.sh`
@@ -174,14 +210,51 @@ System mehr RAM bekommt.
## Installation ## Installation
```bash ```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 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 — `install.sh` ist **Installer und Instanz-Manager in einem** und idempotent —
jeder weitere Aufruf überspringt, was schon steht. 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 ### Basis-Install vs. Instanz-Anlage
Der Installer unterscheidet zwei Ebenen: Der Installer unterscheidet zwei Ebenen:
@@ -424,21 +497,40 @@ ocrmypdf-Version:
### Abhilfe ### Abhilfe
**Weg 1 — Ghostscript anheben** (empfohlen). Der Installer erkennt betroffene > 🚫 **Es gibt auf Debian 12 kein neueres Ghostscript.** `bookworm-backports`
Versionen und bietet auf Debian 12 bookworm-backports an. Manuell: > 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 **Weg 1 — `pdfa_level = ""` lassen** (Default, empfohlen). Ohne PDF/A-Ausgabe
echo 'deb http://deb.debian.org/debian bookworm-backports main' | \ fasst ocrmypdf ≥ 17 Ghostscript gar nicht an; die betroffene Version ist dann
sudo tee /etc/apt/sources.list.d/bookworm-backports.list völlig unproblematisch. Der Preis ist PDF/A — das Ergebnis ist eine normale
sudo apt update && sudo apt install -t bookworm-backports ghostscript durchsuchbare PDF. Für die übliche Anwendung (Scan wird durchsuchbar) reicht
``` das.
Ab Ghostscript 10.02.1 ist alles in Ordnung; PDF/A kann dann eingeschaltet
werden.
**Weg 2 — `skip_text = false` setzen.** Dann wird vorhandener Text neu erkannt **Weg 2 — `skip_text = false` setzen.** Dann wird vorhandener Text neu erkannt
statt übersprungen, und die Bedingung greift nicht mehr. Das kostet Laufzeit bei statt übersprungen, und die Bedingung greift nicht mehr — PDF/A ist damit auch
PDFs, die bereits eine Textebene haben. 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 `status=243/CREDENTIALS`), gibt es schlicht kein journal, in das geschrieben
werden könnte. 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 — **Einordnung.** Der Dienst loggt seit v0.4.1 **ausschließlich** nach journald —
es gibt bewusst kein eigenes Logfile und kein Logverzeichnis. Ein kaputtes 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 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 sudo journalctl --flush
``` ```
Danach `systemctl status systemd-journald` (muss `active (running)` sein) und Danach `systemctl status systemd-journald` gegenprüfen — muss `active
`journalctl -u pdf-ocr-hotfolder@<instanz>` gegenprüfen. Für die anderen (running)` sein. Für die anderen betroffenen Units gilt dasselbe Muster mit
betroffenen Units gilt dasselbe Muster mit deren Unit-Namen. 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 > **Der saubere Weg liegt host-seitig.** Das Drop-in kuriert das Symptom im
> Container. Richtig behoben wird es auf dem Proxmox-Host: Update von > 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 ## Warum das ein eigener Ablauf ist
Die venv unter `/opt/pdf-ocr-hotfolder/venv/` hängt an der **Python-Version der 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 gs --version
``` ```
Auf Debian 13 ist der Ghostscript-Bug aus Debian 12 kein Thema mehr; ein Auf Debian 13 ist der Ghostscript-Bug aus Debian 12 kein Thema mehr — **Debian
bookworm-backports-Eintrag unter `/etc/apt/sources.list.d/` gehört nach dem 13 liefert Ghostscript 10.05.1**. Damit ist PDF/A (`[ocr].pdfa_level = "1"`,
Upgrade entfernt. Hintergrund: `"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). [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` ## 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 ./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 `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 es den gespeicherten Pfad aus `/opt/pdf-ocr-hotfolder/.repo_path` — **das Repo
muss also liegen bleiben**, das Tool kopiert daraus. 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/` | | `/etc/pdf-ocr-hotfolder/` (alle Instanz-Configs) | die **Datenverzeichnisse** `/var/lib/pdf-ocr-hotfolder/` |
| Template-Unit `pdf-ocr-hotfolder@.service` | `__pycache__`, `*.pyc` | | Template-Unit `pdf-ocr-hotfolder@.service` | `__pycache__`, `*.pyc` |
| alle Drop-in-Verzeichnisse `…@*.service.d` | | | 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 `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** **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 (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 systemctl stop 'pdf-ocr-hotfolder@*'
sudo tar -xzf /var/backups/pdf-ocr-hotfolder/backup-YYYYmmdd-HHMMSS.tar.gz -C / sudo tar -xzf /var/backups/pdf-ocr-hotfolder/backup-YYYYmmdd-HHMMSS.tar.gz -C /
sudo systemctl daemon-reload 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 Das Skript nennt diesen Befehl mit dem konkreten Archivnamen selbst — sowohl
beim Abbruch als auch bei einer erkannten Regression. 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 ### 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 - **Die venv ist nicht im Backup.** Wurde sie beim Update neu gebaut oder
hat `pip install --upgrade` Pakete angehoben, holt das Rollback den alten 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 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 …`). (`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 - **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 ü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. `rm -rf /opt/pdf-ocr-hotfolder/pdf_ocr_hotfolder` **vor** dem Entpacken.
- **Die Datenverzeichnisse sind nicht im Backup** — gewollt. Ein Rollback - **Die Datenverzeichnisse sind nicht im Backup** — gewollt. Ein Rollback
verändert keine PDFs, weder in `incoming/` noch in `error/`. 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 ✓" log_info "System-Pakete ok ✓"
# Ghostscript-Versions-Check (Issue #3 + Issue #6) # Ghostscript-Versions-Check (Issue #3 + Issue #6)
if command -v gs >/dev/null 2>&1; then # Die eigentliche Logik steckt in lib/common.sh (check_ghostscript): sie
GS_VER="$(gs --version 2>/dev/null || echo 0.0)" # prueft nur, was wirklich verfuegbar ist, meldet Erfolg ausschliesslich
log_info "Ghostscript: $GS_VER" # nach einem Versionsvergleich und laesst keine nutzlose apt-Quelle
case "$GS_VER" in # zurueck. Ein betroffenes Ghostscript ist kein Abbruchgrund.
10.0.0|10.00.0|10.01.*|10.02.0) check_ghostscript
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
# LXC/Container-Erkennung (Issue #4) # LXC/Container-Erkennung (Issue #4)
if systemd-detect-virt --container -q 2>/dev/null; then 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 "Pruefen: systemctl status systemd-journald"
log_warn "Scheitert es mit 243/CREDENTIALS (Debian 13 in LXC auf" log_warn "Scheitert es mit 243/CREDENTIALS (Debian 13 in LXC auf"
log_warn "Proxmox), hilft ein Drop-in im Container:" 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 " mkdir -p /etc/systemd/system/systemd-journald.service.d"
log_warn " printf '[Service]\\nImportCredential=\\n' > \\" log_warn " { echo '[Service]'; echo 'ImportCredential='; } > /etc/systemd/system/systemd-journald.service.d/no-credentials.conf"
log_warn " /etc/systemd/system/systemd-journald.service.d/no-credentials.conf"
log_warn " systemctl daemon-reload && systemctl restart systemd-journald" log_warn " systemctl daemon-reload && systemctl restart systemd-journald"
log_warn "Details: docs/INSTALLATION.md, Abschnitt Troubleshooting." log_warn "Details: docs/INSTALLATION.md, Abschnitt Troubleshooting."
read -r -p "Trotzdem fortfahren? [J/n]: " JOURNAL_GO 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' 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} $*"; } # Die Meldung selbst geht als %s durch, NICHT als %b (frueher: echo -e). Sonst
log_error() { echo -e "${RED}[ERROR]${NC} $*"; } # wird ein '\n' im Text — etwa in einem Befehl, den wir zum Kopieren anzeigen —
log_step() { echo -e "\n${BLUE}==>${NC} $*"; } # 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. # Bricht ab, wenn nicht root. $1 = der Aufruf, der gemeint ist.
require_root() { require_root() {
@@ -50,6 +57,12 @@ require_root() {
: "${SYSTEMD_DIR:=/etc/systemd/system}" : "${SYSTEMD_DIR:=/etc/systemd/system}"
: "${DEFAULT_USER:=pdfocr}" : "${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" SERVICE_TEMPLATE="pdf-ocr-hotfolder@.service"
LXC_DROPIN_DIR="$SYSTEMD_DIR/${SERVICE_TEMPLATE}.d" LXC_DROPIN_DIR="$SYSTEMD_DIR/${SERVICE_TEMPLATE}.d"
# shellcheck disable=SC2034 # wird von install.sh und update.sh genutzt # shellcheck disable=SC2034 # wird von install.sh und update.sh genutzt
@@ -88,6 +101,199 @@ PKGLIST
} }
# --- END apt-packages --- # --- 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_warn "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 # Python / venv
# ============================================================ # ============================================================
+1 -1
View File
@@ -1,3 +1,3 @@
"""PDF OCR Hotfolder — Scanner-PDFs automatisch durchsuchbar machen.""" """PDF OCR Hotfolder — Scanner-PDFs automatisch durchsuchbar machen."""
__version__ = "0.7.0" __version__ = "0.7.2"
+7 -1
View File
@@ -32,6 +32,9 @@ class OcrConfig:
# blockiert dann JEDE Datei — deshalb pinnt requirements.txt 17.x und der # blockiert dann JEDE Datei — deshalb pinnt requirements.txt 17.x und der
# Preflight prüft die installierte ocrmypdf-Version mit (siehe # Preflight prüft die installierte ocrmypdf-Version mit (siehe
# service._gs_block_reason). # 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 = "" pdfa_level: str = ""
deskew: bool = True deskew: bool = True
clean: bool = False clean: bool = False
@@ -292,7 +295,10 @@ def legacy_warnings(cfg: Config) -> list[str]:
"Bug, wegen dem ocrmypdf die Kombination mit skip_text ablehnt " "Bug, wegen dem ocrmypdf die Kombination mit skip_text ablehnt "
"(Issue #3). Der Preflight bricht ab, falls die installierte " "(Issue #3). Der Preflight bricht ab, falls die installierte "
"Ghostscript-Version betroffen ist; ab 10.02.1 ist alles in " "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 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" "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 ( return (
f"Ghostscript {gs_version} ist von einem bekannten Fehler betroffen " f"Ghostscript {gs_version} ist von einem bekannten Fehler betroffen "
"(10.0.0–10.02.0, der Debian-12-Standard) und wird von ocrmypdf " "(10.0.0–10.02.0, der Debian-12-Standard) und wird von ocrmypdf "
f"abgelehnt: {ursache}. " f"abgelehnt: {ursache}. "
"Abhilfe — eines von beidem: " + abhilfe
"(1) Ghostscript >= 10.02.1 aus bookworm-backports installieren " + " Ein Ghostscript-Upgrade auf Debian 12 gibt es NICHT: "
"(install.sh bietet das an): " "bookworm-backports enthält kein Ghostscript-Paket. Wer auf Debian 12 "
"echo 'deb http://deb.debian.org/debian bookworm-backports main' | " "PDF/A zusammen mit skip_text = true braucht, hat dort keinen Weg."
"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 ".")
) )
+30 -4
View File
@@ -137,13 +137,39 @@ def test_healthy_gs_passes_on_ocrmypdf_16() -> None:
# ---------------- Meldungstext ---------------- # ---------------- Meldungstext ----------------
def test_error_message_names_both_remedies() -> None: 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 _env("10.0.0", "16.13.0"):
with pytest.raises(PreflightError) as exc_info: with pytest.raises(PreflightError) as exc_info:
check_preflight(pdfa_level="", skip_text=True) check_preflight(pdfa_level="", skip_text=True)
msg = str(exc_info.value) msg_no_pdfa = str(exc_info.value)
assert "backports" in msg, "Weg 1: Ghostscript aus bookworm-backports" assert "ocrmypdf >= 17" in msg_no_pdfa, "Weg 1: ocrmypdf anheben"
assert "skip_text = false" in msg, "Weg 2: skip_text abschalten" 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: def test_default_config_pdfa_level_is_empty() -> None:
+34 -9
View File
@@ -901,14 +901,24 @@ strip_root() {
# Das Archiv enthaelt Klartext-Passwoerter aus den Instanz-Configs und wird # Das Archiv enthaelt Klartext-Passwoerter aus den Instanz-Configs und wird
# deshalb root-only (0600) abgelegt. # deshalb root-only (0600) abgelegt.
create_backup() { create_backup() {
local stage tmp d local stage tmp tar_tmp d freeze_rel
local -a items=() local -a items=()
log_step "Backup erstellen" log_step "Backup erstellen"
mkdir -p "$BACKUP_DIR" || die "Backup-Verzeichnis $BACKUP_DIR nicht anlegbar." mkdir -p "$BACKUP_DIR" || die "Backup-Verzeichnis $BACKUP_DIR nicht anlegbar."
chmod 700 "$BACKUP_DIR" 2>/dev/null || true 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." 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 "# pip freeze der alten venv vor dem Update"
echo "# Zeitpunkt: $(date -Is)" echo "# Zeitpunkt: $(date -Is)"
@@ -918,7 +928,7 @@ create_backup() {
else else
echo "# keine funktionierende venv vorhanden" echo "# keine funktionierende venv vorhanden"
fi fi
} > "$stage/pip-freeze.txt" } > "$stage/$freeze_rel"
[ -d "$INSTALL_DIR" ] && items+=("$(strip_root "$INSTALL_DIR")") [ -d "$INSTALL_DIR" ] && items+=("$(strip_root "$INSTALL_DIR")")
[ -d "$CONFIG_DIR" ] && items+=("$(strip_root "$CONFIG_DIR")") [ -d "$CONFIG_DIR" ] && items+=("$(strip_root "$CONFIG_DIR")")
@@ -935,17 +945,24 @@ create_backup() {
fi fi
tmp="$BACKUP_DIR/backup-$(date +%Y%m%d-%H%M%S).tar.gz" tmp="$BACKUP_DIR/backup-$(date +%Y%m%d-%H%M%S).tar.gz"
tar_tmp="${tmp%.gz}"
local old_umask; old_umask="$(umask)" local old_umask; old_umask="$(umask)"
umask 077 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" \
--exclude="$(strip_root "$INSTALL_DIR")/venv.old-*" \ --exclude="$(strip_root "$INSTALL_DIR")/venv.old-*" \
--exclude="$freeze_rel" \
--exclude='*/__pycache__' \ --exclude='*/__pycache__' \
--exclude='*.pyc' \ --exclude='*.pyc' \
-C "$TAR_ROOT" "${items[@]}" \ -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" umask "$old_umask"
rm -f "$tmp" rm -f "$tar_tmp" "$tmp"
rm -rf "$stage" rm -rf "$stage"
die "Backup fehlgeschlagen (volle Platte?). Es wurde NICHTS veraendert." die "Backup fehlgeschlagen (volle Platte?). Es wurde NICHTS veraendert."
fi fi
@@ -956,7 +973,8 @@ create_backup() {
BACKUP_FILE="$tmp" BACKUP_FILE="$tmp"
log_info "Backup: $tmp ($(du -h "$tmp" 2>/dev/null | awk '{print $1}'))" 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_info " NICHT enthalten: venv und die Datenverzeichnisse ($DATA_ROOT)"
log_warn " Achtung: die Instanz-Configs enthalten Klartext-Passwoerter" log_warn " Achtung: die Instanz-Configs enthalten Klartext-Passwoerter"
log_warn " (SMTP/Nextcloud/SFTP) — Archiv ist deshalb root-only (0600)." log_warn " (SMTP/Nextcloud/SFTP) — Archiv ist deshalb root-only (0600)."
@@ -992,9 +1010,16 @@ install_units() {
# Container-Instanzen (Error 226/NAMESPACE). # Container-Instanzen (Error 226/NAMESPACE).
if [ -f "$LXC_DROPIN" ]; then if [ -f "$LXC_DROPIN" ]; then
if [ -f "$REPO_DIR/systemd/lxc-compat.conf" ]; then if [ -f "$REPO_DIR/systemd/lxc-compat.conf" ]; then
cp "$REPO_DIR/systemd/lxc-compat.conf" "$LXC_DROPIN" # Nur melden, was wirklich passiert ist: war die Datei schon
LXC_SYNCED=1 # identisch, ist "nachgezogen" eine Aussage ueber eine Aenderung,
log_info "LXC-Drop-in nachgezogen: $LXC_DROPIN ✓" # die es nicht gab.
if cmp -s "$REPO_DIR/systemd/lxc-compat.conf" "$LXC_DROPIN"; then
log_info "LXC-Drop-in bereits aktuell: $LXC_DROPIN"
else
cp "$REPO_DIR/systemd/lxc-compat.conf" "$LXC_DROPIN"
LXC_SYNCED=1
log_info "LXC-Drop-in nachgezogen: $LXC_DROPIN ✓"
fi
else else
log_warn "systemd/lxc-compat.conf fehlt im Repo — Drop-in bleibt alt." log_warn "systemd/lxc-compat.conf fehlt im Repo — Drop-in bleibt alt."
fi fi