fix(worker): erster Film in einer neuen Ablage war immer unerreichbar
Ampel / ampel (push) Failing after 30s

Im ersten vollstaendigen Durchlauf aufgefallen: Der Rip lief sauber durch (40 GB
Akira, Marke rip_fertig sprang korrekt auf true), die Kompression brach 0,2 s
spaeter ab mit "Dieser Worker erreicht die Ziel (fertige Datei) nicht:
\\192.168.178.62\rippy\movies\Akira (1988)".

Die Freigabe war erreichbar. Es fehlten zwei noch nie angelegte Ordner - movies
und der Filmordner darin. Die Pruefung liess aber nur EINE fehlende Ebene durch
(sie sah nach dirname), obwohl gleich darauf os.makedirs die ganze Kette anlegt.
Damit war jeder ERSTE Film in einer neuen Ablage systematisch unerreichbar.
Jetzt zaehlt, ob irgendein Vorfahre existiert (erster_vorhandener_ordner,
begrenzt auf 12 Stufen, damit auf einer toten Freigabe nicht endlos geklopft
wird).

Zweiter Fehler in derselben Meldung: Sie behauptete "RIPPY_PATH_MAP ist gesetzt,
deckt diesen Pfad aber nicht ab" - und nannte im selben Satz den korrekt
uebersetzten UNC-Pfad. Wer dem folgte, suchte in der Karte statt in der
Freigabe. Die Karte wird nur noch beschuldigt, wenn sie den Pfad wirklich nicht
angefasst hat; sonst steht da, was zutrifft: uebersetzt, aber gerade nicht
erreichbar. Dazu der Artikel-Fehler "erreicht die Ziel" behoben.

Diese Pruefung hatte KEINEN einzigen Test - deshalb kam beides durch. Jetzt 12
(301 gruen). Beim Schreiben ist noch ein dritter Fehler aufgefallen:
`isdir=os.path.isdir` als Vorgabewert bindet die Funktion beim IMPORT, ein
Ersetzen geht danach ins Leere. Aufloesung jetzt beim Aufruf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-26 17:43:51 +02:00
parent 936b0837bf
commit 96c400f2f2
2 changed files with 207 additions and 12 deletions
+64 -12
View File
@@ -735,6 +735,46 @@ def _ist_uebersetzt(container_pfad: str, lokal_pfad: str) -> bool:
return container_pfad != lokal_pfad
# Wie weit wird nach einem vorhandenen Elternordner gesucht. Begrenzt, damit auf
# einer toten Freigabe nicht endlos geklopft wird.
MAX_ELTERN_STUFEN = 12
def erster_vorhandener_ordner(pfad: str, isdir=None, dirname=None) -> str:
"""Der nächste EXISTIERENDE Ordner oberhalb von `pfad` — oder "".
⚠️ Vorfall 26.07.2026, im vollen Durchlauf aufgefallen: Der Rip lief sauber
durch (40 GB), die Kompression brach sofort ab mit „Dieser Worker erreicht
das Ziel nicht: \\\\NAS\\rippy\\movies\\Akira (1988)". Erreichbar war die
Freigabe sehr wohl — es fehlten schlicht ZWEI Ordner: `movies` und der
Filmordner darin. Die Prüfung ließ nur EINE fehlende Ebene durchgehen (sie
sah nach `dirname`), obwohl gleich darauf `os.makedirs` die ganze Kette
anlegt. Ein Ziel, das noch nie beschrieben wurde, war damit systematisch
unerreichbar — also jeder erste Film in einer neuen Ablage.
Es zählt daher, ob IRGENDEIN Vorfahre existiert: Von dem aus kann
`makedirs` den Rest bauen. Existiert keiner, ist die Freigabe wirklich weg.
`isdir`/`dirname` werden BEIM AUFRUF aufgelöst, nicht als Vorgabewert
gebunden — sonst zeigt die Vorgabe für immer auf die Funktion von damals,
und ein Ersetzen von `os.path.isdir` (im Test wie im Betrieb) ginge ins
Leere. Genau darüber bin ich beim Schreiben der Tests gestolpert.
"""
isdir = isdir or os.path.isdir
dirname = dirname or os.path.dirname
aktuell = dirname(pfad or "")
for _ in range(MAX_ELTERN_STUFEN):
if not aktuell:
return ""
if isdir(aktuell):
return aktuell
naechster = dirname(aktuell)
if naechster == aktuell: # Wurzel erreicht
return ""
aktuell = naechster
return ""
def _erreichbarkeit_pruefen(raw_container: str, raw_lokal: str,
final_container: str, final_lokal: str) -> str:
"""Kann DIESER Worker Quelle und Ziel überhaupt sehen? Klartext oder "".
@@ -761,17 +801,18 @@ def _erreichbarkeit_pruefen(raw_container: str, raw_lokal: str,
fremder_worker = not os.path.isdir("/app")
for zweck, container, lokal in (
("Quelle (Rohdaten)", raw_container, raw_lokal),
("Ziel (fertige Datei)", final_container, final_lokal),
("die Quelle (Rohdaten)", raw_container, raw_lokal),
("das Ziel (fertige Datei)", final_container, final_lokal),
):
if os.path.isdir(lokal):
continue
# Ziel darf fehlen, solange sein Elternordner erreichbar ist — es wird
# gleich angelegt. Nur ein unerreichbarer ELTERN-Pfad ist ein Problem.
if zweck.startswith("Ziel") and os.path.isdir(os.path.dirname(lokal) or "."):
# Das ZIEL darf fehlen — es wird gleich mit `os.makedirs` angelegt, und
# zwar samt aller fehlenden Zwischenebenen. Es genügt also, dass
# irgendein Vorfahre existiert (Herleitung in erster_vorhandener_ordner).
if zweck.startswith("das Ziel") and erster_vorhandener_ordner(lokal):
continue
text = [f"Dieser Worker erreicht die {zweck} nicht: {lokal}"]
text = [f"Dieser Worker erreicht {zweck} nicht: {lokal}"]
if fremder_worker and not mapping:
text.append(
"Ursache: RIPPY_PATH_MAP ist auf dieser Maschine nicht gesetzt. "
@@ -784,13 +825,24 @@ def _erreichbarkeit_pruefen(raw_container: str, raw_lokal: str,
"start-tray.bat setzen, z. B. "
"set RIPPY_PATH_MAP=/app/media=\\\\NAS\\rippy-media"
)
elif fremder_worker and mapping and not _ist_uebersetzt(container, lokal):
text.append(
f"RIPPY_PATH_MAP ist gesetzt ({mapping}), deckt diesen Pfad aber "
f'nicht ab: „{container}" wurde von keinem Eintrag übersetzt — '
'fehlt ein Präfix, oder ist es ein anderes Verzeichnis als erwartet?'
)
elif fremder_worker and mapping:
text.append(f"RIPPY_PATH_MAP ist gesetzt ({mapping}), deckt diesen Pfad aber nicht ab.")
if not _ist_uebersetzt(container, lokal):
text.append(
f'{container}" wurde von keinem Eintrag übersetzt — fehlt ein '
'Präfix, oder ist es ein anderes Verzeichnis als erwartet?'
)
# ⚠️ Hier stand bis zum 26.07.2026 dieselbe Meldung wie oben — „deckt
# diesen Pfad nicht ab" —, obwohl die Karte ihn nachweislich übersetzt
# hatte (der UNC-Pfad stand im selben Satz). Wer der Meldung folgte,
# suchte den Fehler in der Karte, während die Freigabe schlicht nicht
# verbunden war. Eine falsche Ursache ist teurer als gar keine.
text.append(
f'Übersetzt wurde er korrekt ({container}{lokal}, Karte: '
f'{mapping}) — dieser Pfad ist auf diesem Rechner nur gerade '
'nicht erreichbar. Ist die Freigabe verbunden? Anmeldedaten noch '
'gültig? Von Hand prüfen: den Pfad im Explorer öffnen.'
)
else:
text.append(
"Der Pfad existiert nicht. Liegt das Arbeitsverzeichnis auf einer "
+143
View File
@@ -0,0 +1,143 @@
"""Tests für die Erreichbarkeits-Prüfung vor der Kompression.
Diese Prüfung hatte KEINEN einzigen Test — und genau deshalb ist sie am
26.07.2026 im ersten vollständigen Durchlauf umgefallen: Der Rip lief sauber
durch (40 GB Akira), die Kompression brach 0,2 s später ab mit „Dieser Worker
erreicht das Ziel nicht", obwohl die Freigabe erreichbar war. Es fehlten nur
zwei noch nie angelegte Ordner.
"""
import tasks
# --- Der eigentliche Fehler: nur EINE fehlende Ebene war erlaubt -------------
def _ohne_schluss_trenner(p):
"""`\\\\NAS\\rippy\\` und `\\\\NAS\\rippy` sind derselbe Ordner.
Wichtig, weil `ntpath.dirname` für die UNC-Wurzel MIT abschließendem
Backslash zurückkommt. Das echte `os.path.isdir` stört das nicht — eine
Attrappe, die stur vergleicht, hätte hier einen Fehler erfunden, den es
nicht gibt.
"""
return p.rstrip("\\/") or p
def _fake_isdir(vorhandene):
"""isdir-Ersatz, der nur die genannten Pfade kennt."""
bekannt = {_ohne_schluss_trenner(p) for p in vorhandene}
return lambda p: _ohne_schluss_trenner(p) in bekannt
def test_findet_den_naechsten_vorhandenen_ordner():
da = {"\\\\NAS\\rippy"}
assert tasks.erster_vorhandener_ordner(
"\\\\NAS\\rippy\\movies\\Akira (1988)",
isdir=_fake_isdir(da), dirname=lambda p: p.rsplit("\\", 1)[0] if "\\" in p.strip("\\") else "",
) == "\\\\NAS\\rippy"
def test_zwei_fehlende_ebenen_sind_in_ordnung():
"""DER Vorfall: `movies` UND der Filmordner fehlten beide.
`os.makedirs` legt die ganze Kette an — es genügt also, dass irgendein
Vorfahre existiert. Die alte Prüfung sah nur nach `dirname` und erklärte
damit jeden ERSTEN Film in einer neuen Ablage für unerreichbar.
"""
da = {"/share"}
assert tasks.erster_vorhandener_ordner(
"/share/movies/Akira (1988)", isdir=_fake_isdir(da)) == "/share"
def test_gar_nichts_erreichbar_gibt_leer():
"""Ist die Freigabe wirklich weg, muss die Prüfung anschlagen."""
assert tasks.erster_vorhandener_ordner(
"/share/movies/Film", isdir=_fake_isdir(set())) == ""
def test_sucht_nicht_endlos_auf_einer_toten_freigabe():
"""Jeder Schritt kostet auf einer toten Freigabe einen Timeout."""
gefragt = []
def zaehlend(p):
gefragt.append(p)
return False
tiefer_pfad = "/" + "/".join(f"e{i}" for i in range(50))
assert tasks.erster_vorhandener_ordner(tiefer_pfad, isdir=zaehlend) == ""
assert len(gefragt) <= tasks.MAX_ELTERN_STUFEN
def test_leerer_pfad_stuerzt_nicht_ab():
assert tasks.erster_vorhandener_ordner("", isdir=_fake_isdir(set())) == ""
# --- Die Meldung: eine falsche Ursache ist teurer als gar keine -------------
def _pruefen(monkeypatch, vorhandene, mapping, im_container=False):
kennt = _fake_isdir(vorhandene)
monkeypatch.setattr(tasks.os.path, "isdir",
lambda p: kennt(p) or (im_container and p == "/app"))
monkeypatch.setenv("RIPPY_PATH_MAP", mapping)
return tasks._erreichbarkeit_pruefen(
"/app/media/rippy/job1", "\\\\NAS\\rippy\\job1",
"/app/media/rippy/movies/Akira (1988)", "\\\\NAS\\rippy\\movies\\Akira (1988)",
)
def test_alles_da_meldet_nichts(monkeypatch):
assert _pruefen(monkeypatch, {"\\\\NAS\\rippy\\job1", "\\\\NAS\\rippy\\movies\\Akira (1988)"},
"/app/media/rippy=\\\\NAS\\rippy") == ""
def test_ziel_fehlt_ganz_ist_kein_fehler(monkeypatch):
"""Der Vorfall vom 26.07.2026 — jetzt geht er durch."""
assert _pruefen(monkeypatch, {"\\\\NAS\\rippy\\job1", "\\\\NAS\\rippy"},
"/app/media/rippy=\\\\NAS\\rippy") == ""
def test_fehlende_quelle_bleibt_ein_fehler(monkeypatch):
meldung = _pruefen(monkeypatch, {"\\\\NAS\\rippy"}, "/app/media/rippy=\\\\NAS\\rippy")
assert "Quelle" in meldung
assert "Rohdaten sind NICHT verloren" in meldung
def test_uebersetzter_aber_unerreichbarer_pfad_beschuldigt_nicht_die_karte(monkeypatch):
"""⚠️ Genau die Falschaussage aus dem Vorfall.
Die Meldung lautete „RIPPY_PATH_MAP … deckt diesen Pfad aber nicht ab",
während sie ihn im selben Satz korrekt übersetzt hatte. Wer dem folgte,
suchte in der Karte statt in der Freigabe.
"""
meldung = _pruefen(monkeypatch, set(), "/app/media/rippy=\\\\NAS\\rippy")
assert "deckt diesen Pfad aber nicht ab" not in meldung
assert "Übersetzt wurde er korrekt" in meldung
assert "Freigabe verbunden" in meldung
def test_nicht_uebersetzter_pfad_beschuldigt_sehr_wohl_die_karte(monkeypatch):
"""Hier ist die Karte wirklich schuld — der Pfad blieb unverändert."""
monkeypatch.setattr(tasks.os.path, "isdir", lambda p: False)
monkeypatch.setenv("RIPPY_PATH_MAP", "/app/temp=Y:\\temp")
meldung = tasks._erreichbarkeit_pruefen(
"/app/media/rippy/job1", "/app/media/rippy/job1",
"/app/media/movies/X", "/app/media/movies/X",
)
assert "deckt diesen Pfad aber nicht ab" in meldung
assert "von keinem Eintrag übersetzt" in meldung
def test_ohne_mapping_nennt_die_wahre_ursache(monkeypatch):
meldung = _pruefen(monkeypatch, set(), "")
assert "nicht gesetzt" in meldung
assert "RIPPY_PATH_MAP" in meldung
def test_artikel_stimmt_in_beiden_meldungen(monkeypatch):
"""„erreicht die Ziel" stand wörtlich im Log des Commanders."""
quelle = _pruefen(monkeypatch, set(), "")
assert "erreicht die Quelle" in quelle
# Quelle da, Ziel unerreichbar (kein einziger Vorfahre) → Ziel-Meldung
ziel = _pruefen(monkeypatch, {"\\\\NAS\\rippy\\job1"},
"/app/media/rippy=\\\\NAS\\rippy")
assert "erreicht das Ziel" in ziel