fix(worker): erster Film in einer neuen Ablage war immer unerreichbar
Ampel / ampel (push) Failing after 30s
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:
+64
-12
@@ -735,6 +735,46 @@ def _ist_uebersetzt(container_pfad: str, lokal_pfad: str) -> bool:
|
|||||||
return container_pfad != lokal_pfad
|
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,
|
def _erreichbarkeit_pruefen(raw_container: str, raw_lokal: str,
|
||||||
final_container: str, final_lokal: str) -> str:
|
final_container: str, final_lokal: str) -> str:
|
||||||
"""Kann DIESER Worker Quelle und Ziel überhaupt sehen? Klartext oder "".
|
"""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")
|
fremder_worker = not os.path.isdir("/app")
|
||||||
|
|
||||||
for zweck, container, lokal in (
|
for zweck, container, lokal in (
|
||||||
("Quelle (Rohdaten)", raw_container, raw_lokal),
|
("die Quelle (Rohdaten)", raw_container, raw_lokal),
|
||||||
("Ziel (fertige Datei)", final_container, final_lokal),
|
("das Ziel (fertige Datei)", final_container, final_lokal),
|
||||||
):
|
):
|
||||||
if os.path.isdir(lokal):
|
if os.path.isdir(lokal):
|
||||||
continue
|
continue
|
||||||
# Ziel darf fehlen, solange sein Elternordner erreichbar ist — es wird
|
# Das ZIEL darf fehlen — es wird gleich mit `os.makedirs` angelegt, und
|
||||||
# gleich angelegt. Nur ein unerreichbarer ELTERN-Pfad ist ein Problem.
|
# zwar samt aller fehlenden Zwischenebenen. Es genügt also, dass
|
||||||
if zweck.startswith("Ziel") and os.path.isdir(os.path.dirname(lokal) or "."):
|
# irgendein Vorfahre existiert (Herleitung in erster_vorhandener_ordner).
|
||||||
|
if zweck.startswith("das Ziel") and erster_vorhandener_ordner(lokal):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
text = [f"Dieser Worker erreicht die {zweck} nicht: {lokal}"]
|
text = [f"Dieser Worker erreicht {zweck} nicht: {lokal}"]
|
||||||
if fremder_worker and not mapping:
|
if fremder_worker and not mapping:
|
||||||
text.append(
|
text.append(
|
||||||
"Ursache: RIPPY_PATH_MAP ist auf dieser Maschine nicht gesetzt. "
|
"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. "
|
"start-tray.bat setzen, z. B. "
|
||||||
"set RIPPY_PATH_MAP=/app/media=\\\\NAS\\rippy-media"
|
"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:
|
elif fremder_worker and mapping:
|
||||||
text.append(f"RIPPY_PATH_MAP ist gesetzt ({mapping}), deckt diesen Pfad aber nicht ab.")
|
# ⚠️ Hier stand bis zum 26.07.2026 dieselbe Meldung wie oben — „deckt
|
||||||
if not _ist_uebersetzt(container, lokal):
|
# diesen Pfad nicht ab" —, obwohl die Karte ihn nachweislich übersetzt
|
||||||
text.append(
|
# hatte (der UNC-Pfad stand im selben Satz). Wer der Meldung folgte,
|
||||||
f'„{container}" wurde von keinem Eintrag übersetzt — fehlt ein '
|
# suchte den Fehler in der Karte, während die Freigabe schlicht nicht
|
||||||
'Präfix, oder ist es ein anderes Verzeichnis als erwartet?'
|
# 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:
|
else:
|
||||||
text.append(
|
text.append(
|
||||||
"Der Pfad existiert nicht. Liegt das Arbeitsverzeichnis auf einer "
|
"Der Pfad existiert nicht. Liegt das Arbeitsverzeichnis auf einer "
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user