Files
rippy/docker/worker/test_original_aufheben.py
T
Hitonabi ef0a574a70 fix(worker,api): Platten-Schutz griff nicht, Fortschritt log, Auswurf tat nichts
Vier Funde aus der Durchsicht, alle auf der VM gemessen.

1. DER PLATTEN-SCHUTZ AUS c065967 WAR WIRKUNGSLOS

_original_aufheben() entschied per os.stat().st_dev, ob umgehaengt oder
kopiert werden muss. Im Worker-Container gemessen - beides gleichzeitig wahr:

    st_dev /app/temp  = 2050
    st_dev /app/media = 2050        → identisch
    os.rename(...)    → EXDEV, "Invalid cross-device link"

Der Kernel vergleicht bei rename() den MOUNT, nicht das Geraet. /app/temp
(Docker-Volume) und /app/media (Bind-Mount) sind zwei Mounts DERSELBEN
ext4-Partition. Die Pruefung sah "gleiches Dateisystem", uebersprang die
Platzpruefung, und shutil.move kopierte doch - 75 GB bei 37 GB frei. Der
Schutz haette genau den Schaden zugelassen, gegen den er gebaut wurde.

Jetzt wird os.rename VERSUCHT statt vorhergesagt: klappt es, ist es
umgehaengt und fertig; kommt EXDEV, steht die Kopie fest und ERST DANN wird
der Platz geprueft. Das ist keine Vermutung mehr, sondern die Antwort des
Kernels. Vier Tests in test_original_aufheben.py, darunter genau der Fall,
der die Platte fuellte. Die zwei alten Tests in test_medien.py sind dorthin
gewandert - sie taeuschten per gefaelschtem os.stat "verschiedene
Dateisysteme" vor, also genau die Annahme, an der der Schutz scheiterte.

2. DIE FORTSCHRITTSANZEIGE ZEIGTE DEN SCAN, NICHT DEN ENCODE

get_progress_from_line matchte jede Zahl vor einem Prozentzeichen. HandBrake
gibt Prozente aber in drei Phasen aus (Formatstrings aus dem Binary gelesen):

    Scanning title %d of %d, preview %d, %.2f %%          → laeuft VOR dem
                                                            Encode bis 100 %
    Encoding: task %d of %d, %.2f %%       (%.2f fps, avg  → der echte Wert
    Encoding: task %d of %d, Searching for start time, ... → Vorlauf

Dazu warf `if progress > 0` im Aufrufer jeden Wert unter 1,00 % weg. Live
beobachtet: Anzeige stand auf 99 %, der Encode bei 1,06 %; sie fiel erst auf
1, als der Encode die 1-%-Marke ueberschritt. Jetzt wird nur die
Encoding-Zeile gelesen, `task N of M` mitgerechnet (sonst springt die
Anzeige bei Zwei-Pass-Presets mitten in der Datei zurueck), und -1 heisst
"keine Angabe" - dasselbe Muster wie bei get_progress_from_prgv.

3. "AUTOMATISCHER AUSWURF" WURDE VON NIEMANDEM GELESEN

Die Einstellung (Standard: ein, "Disc nach erfolgreichem Ripping automatisch
auswerfen") kam in keiner Zeile Backend-Code vor. DVD/Blu-ray warfen deshalb
NIE aus, Audio-CDs IMMER, weil abcde `-x` fest verdrahtet bekam. Jetzt
entscheidet die Einstellung beides: wirf_disc_aus() per CDROMEJECT-ioctl
(fcntl-guarded, der native Windows-Worker laedt das Modul auch) und `-x` nur
noch, wenn gewuenscht.

4. PFAD-PRUEFUNG FIEL AUF PRAEFIX-NAMEN HEREIN

Elf Stellen prueften mit nacktem startswith(MEDIA_ROOT). "/app/media-boese/x"
beginnt mit "/app/media", liegt aber ausserhalb - betroffen waren auch
/browse und /browse/mkdir, wo der Pfad vom Nutzer kommt. Neuer
Zwillings-Helfer unter_wurzel() in api/main.py und worker/tasks.py, alle elf
Stellen umgestellt, Tests in beiden.

Nebenbefund: _zielbasis() benutzte os.path.normpath - unter Windows werden
daraus Backslashes, die MEDIA_ROOT-Pruefung greift nicht mehr, und das
gewaehlte Ziel faellt still auf den Standard zurueck. Genau die Falle, die
_arbeitsverzeichnis() drei Zeilen weiter dokumentiert und mit posixpath
vermeidet. Live war es nie (nur aus rip_disc, das auf Windows verriegelt
ist), jetzt konsistent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 21:05:11 +02:00

125 lines
4.3 KiB
Python

"""Tests fuer _original_aufheben — der Pfad, der am 25.07.2026 die Platte fuellte.
Der Kern: Es wird NICHT vorhergesagt, ob umgehaengt werden kann, sondern
os.rename versucht. Die alte Fassung verglich st_dev und lag damit falsch —
auf der Rippy-VM sind st_dev von /app/temp und /app/media identisch (2050),
os.rename scheitert zwischen ihnen trotzdem mit EXDEV, weil der Kernel den
Mount vergleicht und nicht das Geraet. Die Platzpruefung wurde deshalb
uebersprungen und shutil.move kopierte 75 GB bei 37 GB frei.
"""
import errno
import os
import pytest
import tasks
class FakeDb:
"""Faengt nur die Log-Zeilen ab — mehr braucht _original_aufheben nicht."""
def __init__(self):
self.logs = []
def add_log(self, level, source, message):
self.logs.append((level, message))
def meldungen(self):
return " ".join(m for _, m in self.logs)
@pytest.fixture
def fake_db(monkeypatch):
ersatz = FakeDb()
monkeypatch.setattr(tasks, "db", ersatz)
return ersatz
def _lege_rohdaten_an(tmp_path, groesse=2048):
raw = tmp_path / "raw" / "job-1"
raw.mkdir(parents=True)
(raw / "title_t00.mkv").write_bytes(b"x" * groesse)
final = tmp_path / "media" / "Film (2020)"
final.mkdir(parents=True)
return str(raw), str(final)
def test_umhaengen_wenn_derselbe_mount(tmp_path, fake_db):
"""Der gute Fall: rename klappt, nichts wird kopiert, kein Platz noetig."""
raw, final = _lege_rohdaten_an(tmp_path)
tasks._original_aufheben("job-1", raw, final)
assert os.path.isdir(os.path.join(final, "original"))
assert os.path.isfile(os.path.join(final, "original", "title_t00.mkv"))
assert not os.path.exists(raw)
assert "umgehängt" in fake_db.meldungen()
def test_bei_exdev_und_zu_wenig_platz_wird_nur_gewarnt(tmp_path, fake_db, monkeypatch):
"""Der Fall, der die Platte fuellte: rename geht nicht, Platz reicht nicht.
Vorher lief hier eine Vollkopie an, weil die st_dev-Pruefung „gleiches
Dateisystem" meldete und die Platzpruefung deshalb ausblieb.
"""
raw, final = _lege_rohdaten_an(tmp_path)
def kein_rename(*_a, **_k):
raise OSError(errno.EXDEV, "Invalid cross-device link")
monkeypatch.setattr(tasks.os, "rename", kein_rename)
monkeypatch.setattr(tasks, "_frei_bytes", lambda _p: 1024) # weniger als die Rohdaten
tasks._original_aufheben("job-1", raw, final)
# Rohdaten bleiben unangetastet liegen, es wurde NICHTS kopiert
assert os.path.isfile(os.path.join(raw, "title_t00.mkv"))
assert not os.path.exists(os.path.join(final, "original"))
meldungen = fake_db.meldungen()
assert "NICHT aufgehoben" in meldungen
assert "anderen Mount" in meldungen
assert "Arbeitsverzeichnis" in meldungen # nennt die Abhilfe
def test_bei_exdev_und_genug_platz_wird_kopiert(tmp_path, fake_db, monkeypatch):
raw, final = _lege_rohdaten_an(tmp_path)
echtes_rename = os.rename
aufrufe = {"n": 0}
def rename_erst_exdev(*args, **kwargs):
# Nur der Versuch von _original_aufheben scheitert; shutil.move darf
# intern weiter umbenennen (es kopiert selbst und benennt Teile um).
aufrufe["n"] += 1
if aufrufe["n"] == 1:
raise OSError(errno.EXDEV, "Invalid cross-device link")
return echtes_rename(*args, **kwargs)
monkeypatch.setattr(tasks.os, "rename", rename_erst_exdev)
monkeypatch.setattr(tasks, "_frei_bytes", lambda _p: 10 * 1024**3)
tasks._original_aufheben("job-1", raw, final)
assert os.path.isfile(os.path.join(final, "original", "title_t00.mkv"))
assert "Original behalten" in fake_db.meldungen()
def test_anderer_fehler_wird_ehrlich_gemeldet_und_reisst_job_nicht_mit(
tmp_path, fake_db, monkeypatch
):
"""Ein Fehler beim Aufheben darf den Job NIE scheitern lassen — die
komprimierte Datei ist zu diesem Zeitpunkt fertig und in Ordnung."""
raw, final = _lege_rohdaten_an(tmp_path)
def zugriff_verweigert(*_a, **_k):
raise OSError(errno.EACCES, "Permission denied")
monkeypatch.setattr(tasks.os, "rename", zugriff_verweigert)
tasks._original_aufheben("job-1", raw, final) # darf nicht werfen
meldungen = fake_db.meldungen()
assert "konnte nicht aufgehoben werden" in meldungen
assert raw in meldungen # sagt, WO die Rohdatei liegt