fix(windows): Roh- und Zielordner landeten in einem Ordner namens „app"

Beim Nachstellen des leeren Bildschirms lief ein echter Test-Rip durch. Die
Rohdaten landeten in:

    F:\app\temp\raw\<job>\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 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-08-29 14:15:17 +02:00
co-authored by Claude Opus 5
parent 27c9d9a4bb
commit 423374e65a
2 changed files with 142 additions and 27 deletions
+70 -15
View File
@@ -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:
+72 -12
View File
@@ -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