feat: Wiederaufnahme aus working/, --check-config, feldtauglicher Updater (v0.6.0)
Datenverlust behoben: - Nach hartem Stopp blieb das Original in working/ liegen und wurde nie wieder angefasst (_scan_existing sah nur incoming/). Es wird jetzt beim Start an Ort und Stelle wieder aufgegriffen, mit Kollisionsschutz gegen gleichnamige neue Scans; angefangene __ocr_-Fragmente werden geloescht. - TimeoutStopSec 30 -> 300, damit laufendes OCR zu Ende laufen darf. Config-Drift sichtbar gemacht: - Neues --check-config (Exit 0 sauber / 1 Warnungen / 2 Fehler), das update.sh vor dem Neustart ueber alle Instanz-Configs laufen laesst. - Warnungen fuer [ocr].timeout >= 900 (seit 0.4.0 pro SEITE) und gesetztes pdfa_level, beim Dienststart wie im Check. - Unbekannte Config-Keys werden nicht mehr still verworfen, sondern genannt. Updater feldtauglich: - venv-Health-Check erkennt toten Symlink UND Versions-Drift gegen das System-Python; --rebuild-venv als ausdruecklicher Weg nach einem Debian- Major-Upgrade. Neubau ist ganz-oder-gar-nicht mit Rollback. - apt-Pakete werden auch beim Update synchronisiert (Quelle: install.sh). - Instanz-Erfassung inkl. activating/failed, Verifikation prueft is-failed und NRestarts statt sleep 1 + is-active. - Backup enthaelt Configs, Unit, Drop-ins und pip-freeze.txt, liegt auf 0600 und rotiert auf 5; schlaegt es fehl, bricht das Update vorher ab. - ERR-Trap faehrt die vorher laufenden Instanzen wieder hoch. - lxc-compat.conf wird beim Update nachgezogen. - requirements.txt gepinnt (ocrmypdf 16.13.0, geprueft fuer Python 3.11+3.13). Doku in Installation / Update / OS-Upgrade aufgeteilt (docs/). 135 Tests gruen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,11 +4,24 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
from . import __version__
|
||||
from .config import ConfigError, load_config
|
||||
from .service import HotfolderService, PreflightError
|
||||
from .config import Config, ConfigError, config_warnings, load_config
|
||||
from .service import (
|
||||
HotfolderService,
|
||||
PreflightError,
|
||||
check_output_config,
|
||||
check_preflight,
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Exit-Codes von --check-config (werden vom Updater ausgewertet)
|
||||
CHECK_OK = 0
|
||||
CHECK_WARN = 1
|
||||
CHECK_ERROR = 2
|
||||
|
||||
|
||||
def _setup_logging(level: str) -> None:
|
||||
@@ -19,6 +32,81 @@ def _setup_logging(level: str) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _log_config_warnings(cfg: Config) -> None:
|
||||
"""Schreibt Legacy- und Unbekannt-Warnungen beim Dienststart ins Log."""
|
||||
for warning in config_warnings(cfg):
|
||||
log.warning("Config-Warnung: %s", warning)
|
||||
|
||||
|
||||
def check_config(cfg_path: Path) -> int:
|
||||
"""Lädt und prüft die Config, ohne irgendetwas zu verarbeiten.
|
||||
|
||||
Returns:
|
||||
0 = alles sauber, 1 = nur Warnungen, 2 = Fehler (Config unbrauchbar
|
||||
oder Preflight scheitert).
|
||||
"""
|
||||
print(f"Prüfe Konfiguration: {cfg_path}")
|
||||
|
||||
try:
|
||||
cfg = load_config(cfg_path)
|
||||
except ConfigError as e:
|
||||
print(f"FEHLER: {e}", file=sys.stderr)
|
||||
return CHECK_ERROR
|
||||
except tomllib.TOMLDecodeError as e:
|
||||
print(f"FEHLER: {cfg_path} ist kein gültiges TOML: {e}", file=sys.stderr)
|
||||
return CHECK_ERROR
|
||||
except OSError as e:
|
||||
print(f"FEHLER: {cfg_path} nicht lesbar: {e}", file=sys.stderr)
|
||||
return CHECK_ERROR
|
||||
|
||||
print(" Config gelesen.")
|
||||
for label, path in (("incoming", cfg.paths.incoming),
|
||||
("outgoing", cfg.paths.outgoing),
|
||||
("working ", cfg.paths.working),
|
||||
("error ", cfg.paths.error)):
|
||||
hint = "" if path.is_dir() else " (existiert noch nicht, "\
|
||||
"wird beim Start angelegt)"
|
||||
print(f" {label} = {path}{hint}")
|
||||
print(f" OCR-Sprachen = {cfg.ocr.languages}")
|
||||
print(f" Seiten-Timeout= {cfg.ocr.timeout} s")
|
||||
print(f" PDF/A-Level = {cfg.ocr.pdfa_level or '(aus)'}")
|
||||
|
||||
errors: list[str] = []
|
||||
try:
|
||||
check_preflight(cfg.ocr.pdfa_level)
|
||||
print(" Preflight ok (tesseract, gs vorhanden).")
|
||||
except PreflightError as e:
|
||||
errors.append(str(e))
|
||||
try:
|
||||
check_output_config(cfg.output.original_on_success,
|
||||
cfg.output.archive_dir,
|
||||
cfg.output.name_mode)
|
||||
print(" [output]-Sektion ok.")
|
||||
except PreflightError as e:
|
||||
errors.append(str(e))
|
||||
|
||||
warnings = config_warnings(cfg)
|
||||
if warnings:
|
||||
print(f"\n{len(warnings)} Warnung(en):")
|
||||
for w in warnings:
|
||||
print(f" WARNUNG: {w}")
|
||||
|
||||
if errors:
|
||||
print(f"\n{len(errors)} Fehler:", file=sys.stderr)
|
||||
for e in errors:
|
||||
print(f" FEHLER: {e}", file=sys.stderr)
|
||||
print("\nErgebnis: Config unbrauchbar — der Dienst würde nicht starten.",
|
||||
file=sys.stderr)
|
||||
return CHECK_ERROR
|
||||
|
||||
if warnings:
|
||||
print("\nErgebnis: Config nutzbar, aber mit Warnungen.")
|
||||
return CHECK_WARN
|
||||
|
||||
print("\nErgebnis: Config sauber.")
|
||||
return CHECK_OK
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="pdf-ocr-hotfolder",
|
||||
@@ -29,6 +117,9 @@ def main() -> int:
|
||||
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
||||
parser.add_argument("--once", action="store_true",
|
||||
help="Nur bestehende Dateien verarbeiten und beenden")
|
||||
parser.add_argument("--check-config", action="store_true", dest="check_config",
|
||||
help="Config nur prüfen, nichts verarbeiten "
|
||||
"(Exit 0 = sauber, 1 = Warnungen, 2 = Fehler)")
|
||||
args = parser.parse_args()
|
||||
|
||||
cfg_path = Path(args.config)
|
||||
@@ -36,12 +127,17 @@ def main() -> int:
|
||||
print(f"Config nicht gefunden: {cfg_path}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
if args.check_config:
|
||||
# Hat Vorrang vor --once: es wird nichts verarbeitet.
|
||||
return check_config(cfg_path)
|
||||
|
||||
try:
|
||||
cfg = load_config(cfg_path)
|
||||
except ConfigError as e:
|
||||
print(f"FEHLER: {e}", file=sys.stderr)
|
||||
return 2
|
||||
_setup_logging(cfg.log_level)
|
||||
_log_config_warnings(cfg)
|
||||
|
||||
service = HotfolderService(cfg)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user