diff --git a/AI_AGENT_BRIEFING.md b/AI_AGENT_BRIEFING.md index ff0103e..a0a7e18 100644 --- a/AI_AGENT_BRIEFING.md +++ b/AI_AGENT_BRIEFING.md @@ -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/.toml`. Deshalb `chmod 640` und `chown root:`, 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7902f0b..903923f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index ddfedab..39ee8f0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/VERSION b/VERSION index faef31a..39e898a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.7.0 +0.7.1 diff --git a/config.example.toml b/config.example.toml index 3db60f2..a806ac4 100644 --- a/config.example.toml +++ b/config.example.toml @@ -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 diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index 204f54c..785f349 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -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@.service`. Gegenprobe: eine PDF nach +`incoming/` kopieren und `journalctl -u pdf-ocr-hotfolder@ -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@` 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@` 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@ -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 diff --git a/docs/OS-UPGRADE.md b/docs/OS-UPGRADE.md index 0b5f214..2fdbae0 100644 --- a/docs/OS-UPGRADE.md +++ b/docs/OS-UPGRADE.md @@ -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` diff --git a/docs/UPDATE.md b/docs/UPDATE.md index 77faf86..f30ad11 100644 --- a/docs/UPDATE.md +++ b/docs/UPDATE.md @@ -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@' +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:`-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/`. diff --git a/install.sh b/install.sh index 00b4c24..4fa852b 100755 --- a/install.sh +++ b/install.sh @@ -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 diff --git a/lib/common.sh b/lib/common.sh index aefdfec..e5cc841 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -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 # ============================================================ diff --git a/pdf_ocr_hotfolder/__init__.py b/pdf_ocr_hotfolder/__init__.py index 2be1f0c..79ebe18 100644 --- a/pdf_ocr_hotfolder/__init__.py +++ b/pdf_ocr_hotfolder/__init__.py @@ -1,3 +1,3 @@ """PDF OCR Hotfolder — Scanner-PDFs automatisch durchsuchbar machen.""" -__version__ = "0.7.0" +__version__ = "0.7.1" diff --git a/pdf_ocr_hotfolder/config.py b/pdf_ocr_hotfolder/config.py index 76da43b..38d0fd5 100644 --- a/pdf_ocr_hotfolder/config.py +++ b/pdf_ocr_hotfolder/config.py @@ -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 diff --git a/pdf_ocr_hotfolder/service.py b/pdf_ocr_hotfolder/service.py index 92f958e..547f9b7 100644 --- a/pdf_ocr_hotfolder/service.py +++ b/pdf_ocr_hotfolder/service.py @@ -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." ) diff --git a/tests/test_ghostscript_version.py b/tests/test_ghostscript_version.py index 9031da7..e5ee16d 100644 --- a/tests/test_ghostscript_version.py +++ b/tests/test_ghostscript_version.py @@ -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: diff --git a/update.sh b/update.sh index e04ead1..37a1fd2 100755 --- a/update.sh +++ b/update.sh @@ -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)."