diff --git a/.env.example b/.env.example index 1366eab..3f028a5 100644 --- a/.env.example +++ b/.env.example @@ -1,14 +1,25 @@ # Rippy-Umgebung — nach .env kopieren und Werte eintragen. # Die .env liegt NUR auf der VM (gitignored), nie im Repo. -# PostgreSQL (intern; Compose nutzt aktuell rippy/rippy — Härtung folgt) +# PostgreSQL (intern). Default rippy; für eine exponierte Umgebung hier ein starkes +# Passwort setzen — Compose nutzt diesen Wert jetzt wirklich (DB + App-Verbindung). POSTGRES_PASSWORD=rippy +# Optisches Laufwerk (Host-Geräteknoten) — je Rechner UNTERSCHIEDLICH! +# Der Worker braucht den SCSI-CD-ROM-Knoten (meist /dev/sr0) UND den passenden +# generischen sg-Knoten des Laufwerks. Die sg-Nummer variiert je Host — auf DIESER +# Maschine ermitteln: lsscsi -g (oder: ls -l /dev/sg*). Hier den HOST-Pfad des +# Blu-ray/DVD-Laufwerks eintragen; der Default passt für den Ursprungs-Host. +OPTICAL_SR=/dev/sr0 +OPTICAL_SG=/dev/sg1 + # (JWT/Auth wurde am 24.07.2026 komplett entfernt — Rippy ist Heimnetz-only, -# Commander-Entscheid, siehe KONZEPT.md Abschnitt 10.) +# Commander-Entscheid, siehe KONZEPT.md Abschnitt 10. Härtung für exponierte +# Netze: siehe README, Abschnitt "Härtung für fremde/exponierte Netze".) # Metadaten-APIs -# TMDB (Pflicht für Metadaten-Lookup): kostenlos auf themoviedb.org +# TMDB (empfohlen für Metadaten-Lookup; ohne Key startet Rippy trotzdem, Key auch +# später im UI setzbar): kostenlos auf themoviedb.org TMDB_API_KEY= # TVDb (optional, Serien-Fallback) THETVDB_API_KEY= diff --git a/README.md b/README.md index f30c145..b0efc94 100644 --- a/README.md +++ b/README.md @@ -21,20 +21,23 @@ ersten Mal automatisch (API-Keys, Verarbeitung, erkannte Hardware). ### Laufwerk anpassen -Standard ist `/dev/sr0` (+ `/dev/sg1` für den Worker — MakeMKV spricht -Laufwerke über die SCSI-Generic-Schicht an). Andere Geräte? Lege eine -`docker-compose.override.yml` an: +Der Worker braucht **zwei** Host-Geräteknoten: den CD-ROM-Knoten (meist +`/dev/sr0`) und den passenden **SCSI-Generic-Knoten** (`/dev/sgN`) — MakeMKV +spricht Laufwerke über die sg-Schicht an. Die sg-Nummer ist **je Host anders** +(auf frischen Systemen oft `/dev/sg0`). -```yaml -services: - api: - devices: ["/dev/sr1:/dev/sr0"] - worker: - devices: ["/dev/sr1:/dev/sr0", "/dev/sg2:/dev/sg1"] +Ermitteln, welche Knoten dein Blu-ray/DVD-Laufwerk hat: +```bash +lsscsi -g # zeigt Modell + zugehörigen /dev/sgN +# oder: ls -la /sys/class/scsi_generic/ ``` - -Welche sg-Nummer dein Laufwerk hat, verrät `lsscsi -g` oder -`ls -la /sys/class/scsi_generic/`. +Dann in der `.env` eintragen (Defaults passen für den Ursprungs-Host): +```bash +OPTICAL_SR=/dev/sr0 # CD-ROM-Knoten deines Laufwerks +OPTICAL_SG=/dev/sg0 # sg-Knoten deines Laufwerks +``` +`docker compose up -d` nutzt diese Werte. (Für Feineinstellungen geht weiterhin +eine `docker-compose.override.yml` mit eigenen `devices:`-Einträgen.) **Laufwerk in einer VM?** Per USB-Passthrough anhand der Vendor-ID durchreichen (Proxmox: `qm set -usb0 host=xxxx:yyyy,usb3=1`) — @@ -150,6 +153,9 @@ bleibt All-in-one. | `THETVDB_API_KEY` | optional | Serien-Fallback | | `MAKEMKV_APP_KEY` | optional | MakeMKV-Beta-Key (Forum); DVDs gehen ohne — bequemer: im UI unter Einstellungen → System pflegen | | `MAKEMKV_URL_BASE` | optional | alternative Download-Quelle für den Image-Build | +| `OPTICAL_SR` | je Host | Host-Pfad des CD-ROM-Knotens (Default `/dev/sr0`) — siehe „Laufwerk anpassen" | +| `OPTICAL_SG` | je Host | Host-Pfad des sg-Knotens des Laufwerks (Default `/dev/sg1`; oft `/dev/sg0`) | +| `POSTGRES_PASSWORD` | optional | DB-Passwort (Default `rippy`) — für exponierte Umgebungen ein starkes Passwort setzen | UI-Einstellungen (Wizard/Settings) überstimmen die Env-Variablen. @@ -165,7 +171,16 @@ Voraussetzungen auf dem Ziel-Host: In einer VM: per **USB-Passthrough** (Vendor-ID) durchreichen, NICHT als emuliertes CD-ROM — siehe „Laufwerk anpassen" oben. 3. Ablage-Basis anlegen: `mkdir -p /srv/rippy/media` (oder Pfad in - `docker-compose.yml` anpassen — muss ein rshared-fähiger Bind sein). + `docker-compose.yml` anpassen). Der Bind nutzt **shared/slave-Propagation** + (fürs In-UI-Einhängen von NAS-Freigaben). Auf den meisten systemd-Linux-Hosts + ist `/` schon `rshared`, dann ist nichts zu tun. Bricht `docker compose up` + mit *„path … is mounted on / but it is not a shared mount"* ab, einmalig: + ```bash + sudo mount --bind /srv/rippy/media /srv/rippy/media + sudo mount --make-rshared /srv/rippy/media + ``` + (reboot-fest als systemd-`.mount`-Unit persistieren.) Braucht einen klassischen + Linux-Docker-Host mit `SYS_ADMIN` — nicht Docker-Desktop/rootless/Podman. Dann wie im Schnellstart: klonen, `.env` füllen, `docker compose up -d --build`, `http://` öffnen — der Einrichtungs-Assistent führt durch @@ -175,6 +190,25 @@ docker compose up -d --build`. Nicht mitnehmen musst du: Gitea, Arcane, den CI-Runner — das ist die Entwicklungs-Infrastruktur DIESER Installation, nicht Teil von Rippy. +### Härtung für fremde/exponierte Netze + +Rippy ist bewusst **Heimnetz-only**: Es hat **keine Authentifizierung**, und +Redis + PostgreSQL veröffentlichen Ports auf allen Interfaces (für Remote-Worker). +Im vertrauten LAN ist das ein akzeptierter Kompromiss — **exponiere Rippy niemals +ungeschützt an ein unsicheres oder öffentliches Netz.** Wer es weiter absichern will: + +- **Zugriff kapseln:** API (`:8000`) und UI hinter einen Reverse-Proxy mit Auth + (Basic-/Forward-Auth) legen oder nur über VPN erreichbar machen; Port `8000` + nicht auf `0.0.0.0` binden. +- **DB/Broker abschotten:** `POSTGRES_PASSWORD` in der `.env` auf ein starkes + Passwort setzen; die Ports `5432`/`6379` nur an ein internes/VPN-Interface binden + (z. B. `"10.0.0.5:5432:5432"`) statt an `0.0.0.0`; Redis mit `--requirepass + ` starten und das Passwort in `REDIS_URL` ergänzen. +- **CORS:** `main.py` erlaubt aktuell `*` (nötig, weil UI und API getrennte Ports + sind) — hinter einem Proxy auf die echte UI-Herkunft einschränken. +- **NAS-Zugangsdaten** liegen bewusst im Klartext in der DB — ein weiterer Grund, + den DB-Port nie offen ins unsichere Netz zu hängen. + ## Entwicklung CI („Ampel") läuft bei jedem Push: Ruff, pytest, Vite-Build. Es gibt genau diff --git a/deploy.sh b/deploy.sh index ea2a0c0..68027e0 100644 --- a/deploy.sh +++ b/deploy.sh @@ -1,27 +1,48 @@ #!/usr/bin/env bash -# Deploy für Rippy — deployt den aktuellen 'main'-Stand auf die VM. -# Single Source of Truth ist seit 24.07.2026 'main' (kein stable-Branch mehr); -# die Ampel prüft nur noch, befördert aber nichts. Vor dem Deploy also selbst -# sicherstellen, dass die Ampel für den zu deployenden Commit GRÜN ist. +# Deploy fuer Rippy — deployt den aktuellen 'main'-Stand auf die Ziel-VM. +# Single Source of Truth ist 'main' (kein stable-Branch); die Ampel prueft nur, +# befoerdert nichts. Vor dem Deploy selbst sicherstellen, dass die Ampel fuer den +# zu deployenden Commit GRUEN ist. # -# Nutzt einen EIGENEN Klon (~/notfall-rippy) und fasst Arcanes -# Projektverzeichnis (~/projects/rippy) NIE an — 22.07. gelernt: ein git-Klon -# im Arcane-Verzeichnis kollidiert mit dessen Sync (read-only .git-Objekte). -# Die .env gehört Arcane und wird nur GELESEN (für compose-Interpolation). +# Generisch/portabel: Ziel-Host, Repo-URL und .env-Pfad kommen aus der Umgebung — +# KEINE festen Adressen im Repo. Fuer eine Erst-Installation auf einem eigenen Host +# reicht ohnehin der einfache Weg (siehe README): +# git clone && cd rippy && cp .env.example .env (Werte eintragen) +# docker compose up -d --build +# +# Dieses Skript ist nur die bequeme Remote-Variante: es SSHt zur VM, nutzt dort +# einen EIGENEN Klon (Default ~/notfall-rippy) und fasst ein evtl. vorhandenes +# Projektverzeichnis NIE an (git-Klon dort kollidiert mit dessen Sync). Die .env +# gehoert der VM und wird nur GELESEN (fuer compose-Interpolation). +# +# Konfiguration ueber Umgebungsvariablen: +# RIPPY_VM Ziel-Host fuer ssh, z.B. user@10.0.0.5 (PFLICHT) +# RIPPY_REPO_URL Git-Repo-URL (branch main) (PFLICHT) +# RIPPY_ENV_SRC Pfad zur .env auf der VM (Default: ~/rippy/.env) +# RIPPY_CLONE Arbeits-Klon auf der VM (Default: ~/notfall-rippy) +# Optionales Argument $1: nur einen Dienst neu bauen, z.B. ./deploy.sh api set -euo pipefail -VM="arcane@192.168.178.162" -DIENST="${1:-}" # optional: nur einen Dienst neu bauen, z.B. ./deploy.sh api +VM="${RIPPY_VM:?RIPPY_VM setzen, z.B. RIPPY_VM=user@10.0.0.5}" +REPO_URL="${RIPPY_REPO_URL:?RIPPY_REPO_URL setzen (Git-URL des Rippy-Repos)}" +ENV_SRC="${RIPPY_ENV_SRC:-}" # leer -> Remote-Default ~/rippy/.env +CLONE="${RIPPY_CLONE:-}" # leer -> Remote-Default ~/notfall-rippy +DIENST="${1:-}" -ssh "$VM" 'bash -s' </dev/null || echo "WARNUNG: keine .env in Arcanes Projektverzeichnis" +cp "$ENV_SRC" .env 2>/dev/null || echo "WARNUNG: keine .env unter $ENV_SRC gefunden" docker compose -p rippy up -d --build $DIENST docker compose -p rippy ps --format 'table {{.Name}}\t{{.Status}}' REMOTE diff --git a/docker-compose.yml b/docker-compose.yml index e7c0509..2e3b15f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,7 @@ services: context: . dockerfile: docker/api/Dockerfile environment: - - DATABASE_URL=postgresql://rippy:rippy@postgres:5432/rippy + - DATABASE_URL=postgresql://rippy:${POSTGRES_PASSWORD:-rippy}@postgres:5432/rippy - REDIS_URL=redis://redis:6379/0 - TMDB_API_KEY=${TMDB_API_KEY:-} - THETVDB_API_KEY=${THETVDB_API_KEY:-} @@ -48,7 +48,7 @@ services: propagation: rshared - temp:/app/temp devices: - - /dev/sr0:/dev/sr0 + - ${OPTICAL_SR:-/dev/sr0}:/dev/sr0 networks: - rippy-net depends_on: @@ -74,7 +74,7 @@ services: # Einstellungen → System meldet, wenn es eine neuere Version gibt. MAKEMKV_VERSION: ${MAKEMKV_VERSION:-1.18.4} environment: - - DATABASE_URL=postgresql://rippy:rippy@postgres:5432/rippy + - DATABASE_URL=postgresql://rippy:${POSTGRES_PASSWORD:-rippy}@postgres:5432/rippy - REDIS_URL=redis://redis:6379/0 - RIP_OUTPUT_DIR=/app/media - MAKEMKV_APP_KEY=${MAKEMKV_APP_KEY} @@ -93,13 +93,14 @@ services: propagation: rslave - temp:/app/temp devices: - - /dev/sr0:/dev/sr0 - # MakeMKV spricht Laufwerke über die SCSI-Generic-Schicht an — ohne den - # sg-Knoten findet es „keine usable optical drives" (Deploy-Befund 23.07.). - # In dieser VM: sg0 = Systemplatte, sg1 = das BD-Laufwerk. Achtung: nach - # einem USB-Reconnect zur Laufzeit kann die sg-Nummer wandern → Container - # neu starten. - - /dev/sg1:/dev/sg1 + # Host-Geräteknoten via .env (OPTICAL_SR/OPTICAL_SG) — je Rechner ANDERS! + # MakeMKV spricht Laufwerke über die SCSI-Generic-Schicht an; ohne den + # passenden sg-Knoten findet es „keine usable optical drives". Den sg-Knoten + # des Laufwerks auf DIESEM Host ermitteln (lsscsi -g) und in die .env eintragen. + # Achtung: nach USB-Reconnect zur Laufzeit kann die sg-Nummer wandern → + # Container neu starten. Defaults (sr0/sg1) passen für den Ursprungs-Host. + - ${OPTICAL_SR:-/dev/sr0}:/dev/sr0 + - ${OPTICAL_SG:-/dev/sg1}:/dev/sg1 networks: - rippy-net depends_on: @@ -132,7 +133,7 @@ services: image: postgres:16-alpine environment: - POSTGRES_USER=rippy - - POSTGRES_PASSWORD=rippy + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-rippy} - POSTGRES_DB=rippy # Veröffentlicht (Befund 24.07.): Remote-Worker (GPU-Maschine, künftiger # Windows-Worker) verbinden sich über :5432/6379 — ohne diff --git a/docker/api/config_validation.py b/docker/api/config_validation.py index 0524ae6..c434f6f 100644 --- a/docker/api/config_validation.py +++ b/docker/api/config_validation.py @@ -14,12 +14,14 @@ def validate_config() -> Settings: try: settings = Settings() - # TMDB API Key ist Pflicht (gemäß KONZEPT.md) + # TMDB-Key ist EMPFOHLEN, nicht zwingend: main.py fängt diesen Fehler ab und + # gibt nur eine Warnung aus — die API startet auch ohne Key, und der Key lässt + # sich jederzeit im UI (Einstellungen → System) nachtragen. if not settings.tmdb_api_key: raise ConfigValidationError( - "TMDB_API_KEY ist erforderlich für Metadaten-Lookup.\n" - "Hole dir einen免费en Key auf https://www.themoviedb.org/\n" - "Setze ihn als Umgebungsvariable: TMDB_API_KEY=dein_key" + "TMDB_API_KEY ist nicht gesetzt (empfohlen für Metadaten-Lookup).\n" + "Kostenlosen Key auf https://www.themoviedb.org/ holen und als\n" + "Umgebungsvariable TMDB_API_KEY setzen — oder später im UI eintragen." ) return settings diff --git a/docker/postgres/Dockerfile b/docker/postgres/Dockerfile deleted file mode 100644 index 3baca12..0000000 --- a/docker/postgres/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM postgres:16 - -COPY init.sql /docker-entrypoint-initdb.d/ - -EXPOSE 5432 - diff --git a/docker/postgres/init.sql b/docker/postgres/init.sql deleted file mode 100644 index 7a88f81..0000000 --- a/docker/postgres/init.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE DATABASE rippy; -CREATE USER rippy WITH PASSWORD 'rippy123'; -GRANT ALL PRIVILEGES ON DATABASE rippy TO rippy; diff --git a/docker/redis/Dockerfile b/docker/redis/Dockerfile deleted file mode 100644 index a8c4bf3..0000000 --- a/docker/redis/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM redis:7-alpine - -COPY redis.conf /usr/local/etc/redis/redis.conf - -CMD ["redis-server", "/usr/local/etc/redis/redis.conf"] diff --git a/docker/redis/redis.conf b/docker/redis/redis.conf deleted file mode 100644 index 81d2bcc..0000000 --- a/docker/redis/redis.conf +++ /dev/null @@ -1,6 +0,0 @@ -appendonly yes -appendfsync everysec -timeout 0 -tcp-keepalive 300 -loglevel notice -databases 16 diff --git a/docker/ui/src/App.tsx b/docker/ui/src/App.tsx index b7949b2..4ce0706 100644 --- a/docker/ui/src/App.tsx +++ b/docker/ui/src/App.tsx @@ -1,14 +1,25 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import { LayoutDashboard, BookOpen, FileText, Settings as SettingsIcon } from 'lucide-react' import Dashboard from './pages/Dashboard' import SettingsPage from './pages/Settings' import LogsPage from './pages/Logs' import AnleitungPage from './pages/Anleitung' +import FirstRunWizard from './components/FirstRunWizard' +import { api } from './lib/api' type Page = 'dashboard' | 'anleitung' | 'logs' | 'settings' export default function App() { const [currentPage, setCurrentPage] = useState('dashboard') + // First-Run: solange der Einrichtungs-Assistent nicht abgeschlossen ist, zeigen wir + // ihn statt des Dashboards. null = /setup noch nicht geprüft (kein Aufblitzen). + const [setupDone, setSetupDone] = useState(null) + + useEffect(() => { + api.get('/setup') + .then((r) => setSetupDone(Boolean(r.data?.done))) + .catch(() => setSetupDone(true)) // API/Setup nicht erreichbar → nicht blockieren + }, []) const navItems: { id: Page; label: string; icon: any }[] = [ { id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard }, @@ -17,6 +28,9 @@ export default function App() { { id: 'settings', label: 'Einstellungen', icon: SettingsIcon }, ] + if (setupDone === null) return null // kurzer Moment, bis /setup geantwortet hat + if (!setupDone) return setSetupDone(true)} /> + return (
{/* Top Header Navigation Bar */} diff --git a/udev/README.md b/udev/README.md deleted file mode 100644 index 5ec9364..0000000 --- a/udev/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# Rippy udev-Setup - -## Problem -Rippy findet kein Laufwerk, obwohl eines am Host angeschlossen ist. - -## Ursache -Die `docker-compose.yml` nutzte anonyme Volumes für `/dev/disc`, `/dev/cdrom`, `/dev/dvd`: -```yaml -volumes: - - disc:/dev/disc # ❌ Anonymes Volume, leer -``` - -## Lösung -Direkte Mounts vom Host in den Worker-Container: - -### 1. docker-compose.yml anpassen -```yaml -volumes: - - /dev/cdrom:/dev/cdrom:ro - - /dev/dvd:/dev/dvd:ro - - /dev/sr0:/dev/sr0:ro - - disc:/dev/disc # ✅ Shared-Mount für udev-Symlinks -``` - -### 2. Worker-Dockerfile erweitern -- `isoinfo` für Disc-Typ-Erkennung -- `udev` für Geräteverwaltung - -### 3. udev-Regel installieren -Kopiere `99-disc-ripper.rules` nach `/etc/udev/rules.d/` - -### 4. udev-Daemon ausführen -Der Daemon (`udev_daemon.py`) läuft im Worker-Container und erstellt Symlinks mit UUID/Serial. - -## Installation auf Arcane VM - -```bash -cd /path/to/rippy/udev -bash install-arcane.sh -``` - -## Testen -1. Leg eine CD/DVD ein -2. Logs prüfen: `docker logs -f rippy-worker` -3. Symlinks: `docker exec rippy-worker ls -la /dev/disc` -4. Devices: `docker exec rippy-worker ls -la /dev/cdrom /dev/dvd /dev/sr0` - -## Dateien -- `99-disc-ripper.rules` — udev-Regel für Einwurf/Auswurf -- `udev_daemon.py` — Daemon für Disc-Erkennung -- `rippy-udev.service` — systemd-Service (optional) -- `install-arcane.sh` — Installations-Skript für Arcane VM \ No newline at end of file diff --git a/udev/install-arcane.sh b/udev/install-arcane.sh deleted file mode 100644 index 0dd3546..0000000 --- a/udev/install-arcane.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash -# Installations-Skript für Arcane VM (Debian 12) -# Auf der Arcane VM ausführen - -set -e - -echo "=== Rippy udev-Setup für Arcane VM ===" - -# 1. udev-Regel installieren -echo "Installiere udev-Regel..." -cp 99-disc-ripper.rules /etc/udev/rules.d/ -echo "✅ /etc/udev/rules.d/99-disc-ripper.rules installiert" - -# 2. udev-Daemon in Worker-Container kopieren -echo "Kopiere udev-Daemon in Worker-Container..." -docker cp udev_daemon.py rippy-worker:/app/udev/ -docker cp 99-disc-ripper.rules rippy-worker:/etc/udev/rules.d/ -docker cp rippy-udev.service rippy-worker:/etc/systemd/system/ -echo "✅ udev-Daemon und Regeln in Container kopiert" - -# 3. udev-Regel neu laden (im Host) -echo "Lade udev-Regeln im Host neu..." -sudo udevadm control --reload-rules -sudo udevadm trigger --subsystem-match=block -echo "✅ udev-Regeln im Host neu geladen" - -# 4. udev-Regel in Container neu laden -echo "Lade udev-Regeln im Container neu..." -docker exec -i rippy-worker udevadm control --reload-rules -docker exec -i rippy-worker udevadm trigger --subsystem-match=block -echo "✅ udev-Regeln im Container neu geladen" - -# 5. Service aktivieren (optional - wenn im Container als systemd läuft) -if docker exec -i rippy-worker which systemctl > /dev/null 2>&1; then - echo "Aktiviere systemd-Service..." - docker exec -i rippy-worker systemctl daemon-reload - docker exec -i rippy-worker systemctl enable rippy-udev - echo "✅ systemd-Service aktiviert" -fi - -echo "=== Setup abgeschlossen ===" -echo "" -echo "Testen:" -echo " 1. Leg eine CD/DVD ein" -echo " 2. Logs prüfen: docker logs -f rippy-worker" -echo " 3. Symlinks: docker exec rippy-worker ls -la /dev/disc" -echo " 4. Devices: docker exec rippy-worker ls -la /dev/cdrom /dev/dvd /dev/sr0" -echo "" -echo "Hinweis: /dev/disc ist ein Shared-Mount, der udev-Daemon erstellt Symlinks" -echo " mit UUID/Serial für jedes erkannte Laufwerk." diff --git a/udev/install-udev.sh b/udev/install-udev.sh deleted file mode 100644 index b02257f..0000000 --- a/udev/install-udev.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -# Installations-Skript für Rippy udev-Regel + Service -# Auf dem Host (Arcane VM / Proxmox LXC) ausführen - -set -e - -echo "=== Rippy udev-Setup ===" - -# Prüfen ob wir root sind -if [ "$EUID" -ne 0 ]; then - echo "❌ Bitte als root ausführen (sudo bash install-udev.sh)" - exit 1 -fi - -# 1. udev-Regel installieren -echo "Installiere udev-Regel..." -cp 99-disc-ripper.rules /etc/udev/rules.d/ -echo "✅ /etc/udev/rules.d/99-disc-ripper.rules installiert" - -# 2. udev-Daemon in den Container kopieren -echo "Kopiere udev-Daemon in Worker-Container..." -docker cp udev_daemon.py rippy-worker:/app/udev/ -docker cp 99-disc-ripper.rules rippy-worker:/etc/udev/rules.d/ -docker cp rippy-udev.service rippy-worker:/etc/systemd/system/ -echo "✅ udev-Daemon und Regeln in Container kopiert" - -# 3. udev-Regel neu laden -echo "Lade udev-Regeln neu..." -sudo udevadm control --reload-rules -sudo udevadm trigger --subsystem-match=block -echo "✅ udev-Regeln neu geladen" - -# 4. Service aktivieren (optional - wenn im Container als systemd läuft) -if docker exec -i rippy-worker which systemctl > /dev/null 2>&1; then - echo "Aktiviere systemd-Service..." - docker exec -i rippy-worker systemctl daemon-reload - docker exec -i rippy-worker systemctl enable rippy-udev - echo "✅ systemd-Service aktiviert" -fi - -echo "=== Setup abgeschlossen ===" -echo "Teste Disc-Einwurf: Leg eine CD/DVD ein" -echo "Prüfe Logs: docker logs -f rippy-worker" diff --git a/udev/rippy-udev.service b/udev/rippy-udev.service deleted file mode 100644 index c6bc4db..0000000 --- a/udev/rippy-udev.service +++ /dev/null @@ -1,29 +0,0 @@ -# systemd-Service für Rippy udev-Daemon -# Diese Datei muss nach /etc/systemd/system/rippy-udev.service kopiert werden - -[Unit] -Description=Rippy Disc Detection Daemon -After=dev-disc.device -Requires=dev-disc.device - -[Service] -Type=simple -ExecStart=/usr/bin/python3 /app/udev_daemon.py -Restart=always -RestartSec=5 -WorkingDirectory=/app -User=root -Group=root - -# Environment -Environment=REDIS_URL=redis://redis:6379/0 -Environment=PATH=/usr/local/bin:/usr/bin:/bin - -# Security Hardening -NoNewPrivileges=yes -ProtectSystem=strict -ProtectHome=read-only -PrivateTmp=yes - -[Install] -WantedBy=multi-user.target diff --git a/udev/udev_daemon.py b/udev/udev_daemon.py deleted file mode 100644 index 80a1ed9..0000000 --- a/udev/udev_daemon.py +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env python3 -""" -udev-Daemon für Rippy -Erkennt Disc-Einwurf/-auswurf und erstellt Celery-Jobs. -""" - -import os -import sys -import subprocess -import redis -import json - -REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0") -DISC_DEVICE_PATH = "/dev/disc" - -# Redis verbinden -def get_redis_client(): - try: - return redis.from_url(REDIS_URL) - except Exception as e: - print(f"Fehler beim Verbinden mit Redis: {e}") - sys.exit(1) - - -def get_device_info(device_name: str) -> dict: - """Ermittle Device-Info via udevadm.""" - try: - result = subprocess.run( - ["udevadm", "info", "-q", "property", "-n", f"/dev/{device_name}"], - capture_output=True, - text=True, - timeout=5 - ) - - info = {} - for line in result.stdout.strip().split('\n'): - if '=' in line: - key, value = line.split('=', 1) - info[key] = value - - return info - except Exception as e: - print(f"Fehler beim Auslesen von Device {device_name}: {e}") - return {} - - -def create_device_symlink(device_name: str, info: dict) -> str: - """Erstelle Symlink mit UUID/Serial-Nummer.""" - serial = info.get("ID_SERIAL", "unknown") - model = info.get("ID_MODEL", "unknown") - - symlink_name = f"{model}_{serial}".replace(" ", "_") - symlink_path = f"{DISC_DEVICE_PATH}/{symlink_name}" - - os.makedirs(DISC_DEVICE_PATH, exist_ok=True) - - # Alten Symlink entfernen falls vorhanden - if os.path.exists(symlink_path): - os.unlink(symlink_path) - - # Neuen Symlink erstellen - os.symlink(f"/dev/{device_name}", symlink_path) - - return symlink_path - - -def delete_device_symlink(device_name: str): - """Lösche Symlink beim Auswurf.""" - symlink_path = f"{DISC_DEVICE_PATH}/{device_name}" - if os.path.exists(symlink_path): - os.unlink(symlink_path) - - -def detect_disc_type(device_path: str) -> str: - """Erkenne Disc-Typ (CD/DVD/Blu-ray).""" - try: - result = subprocess.run( - ["isoinfo", "-d", "-i", device_path], - capture_output=True, - text=True, - timeout=10 - ) - - output = result.stdout.lower() - - if "rock ridge" in output or "joliet" in output: - if "blu-ray" in output: - return "bluray" - return "dvd" - elif "cda" in output: - return "cd" - - return "unknown" - except Exception: - return "unknown" - - -def create_job(device_path: str, disc_type: str): - """Erstelle Celery-Job für Disc-Erkennung.""" - try: - client = get_redis_client() - - job_data = { - "device_path": device_path, - "disc_type": disc_type, - "timestamp": str(int(subprocess.check_output(["date", "+%s"]).decode().strip())), - "status": "pending" - } - - client.lpush("rippy:jobs", json.dumps(job_data)) - print(f"Job erstellt: {disc_type} -> {device_path}") - - except Exception as e: - print(f"Fehler beim Erstellen des Jobs: {e}") - - -def handle_add(device_name: str): - """Handle Disc-Einwurf.""" - print(f"Disc-Einwurf erkannt: {device_name}") - - info = get_device_info(device_name) - if not info: - print("Konnte Device-Info nicht auslesen") - return - - symlink_path = create_device_symlink(device_name, info) - print(f"Symlink erstellt: {symlink_path}") - - disc_type = detect_disc_type(symlink_path) - print(f"Disc-Typ erkannt: {disc_type}") - - create_job(symlink_path, disc_type) - - -def handle_remove(device_name: str): - """Handle Disc-Auswurf.""" - print(f"Disc-Auswurf erkannt: {device_name}") - delete_device_symlink(device_name) - print(f"Symlink gelöscht: /dev/disc/{device_name}") - - -def main(): - """Hauptfunktion.""" - if len(sys.argv) < 3: - print("Usage: udev_daemon.py ") - sys.exit(1) - - action = sys.argv[1] - device = sys.argv[2] - - if action == "add": - handle_add(device) - elif action == "remove": - handle_remove(device) - else: - print(f"Unbekannte Aktion: {action}") - - -if __name__ == "__main__": - main()