feat: stille Datenverlust-Pfade geschlossen, gemeinsame Shell-Lib (v0.7.0)

Vor dem Rollout durchgesehen und die verbliebenen Stellen geschlossen, an
denen etwas schiefgehen konnte, ohne dass es irgendwo sichtbar wurde.

Datenverlust:
- veraPDF: das in [verapdf].binary konfigurierte Programm wird im Preflight
  geprueft. Bisher galt bei falschem Pfad JEDE Datei als "nicht konform" —
  Ergebnis nach error/, Original geloescht (Default delete). run_verapdf()
  trennt jetzt ausserdem ein echtes FAIL-Urteil von einer Stoerung
  (VeraPdfUnavailable: nicht startbar, abgestuerzt, kein PASS/FAIL in der
  Ausgabe). Bei Stoerung wandern Original UND Ergebnis nach error/, das
  Original wird nicht entsorgt.
- Gleichnamige Dateien wurden in outgoing/, error/ und beim Ordner-Upload
  mit abweichendem target kommentarlos ueberschrieben. Jetzt Zeitstempel
  daneben, mit Warnung; ProcessResult.output traegt den echten Pfad.

Robustheit:
- Kaputtes oder nicht lesbares TOML beim Start: Exit 2 statt Traceback.
- RestartPreventExitStatus=2 in der Unit — Exit 2 (Config/Preflight) laeuft
  nicht mehr endlos neu, die Instanz bleibt sichtbar failed stehen.
- Toter watchdog-Observer wird erkannt: Exit 3, systemd setzt den Watch neu
  auf. Vorher blieb die Unit "active" und verarbeitete nichts mehr.
- Relative Pfade in [paths]/archive_dir/target sind ein Config-Fehler statt
  still unter /opt zu landen.
- Fehler beim Archivieren entwertet den Durchlauf nicht mehr: Upload und
  Mail laufen, Sichtbarkeit ueber log.error + "OK mit Warnung"-Mail.
- Nicht-PDFs in incoming/ werden beim Start-Scan gesammelt gemeldet.
- Logging explizit nach stdout (die Doku versprach das schon).

Struktur:
- Neue lib/common.sh, von install.sh und update.sh gesourct. Die doppelte
  venv_is_healthy() gibt es nur noch einmal, in der gruendlichen Fassung —
  die schlanke in install.sh haette eine nach einem Distro-Sprung kaputte
  venv als gesund durchgewunken (nachgewiesen).
- install.sh warnt in Containern, wenn systemd-journald nicht laeuft.

Doku: Dateisystem-Festlegung (ext4/xfs/zfs, kein CIFS/NFS wegen inotify),
Debian 13 in LXC auf Proxmox scheitert an journald (243/CREDENTIALS,
AppArmor blockiert sd-mkdcreds) inkl. Abhilfe, echte Speicher-Messwerte,
Exit-Code-Tabelle.

254 Tests gruen (vorher 152).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-23 00:59:17 +02:00
parent 305454eeb5
commit cd803a3dfe
28 changed files with 2902 additions and 286 deletions
+68
View File
@@ -64,3 +64,71 @@ def test_upload_folder_reports_failure(tmp_path: Path) -> None:
assert upload_folder(src, FolderUpload(enabled=True,
target=str(tmp_path / "ziel")),
tmp_path / "out") is False
# ---------------- Punkt 3: kein stilles Überschreiben im Ziel ----------------
def test_upload_folder_does_not_overwrite_existing(tmp_path: Path) -> None:
"""Gleichnamige Datei im abweichenden target wurde bisher still ersetzt."""
src = tmp_path / "out" / "OCR_scan.pdf"
src.parent.mkdir()
src.write_bytes(b"%PDF-1.4 neu\n")
target = tmp_path / "ziel"
target.mkdir()
(target / "OCR_scan.pdf").write_bytes(b"%PDF-1.4 alt\n")
assert upload_folder(src, FolderUpload(enabled=True, target=str(target)),
tmp_path / "out") is True
assert (target / "OCR_scan.pdf").read_bytes() == b"%PDF-1.4 alt\n"
neu = list(target.glob("OCR_scan_*.pdf"))
assert len(neu) == 1
assert neu[0].read_bytes() == b"%PDF-1.4 neu\n"
def test_upload_folder_logs_warning_on_collision(tmp_path: Path, caplog) -> None:
import logging
src = tmp_path / "out" / "OCR_scan.pdf"
src.parent.mkdir()
src.write_bytes(b"%PDF-1.4 neu\n")
target = tmp_path / "ziel"
target.mkdir()
(target / "OCR_scan.pdf").write_bytes(b"%PDF-1.4 alt\n")
with caplog.at_level(logging.WARNING, logger="pdf_ocr_hotfolder.uploaders"):
upload_folder(src, FolderUpload(enabled=True, target=str(target)),
tmp_path / "out")
assert "OCR_scan.pdf" in caplog.text
assert "überschrieben" in caplog.text
def test_upload_folder_without_collision_logs_nothing(tmp_path: Path, caplog) -> None:
import logging
src = tmp_path / "out" / "OCR_scan.pdf"
src.parent.mkdir()
src.write_bytes(b"%PDF-1.4\n")
with caplog.at_level(logging.WARNING, logger="pdf_ocr_hotfolder.uploaders"):
upload_folder(src, FolderUpload(enabled=True, target=str(tmp_path / "ziel")),
tmp_path / "out")
assert "überschrieben" not in caplog.text
assert (tmp_path / "ziel" / "OCR_scan.pdf").exists()
def test_upload_folder_self_target_is_not_renamed(tmp_path: Path) -> None:
"""Default (leeres target -> outgoing/): der resolve()-Kurzschluss greift.
Ohne ihn würde die Datei hier gegen sich selbst kollidieren und eine
Zeitstempel-Kopie neben sich selbst erzeugen.
"""
out = tmp_path / "out"
out.mkdir()
src = out / "OCR_scan.pdf"
src.write_bytes(b"%PDF-1.4\n")
assert upload_folder(src, FolderUpload(enabled=True, target=""), out) is True
assert list(out.iterdir()) == [src]