From 423374e65ab63d95bb4c0e3f1d11b88efc2c8236 Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Sat, 29 Aug 2026 14:15:17 +0200 Subject: [PATCH] =?UTF-8?q?fix(windows):=20Roh-=20und=20Zielordner=20lande?= =?UTF-8?q?ten=20in=20einem=20Ordner=20namens=20=E2=80=9Eapp"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Beim Nachstellen des leeren Bildschirms lief ein echter Test-Rip durch. Die Rohdaten landeten in: F:\app\temp\raw\\Evangelion 2.22_t00.mkv (436 MB) Also in einem Ordner namens `app` auf dem Laufwerk, von dem Rippy gerade lief. ## Zwei Container-Wurzeln im Worker RAW_DIR = /app/temp/raw MEDIA_ROOT = /app/media Unter Windows sind das keine Pfade, sondern Unfaelle. Schlimmer: Die Pruefung `unter_wurzel(wahl, MEDIA_ROOT)` verwarf auch eine AUSDRUECKLICHE Wahl — ein Arbeitsordner wie `D:\Roh` liegt nicht unter `/app/media`, also fiel er still auf den Container-Standard zurueck. **Damit kam der Arbeitsordner, den der Commander am 28.08.2026 ausdruecklich bestellt hat, unter Windows nie an.** Der Dialog zeigte ihn, das Setzen ging, und der Worker ignorierte ihn — ohne ein Wort. Dasselbe galt fuer das Ziel: Eine UNC-Freigabe liegt unter gar keiner lokalen Wurzel, also waere die fertige Datei in `X:\app\media\bluray` gelandet. ## Die Wurzeln kommen jetzt aus dem Betrieb Im Container aendert sich NICHTS: dort ist `/app/media` die Wurzel und `frei` falsch. Nativ zaehlt die Wahl des Nutzers — dort IST sein Laufwerk die Grenze. Die drei bestehenden Tests wurden rot, und zwar zu Recht: Sie pruefen die Container-Regel, liefen aber unter Windows, wo `frei` gilt. Die Wurzeln sind deshalb einspritzbar — beide Betriebsfaelle sind jetzt auf jedem Rechner pruefbar, statt vom laufenden abzuhaengen. 837 Tests gruen, ruff sauber. Co-Authored-By: Claude Opus 5 --- docker/worker/ablauf.py | 85 +++++++++++++++++++++++++++++------- docker/worker/test_medien.py | 84 ++++++++++++++++++++++++++++++----- 2 files changed, 142 insertions(+), 27 deletions(-) diff --git a/docker/worker/ablauf.py b/docker/worker/ablauf.py index 4447218..c6ffd3c 100644 --- a/docker/worker/ablauf.py +++ b/docker/worker/ablauf.py @@ -157,7 +157,7 @@ def unter_wurzel(pfad: str, wurzel: str) -> bool: return pfad == sauber or pfad.startswith(sauber + "/") -def _zielbasis(target_dir, disc_type: str) -> str: +def _zielbasis(target_dir, disc_type: str, wurzeln=None) -> str: """Ablagebasis: vom Nutzer gewähltes Ziel (validiert) oder Standard. posixpath statt os.path — aus demselben Grund wie in _arbeitsverzeichnis: @@ -167,16 +167,70 @@ def _zielbasis(target_dir, disc_type: str) -> str: als der Test dafür erstmals unter Windows lief. Live war es nie: aufgerufen wird nur aus rip_disc, und das ist auf Windows-Workern verriegelt. """ + wurzel, _vorgabe, frei = wurzeln or _betriebs_wurzeln() if target_dir: - normalisiert = posixpath.normpath(target_dir) - if unter_wurzel(normalisiert, MEDIA_ROOT): + normalisiert = _normalisiert(target_dir) + if frei or unter_wurzel(normalisiert, wurzel): return normalisiert - return posixpath.join(RIP_OUTPUT_DIR, disc_type) + # Ohne Wahl: die Wurzel dieses Betriebs, nicht die des Containers. Sonst + # landete die fertige Datei unter Windows in `X:\app\media\bluray` — + # einem Ordner, den niemand gesucht hat (gemessen 29.08.2026). + from rippy import pfade + + return pfade.verbinden(wurzel if frei else RIP_OUTPUT_DIR, disc_type) -def _arbeitsverzeichnis(einstellungen: dict, job_wahl: str = "") -> str: +def _betriebs_wurzeln() -> tuple: + """`(medien_wurzel, arbeits_vorgabe, frei)` für DIESEN Betrieb. + + ## Warum das hier gebraucht wird (Befund 29.08.2026) + + Beim Nachstellen des leeren Bildschirms lief ein echter Test-Rip durch — + und die Rohdaten landeten in **`F:\\app\\temp\\raw`**. Also in einem + Ordner namens `app` auf dem Laufwerk, von dem Rippy gerade lief. + + `RAW_DIR` ist `/app/temp/raw` und `MEDIA_ROOT` ist `/app/media`; unter + Windows sind das keine Pfade, sondern Unfälle. Schlimmer noch: Die + Prüfung `unter_wurzel(wahl, MEDIA_ROOT)` verwarf **auch eine ausdrückliche + Wahl** — ein Arbeitsordner wie `D:\\Roh` liegt nicht unter `/app/media`, + also fiel er still auf den Container-Standard zurück. + + Damit kam der Arbeitsordner, den der Commander am 28.08.2026 ausdrücklich + bestellt hat („kannst du noch einbauen das man den arbeitsordner … setzen + kann"), unter Windows nie an. Der Dialog zeigte ihn, das Setzen ging, und + der Worker ignorierte ihn — ohne ein Wort. + + Im Container ändert sich nichts: Dort ist `/app/media` die Wurzel, und + `frei` ist falsch. + """ + from rippy import betrieb, config + + try: + werte = config.laden() + except Exception: # noqa: BLE001 + werte = {} + return (betrieb.medien_wurzel(werte) or MEDIA_ROOT, + betrieb.arbeits_vorgabe(werte) or RAW_DIR, + betrieb.frei_blaettern(werte)) + + +def _normalisiert(wert: str) -> str: + """Pfad säubern — nach seiner FORM, nicht nach dem laufenden Rechner. + + `posixpath` für Container-Pfade (sonst macht Windows Backslashes daraus + und die Wurzelprüfung greift nicht mehr), `os.path` für echte + Windows-Pfade. Dieselbe Regel wie in `rippy.pfade`. + """ + from rippy import pfade + + return os.path.normpath(wert) if pfade.ist_windows_pfad(wert) \ + else posixpath.normpath(wert) + + +def _arbeitsverzeichnis(einstellungen: dict, job_wahl: str = "", + wurzeln=None) -> str: """Basis für Roh-Rips. Reihenfolge: Wahl DIESES Rips → UI-Setting - `workDir` → Container-Default /app/temp/raw. + `workDir` → Vorgabe dieses Betriebs (Container: /app/temp/raw). Hintergrund (Commander 24.07.): Die VM-Platte (150 GB) reicht für BD-50, aber eine 4K-UHD (bis 100 GB roh + Kompression daneben) sprengt sie — @@ -187,17 +241,18 @@ def _arbeitsverzeichnis(einstellungen: dict, job_wahl: str = "") -> str: Standard — und ist damit der Wert, der bei Vollautomatik-Rips greift, bei denen niemand gefragt wird. """ - # posixpath statt os.path: Das sind IMMER Container-Pfade (/app/media/...), - # auch wenn ein nativer Windows-Worker dieses Modul lädt — der übersetzt - # sie erst später mit pfad_lokal(). os.path.normpath macht unter Windows - # Backslashes daraus, und dann greift die MEDIA_ROOT-Prüfung nicht mehr. + wurzel, vorgabe, frei = wurzeln or _betriebs_wurzeln() for kandidat in (job_wahl, einstellungen.get("workDir")): wert = (kandidat or "").strip() - if wert: - normalisiert = posixpath.normpath(wert) - if unter_wurzel(normalisiert, MEDIA_ROOT): - return normalisiert - return RAW_DIR + if not wert: + continue + normalisiert = _normalisiert(wert) + # Nativ zählt die Wahl des Nutzers — dort IST sein Laufwerk die + # Grenze. Im Container bleibt die Wurzelprüfung: Ein Pfad ausserhalb + # von /app/media wäre dort ein Pfad ins Nichts. + if frei or unter_wurzel(normalisiert, wurzel): + return normalisiert + return vorgabe def _frei_bytes(pfad: str) -> int: diff --git a/docker/worker/test_medien.py b/docker/worker/test_medien.py index ce0e089..b8a288e 100644 --- a/docker/worker/test_medien.py +++ b/docker/worker/test_medien.py @@ -121,24 +121,33 @@ def test_pfad_lokal_uebersetzt_fuer_windows_worker(): # Praxis scheiterte (st_dev war identisch, os.rename trotzdem EXDEV). +# Die Wurzeln je Betrieb — eingespritzt, damit BEIDE Faelle ueberall pruefbar +# sind. Vorher hingen diese Tests am laufenden Rechner: Unter Windows ist +# `frei` wahr, und die Container-Regeln galten dort nicht mehr (am 29.08.2026 +# prompt rot geworden). +CONTAINER = ("/app/media", "/app/temp/raw", False) +NATIV = (r"C:\Users\Tobi\Videos\Rippy", r"C:\Users\Tobi\Videos\Rippy\_arbeit", True) + + def test_arbeitsverzeichnis_wahl_des_rips_schlaegt_die_einstellung(): """Pro Rip wählbar (Commander 25.07.2026), Einstellung bleibt Standard. - Reihenfolge: Wahl dieses Rips -> Setting -> Container-Default. Der + Reihenfolge: Wahl dieses Rips -> Setting -> Vorgabe des Betriebs. Der Setting-Wert ist genau der, der bei Vollautomatik-Rips greift, weil dort niemand gefragt wird. """ import ablauf as tasks einst = {"workDir": "/app/media/movies"} - assert tasks._arbeitsverzeichnis(einst, "/app/media/rippy") == "/app/media/rippy" - assert tasks._arbeitsverzeichnis(einst) == "/app/media/movies" - assert tasks._arbeitsverzeichnis({}) == tasks.RAW_DIR + w = CONTAINER + assert tasks._arbeitsverzeichnis(einst, "/app/media/rippy", w) == "/app/media/rippy" + assert tasks._arbeitsverzeichnis(einst, "", w) == "/app/media/movies" + assert tasks._arbeitsverzeichnis({}, "", w) == "/app/temp/raw" # Ausbruchsversuche und Pfade außerhalb /app/media fallen durch - assert tasks._arbeitsverzeichnis({}, "/etc") == tasks.RAW_DIR - assert tasks._arbeitsverzeichnis({}, "/app/media/../etc") == tasks.RAW_DIR + assert tasks._arbeitsverzeichnis({}, "/etc", w) == "/app/temp/raw" + assert tasks._arbeitsverzeichnis({}, "/app/media/../etc", w) == "/app/temp/raw" # Leere Wahl fällt sauber auf die Einstellung zurück - assert tasks._arbeitsverzeichnis(einst, " ") == "/app/media/movies" + assert tasks._arbeitsverzeichnis(einst, " ", w) == "/app/media/movies" def test_unter_wurzel_faellt_nicht_auf_praefix_namen_herein(): @@ -160,16 +169,67 @@ def test_unter_wurzel_faellt_nicht_auf_praefix_namen_herein(): def test_zielbasis_lehnt_praefix_ausbruch_ab(): + """Im CONTAINER bleibt die Wurzel eine Wurzel — daran ändert die + Windows-Reparatur nichts.""" import ablauf as tasks - assert tasks._zielbasis("/app/media/movies", "bluray") == "/app/media/movies" + w = CONTAINER + assert tasks._zielbasis("/app/media/movies", "bluray", w) == "/app/media/movies" # Ausbruch per Praefix-Namen fällt auf den Standard zurück - assert tasks._zielbasis("/app/media-boese", "bluray") != "/app/media-boese" - assert tasks._zielbasis("/etc", "bluray") != "/etc" + assert tasks._zielbasis("/app/media-boese", "bluray", w) != "/app/media-boese" + assert tasks._zielbasis("/etc", "bluray", w) != "/etc" def test_arbeitsverzeichnis_lehnt_praefix_ausbruch_ab(): import ablauf as tasks - assert tasks._arbeitsverzeichnis({}, "/app/media-boese") == tasks.RAW_DIR - assert tasks._arbeitsverzeichnis({"workDir": "/app/mediaX"}) == tasks.RAW_DIR + w = CONTAINER + assert tasks._arbeitsverzeichnis({}, "/app/media-boese", w) == "/app/temp/raw" + assert tasks._arbeitsverzeichnis({"workDir": "/app/mediaX"}, "", w) == "/app/temp/raw" + + +# ── Nativ: der Befund vom 29.08.2026 ──────────────────────────────────── +# +# Beim Nachstellen des leeren Bildschirms lief ein echter Test-Rip durch, und +# die Rohdaten landeten in `F:\app\temp\raw` — einem Ordner namens `app` auf +# dem Laufwerk, von dem Rippy gerade lief. Ursache: `RAW_DIR` ist +# `/app/temp/raw`, und die Pruefung `unter_wurzel(wahl, "/app/media")` verwarf +# sogar eine AUSDRUECKLICHE Wahl. +# +# Damit kam der Arbeitsordner, den der Commander am 28.08.2026 bestellt hat, +# unter Windows nie an: Der Dialog zeigte ihn, das Setzen ging, der Worker +# ignorierte ihn — ohne ein Wort. + + +def test_nativ_zaehlt_die_wahl_des_nutzers(): + """DER Befund. `D:\\Roh` liegt unter keiner Container-Wurzel und wurde + deshalb still verworfen.""" + import ablauf as tasks + + assert tasks._arbeitsverzeichnis({}, r"D:\Roh", NATIV) == r"D:\Roh" + assert tasks._arbeitsverzeichnis({"workDir": r"E:\Arbeit"}, "", NATIV) == r"E:\Arbeit" + + +def test_nativ_faellt_auf_den_ort_aus_der_installation_zurueck(): + """Nicht auf `/app/temp/raw` — das wurde unter Windows zu `X:\\app\\temp`.""" + import ablauf as tasks + + assert tasks._arbeitsverzeichnis({}, "", NATIV) == NATIV[1] + assert "/app/" not in tasks._arbeitsverzeichnis({}, "", NATIV) + + +def test_nativ_nimmt_auch_eine_freigabe_als_ziel(): + """Sein Ziel ist eine UNC-Freigabe — die liegt unter gar keiner lokalen + Wurzel.""" + import ablauf as tasks + + unc = r"\\192.168.179.62\rippy\movies" + assert tasks._zielbasis(unc, "bluray", NATIV) == unc + + +def test_nativ_ohne_wahl_landet_unter_der_eigenen_ablage(): + import ablauf as tasks + + ziel = tasks._zielbasis("", "bluray", NATIV) + assert ziel.startswith(NATIV[0]) + assert "/app/" not in ziel