Files
rippy/docker/api/mounts.py
T
Hitonabi b584cc29ad
Ampel / ampel (push) Successful in 29s
Universal-Sprint: Wizard, UI-Mounts, Encoder-Erkennung, Task-Split, README
Commander-Ziel: All-in-one, universell, weitergebbar.

- First-Run-Wizard: startet automatisch bei neuer Installation (Keys,
  Verarbeitung, erkannte Hardware); /setup + /setup/complete
- Data-Mounts via UI: Einstellungen -> Speicherziele haengt NFS/SMB direkt
  ein (mounts.py, CAP_SYS_ADMIN + rshared-Propagation, Auto-Remount beim
  Start, CIFS-Creds via Datei statt Kommandozeile); nfs-common/cifs-utils
  im api-Image
- Encoder-Erkennung: jeder Worker meldet beim Start ehrlich seine
  Faehigkeiten (caps.py -> workers-Tabelle), GET /capabilities, Anzeige
  in Wizard + Verarbeitung-Tab
- Task-Split: transcode_files als eigener Task auf Queue "transcode"
  (Basis fuer optionale Remote-GPU-Worker, deploy/remote-transcode-worker.yml
  EXPERIMENTELL) + POST /jobs/{id}/retry-transcode + UI-Knopf
  "Neu komprimieren" bei fehlgeschlagenen Jobs
- API-Keys aus der DB: Settings-UI/Wizard ueberstimmen Env — vorher waren
  die Key-Felder im UI reine Dekoration (Clients lasen nur Env)
- README komplett neu: generischer Schnellstart, Laufwerk-Override via
  docker-compose.override.yml, Architektur, Env-Tabelle

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 20:49:08 +02:00

110 lines
3.7 KiB
Python

"""Netzwerk-Speicherziele (NFS/SMB) direkt aus Rippy heraus einhängen.
Commander-Anforderung 23.07.: Data-Mounts über das UI, universell für jede
Installation. Der api-Container läuft dafür mit CAP_SYS_ADMIN und einem
rshared-Bind auf /app/media — ein Mount hier propagiert über den Host in
alle anderen Container (Worker sieht das Ziel sofort).
Zugangsdaten liegen in Postgres (Klartext — bewusster Heimnetz-Kompromiss,
im README dokumentiert; SMB-Credentials wandern NIE in die Kommandozeile,
sondern über eine temporäre credentials-Datei).
"""
import os
import re
import subprocess
import tempfile
import db
MEDIA_ROOT = "/app/media"
NAME_MUSTER = re.compile(r"^[a-z0-9][a-z0-9-]{1,30}$")
def _mountpoint(name: str) -> str:
return os.path.join(MEDIA_ROOT, name)
def validiere_name(name: str) -> bool:
return bool(NAME_MUSTER.match(name))
def ist_gemountet(name: str) -> bool:
return os.path.ismount(_mountpoint(name))
def mounten(name: str, typ: str, quelle: str, optionen: str = "",
username: str = "", passwort: str = "") -> None:
"""Hängt ein NFS/CIFS-Ziel unter /app/media/<name> ein. Wirft RuntimeError."""
ziel = _mountpoint(name)
os.makedirs(ziel, exist_ok=True)
if os.path.ismount(ziel):
return
creds_datei = None
try:
if typ == "nfs":
opts = optionen or "vers=4,soft,timeo=100"
cmd = ["mount", "-t", "nfs", "-o", opts, quelle, ziel]
elif typ == "cifs":
teile = [optionen] if optionen else []
if username:
# Credentials über Datei statt Kommandozeile (ps-sichtbar!)
creds = tempfile.NamedTemporaryFile(
"w", delete=False, prefix="cifs-", suffix=".cred"
)
creds.write(f"username={username}\npassword={passwort or ''}\n")
creds.close()
os.chmod(creds.name, 0o600)
creds_datei = creds.name
teile.append(f"credentials={creds.name}")
else:
teile.append("guest")
teile.append("iocharset=utf8")
cmd = ["mount", "-t", "cifs", "-o", ",".join(teile), quelle, ziel]
else:
raise RuntimeError(f"Unbekannter Typ: {typ} (nfs oder cifs)")
ergebnis = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
if ergebnis.returncode != 0:
fehler = (ergebnis.stderr or ergebnis.stdout or "").strip()
raise RuntimeError(f"mount schlug fehl: {fehler[:300]}")
finally:
if creds_datei:
try:
os.unlink(creds_datei)
except OSError:
pass
def aushaengen(name: str) -> None:
ziel = _mountpoint(name)
if os.path.ismount(ziel):
ergebnis = subprocess.run(
["umount", ziel], capture_output=True, text=True, timeout=30
)
if ergebnis.returncode != 0:
raise RuntimeError(
f"umount schlug fehl: {(ergebnis.stderr or '').strip()[:300]}"
)
try:
os.rmdir(ziel)
except OSError:
pass # nicht leer oder weg — egal
def alle_remounten() -> list:
"""Beim API-Start: alle gespeicherten Mounts wiederherstellen."""
meldungen = []
for eintrag in db.list_mounts():
try:
mounten(
eintrag["name"], eintrag["typ"], eintrag["quelle"],
eintrag.get("optionen") or "", eintrag.get("username") or "",
eintrag.get("passwort") or "",
)
meldungen.append(f"{eintrag['name']}: eingehängt")
except Exception as e:
meldungen.append(f"{eintrag['name']}: FEHLER — {e}")
return meldungen