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:
@@ -105,6 +105,18 @@ class Config:
|
||||
sftp: SftpUpload
|
||||
email: EmailNotify
|
||||
log_level: str = "INFO"
|
||||
# Einträge der TOML, die zu keiner Dataclass gehören (Tippfehler oder
|
||||
# Optionen aus älteren Versionen). load_config() sammelt sie hier ein,
|
||||
# statt sie stumm zu verwerfen — geloggt wird erst weiter oben, damit
|
||||
# load_config() ohne konfiguriertes Logging benutzbar bleibt.
|
||||
unknown_keys: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
# Bekannte Sektionen — alles andere landet in Config.unknown_keys
|
||||
_KNOWN_SECTIONS = ("paths", "ocr", "output", "verapdf", "upload", "notify", "logging")
|
||||
_KNOWN_UPLOAD_TARGETS = ("folder", "nextcloud", "sftp")
|
||||
_KNOWN_NOTIFY_TARGETS = ("email",)
|
||||
_KNOWN_LOGGING_KEYS = ("level",)
|
||||
|
||||
|
||||
def _section(data: dict[str, Any], *keys: str) -> dict[str, Any]:
|
||||
@@ -130,6 +142,42 @@ def _require_path(p: dict[str, Any], key: str, cfg_path: Path) -> Path:
|
||||
return Path(str(value))
|
||||
|
||||
|
||||
def _unknown_in(data: dict[str, Any], keys: tuple[str, ...],
|
||||
known: tuple[str, ...]) -> list[str]:
|
||||
"""Listet alle Keys einer Sektion auf, die nicht in `known` stehen."""
|
||||
label = ".".join(keys)
|
||||
return [f"[{label}].{k}" for k in _section(data, *keys) if k not in known]
|
||||
|
||||
|
||||
def _collect_unknown_keys(data: dict[str, Any]) -> list[str]:
|
||||
"""Sammelt alle TOML-Einträge, die nirgends ausgewertet werden.
|
||||
|
||||
Dazu zählen Tippfehler (`[ocr].langauges`), Optionen aus älteren
|
||||
Versionen und komplett unbekannte Sektionen. Die Reihenfolge entspricht
|
||||
der Datei, damit die Meldung reproduzierbar bleibt.
|
||||
"""
|
||||
unknown: list[str] = []
|
||||
for name, value in data.items():
|
||||
if name not in _KNOWN_SECTIONS:
|
||||
unknown.append(f"[{name}]" if isinstance(value, dict) else name)
|
||||
unknown += _unknown_in(data, ("paths",), tuple(Paths.__annotations__))
|
||||
unknown += _unknown_in(data, ("ocr",), tuple(OcrConfig.__annotations__))
|
||||
unknown += _unknown_in(data, ("output",), tuple(OutputConfig.__annotations__))
|
||||
unknown += _unknown_in(data, ("verapdf",), tuple(VeraPdfConfig.__annotations__))
|
||||
for sub in _section(data, "upload"):
|
||||
if sub not in _KNOWN_UPLOAD_TARGETS:
|
||||
unknown.append(f"[upload.{sub}]")
|
||||
unknown += _unknown_in(data, ("upload", "folder"), tuple(FolderUpload.__annotations__))
|
||||
unknown += _unknown_in(data, ("upload", "nextcloud"), tuple(NextcloudUpload.__annotations__))
|
||||
unknown += _unknown_in(data, ("upload", "sftp"), tuple(SftpUpload.__annotations__))
|
||||
for sub in _section(data, "notify"):
|
||||
if sub not in _KNOWN_NOTIFY_TARGETS:
|
||||
unknown.append(f"[notify.{sub}]")
|
||||
unknown += _unknown_in(data, ("notify", "email"), tuple(EmailNotify.__annotations__))
|
||||
unknown += _unknown_in(data, ("logging",), _KNOWN_LOGGING_KEYS)
|
||||
return unknown
|
||||
|
||||
|
||||
def load_config(path: str | Path) -> Config:
|
||||
path = Path(path)
|
||||
with path.open("rb") as f:
|
||||
@@ -171,4 +219,54 @@ def load_config(path: str | Path) -> Config:
|
||||
paths=paths, ocr=ocr, output=output, verapdf=verapdf,
|
||||
folder=folder, nextcloud=nextcloud, sftp=sftp, email=email,
|
||||
log_level=log_level,
|
||||
unknown_keys=_collect_unknown_keys(data),
|
||||
)
|
||||
|
||||
|
||||
# ---- Legacy- und Plausibilitätswarnungen ----
|
||||
|
||||
# [ocr].timeout war vor 0.4.0 ein (wirkungsloses) Gesamt-Timeout mit Default
|
||||
# 1800. Ab diesem Wert gehen wir von einem Altwert aus.
|
||||
LEGACY_TIMEOUT_THRESHOLD = 900
|
||||
# Richtwert für das Seiten-Timeout seit 0.4.0
|
||||
RECOMMENDED_PAGE_TIMEOUT = 300
|
||||
|
||||
|
||||
def legacy_warnings(cfg: Config) -> list[str]:
|
||||
"""Warnt vor Einträgen, deren Bedeutung sich geändert hat.
|
||||
|
||||
Die Meldungen werden sowohl beim Dienststart ins Log geschrieben als auch
|
||||
von `--check-config` ausgegeben — deshalb steht der Text nur hier.
|
||||
"""
|
||||
out: list[str] = []
|
||||
if cfg.ocr.timeout >= LEGACY_TIMEOUT_THRESHOLD:
|
||||
out.append(
|
||||
f"[ocr].timeout = {cfg.ocr.timeout}: Seit Version 0.4.0 sind das "
|
||||
"Sekunden PRO SEITE (vorher ein wirkungsloses Gesamt-Timeout mit "
|
||||
f"Default 1800). Ein Wert >= {LEGACY_TIMEOUT_THRESHOLD} stammt fast "
|
||||
"sicher aus einer alten Config und lässt eine einzelne Seite "
|
||||
f"unnötig lange laufen. Richtwert: {RECOMMENDED_PAGE_TIMEOUT}."
|
||||
)
|
||||
if cfg.ocr.pdfa_level:
|
||||
out.append(
|
||||
f"[ocr].pdfa_level = {cfg.ocr.pdfa_level!r}: PDF/A-Ausgabe ist "
|
||||
"aktiv. Ghostscript 10.0.0-10.02.0 (Debian-12-Default) hat einen "
|
||||
"Bug, der zusammen mit skip_text das OCR blockiert (Issue #3). Der "
|
||||
"Preflight bricht ab, falls die installierte Ghostscript-Version "
|
||||
"betroffen ist; ab 10.02.1 ist alles in Ordnung."
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def unknown_key_warnings(cfg: Config) -> list[str]:
|
||||
"""Macht die beim Laden verworfenen Einträge sichtbar."""
|
||||
return [
|
||||
f"Unbekannter Config-Eintrag {key} — wird ignoriert (Tippfehler oder "
|
||||
"Option aus einer älteren Version?)"
|
||||
for key in cfg.unknown_keys
|
||||
]
|
||||
|
||||
|
||||
def config_warnings(cfg: Config) -> list[str]:
|
||||
"""Alle Warnungen zu einer geladenen Config (Legacy + unbekannte Keys)."""
|
||||
return legacy_warnings(cfg) + unknown_key_warnings(cfg)
|
||||
|
||||
Reference in New Issue
Block a user