# d4rk_media Eigener Medien-Dienst für alle FiveM-Ressourcen: Dateien annehmen, ablegen, ausliefern, in einem Dashboard verwalten. **Ein Container**, SQLite, Dateien auf einer Platte. Läuft unter **https://fivemanage.d4rkst3r.de** — Dashboard und API an der Wurzel, Dateien unter `/f/`. ``` vehicles/adder.webp → https://fivemanage.d4rkst3r.de/f/vehicles/adder.webp ``` **Der Pfad ist der Schlüssel.** `vehicles/adder.webp` bleibt `vehicles/adder.webp`, damit die Vorlage in einem Skript schlicht `{model}.webp` lauten kann. Die Adressen stehen nirgends in der Datenbank — sie werden bei jeder Antwort aus `PUBLIC_URL` gebaut. Ein Umzug auf einen anderen Namen ist eine geänderte Variable, kein Datenbankeingriff. | | | |---|---| | **Anleitung für Skripte** | [Wiki](https://git.d4rkst3r.de/D4rkst3r/d4rk_media/wiki) — Quelle ist `docs/` | | **Stand und Reihenfolge** | `ROADMAP.md` | | **Arbeitsweise** | `CLAUDE.md` — prüfen statt raten | --- ## Starten ```bash cp .env.example .env # PUBLIC_URL und ADMIN_PASSWORD ausfüllen docker compose up -d --build ``` Das war es. Ein Container, ein Volume, kein zweiter Dienst. **In Portainer:** Stacks → Add stack → Repository, `Compose path: docker-compose.yml`. Die Werte kommen als Stack-Variablen. > **`Compose path` nimmt genau EINE Datei.** Ergänzungen werden stillschweigend > übergangen — alle Container gesund, alles 502. Das hat beim Vorgänger einen > Nachmittag gekostet. ### An der Oberfläche arbeiten ```bash npm --prefix server/ui install npm --prefix server/ui run dev ``` Läuft auf `localhost:5173` und leitet `/api` und `/f` an den Container weiter. Dass das Sitzungs-Cookie dabei ankommt, liegt an einer Ausnahme: es trägt `Secure`, und ein Browser nimmt so eines über `http` **nur auf localhost** an. --- ## Was der Dienst kann **Hochladen** — über die Skript-API mit Token (drei Rumpfformen: multipart, Base64, rohe Bytes) oder aus dem Dashboard per Ziehen und Ablegen. Bilder, Videos, Ton, PDF. Ein **ZIP wird ausgepackt** statt abgelegt, und die Ordner darin bleiben erhalten — 3670 Bilder einzeln ins Fenster zu ziehen ist keine Bedienung. **Ausliefern** — mit ETag, `304`, Bereichsanfragen und `nosniff`. Vorschaubilder werden beim Upload erzeugt (320 px WebP): aus 7,5 MB je Galerieseite werden 766 KB. **Sparen, ohne Adressen zu ändern** — neben jedem PNG und JPEG liegt eine WebP-Fassung, ausgeliefert unter *derselben* Adresse je nach `Accept` des Aufrufers. Nachgemessen über den ganzen Bestand: **199 MB → 33 MB**, das sind 83 %. Für Lua ändert sich nichts — es schickt kein `Accept: image/webp` und bekommt weiterhin das Original. **Zurückholen** — Gelöschtes liegt 30 Tage im Papierkorb, danach räumt der Dienst selbst auf. Die Datei ist dann nur nicht mehr öffentlich, nicht weg. **Zählen** — wie oft eine Datei geholt wurde und wann zuletzt. Beantwortet die Frage, die ein Aufbewahrungsbericht sonst nicht beantworten kann: *braucht das noch jemand?* **Verwalten** — Galerie mit Ordnerbaum, Suche, Filter nach Medienart, Sortierung, Mehrfachauswahl, Verschieben, Löschen. Betrachter für Bild, Video, Ton und PDF ohne neuen Tab. **Nachvollziehen** — ein Verlauf: wer hat wann was abgelegt, ersetzt, verschoben oder gelöscht. **Anmelden** — über Discord (eine Rolle auf dem Server genügt) oder mit Passwort. Das Passwort bleibt als Notausgang. **Sichern** — täglich, geprüft, in die Nextcloud. Und der Dienst meldet nach Discord, wenn die Sicherung ausbleibt. --- ## Die eine Regel **Jeder Knopf gibt Rückmeldung, auch der erfolgreiche.** Das ist der Fehler, an dem der Fivemanage-Anlauf gescheitert ist: dessen `http-util.ts` warf jeden Fehlschlag als `new Error(…)`, die Hooks prüften auf `instanceof ApiError` — und damit verschwand **jeder** Fehler lautlos. `Create organization` gab weder Erfolg noch Misserfolg zurück; das Ergebnis waren neunzehn gleichnamige Organisationen. Deshalb hier: - In `server/ui/src/api.ts` steht der **einzige** `fetch` und **genau eine** Fehlerklasse. - In `store.ts` steht `run()`: Aktion rein, Erfolgstext rein — es meldet bei Erfolg diesen und bei Fehlschlag den Text **des Dienstes**. Wer darüber geht, kann keinen stillen Knopf bauen. - Jeder wartende Knopf ist gesperrt und zeigt einen Kreisel. - Fehlermeldungen bleiben stehen, bis jemand sie wegklickt. - Der Dienst hat eine Auffanglinie (`app.onError`): auch ein unerwarteter Fehler bekommt einen Satz statt eines nackten `Internal Server Error`. --- ## Betrieb ### Sicherung `tools/sichern.ps1`, täglich 04:30 als geplante Aufgabe. Die Datenbank wird **nicht kopiert**, sondern über SQLites eigene Sicherungsschnittstelle herausgeholt — im WAL-Modus liegt das Zuletzte noch nicht in `media.db`. Die Bilder kommen aus einem **nur lesend** eingehängten Volume dazu, alles in ein `tar.gz`. **Und sie prüft sich selbst:** auspacken, Datenbank öffnen, Medien, Token und Benutzer zählen, mit dem laufenden Dienst vergleichen. Danach geht sie per WebDAV in die Nextcloud und wird **von dort zurückgeholt und die Prüfsumme verglichen** — ein `PUT`, der 201 sagt, hat nichts bewiesen. Zugangsdaten und Aufbewahrung stehen im Panel unter *Einstellungen*. ### Wiki ```bash pwsh -File tools\wiki.ps1 ``` Trägt `docs/*.md` ins Gitea-Wiki. **Einseitig** — `docs/` ist die Quelle, das Wiki die Kopie. Wer dort tippt, verliert es beim nächsten Lauf. ### Zurückspielen ```bash tar xzf d4rk_media-JJJJ-MM-TT-HHMM.tar.gz ``` Ergibt `media.db` und `files/`. Beides ins Volume legen, während der Container steht. Vorschaubilder und WebP-Fassungen sind **nicht** im Archiv — beides ist abgeleitet und wird mit zwei Knöpfen unter *Speicher → Wartung* neu gerechnet. Der Papierkorb ist ebenfalls nicht drin: was dort liegt, ist gelöscht. --- ## Was hier bewusst fehlt **Logging.** Genau dafür schleppt Fivemanage ClickHouse mit, und genau da ist der Wartungsaufwand echt. Der Verlauf im Dashboard beantwortet „wer war das" — mehr braucht es hier nicht. **Mandanten, Rollen, Teams.** Ein Dienst, den eine Person betreibt. **Automatisches Löschen.** Vorbereitet ist der Bericht davor: wie viel ist älter als 7/30/90/365 Tage, was liegt doppelt. Eine Regel zu bauen, bevor man weiß, was da liegt, ist der Weg, wie man Daten verliert. **Spielerportal.** Setzt voraus, dass Spieler hochladen. Der richtige Weg wäre dann `presigned URLs` — kurzlebige signierte Adressen, mit denen ein Client hochlädt, ohne je einen Token zu sehen. --- ## Zwei Dinge, die Zeit gekostet haben **Der Proxy hielt Bilder fest.** NPMs `assets.conf` greift über eine Regex auf *jede* URL, die auf `.webp`, `.png`, `.js`, `.css` endet, legt die Antwort 30 Minuten weg und ignoriert dabei unsere Kopfzeilen. Ein überschriebenes Bild blieb damit eine halbe Stunde alt — und `/api/exists/…webp` wurde zwischengespeichert und danach **auch ohne Token** ausgeliefert. Behoben mit drei Blöcken in der *Custom Nginx Configuration* des Proxy-Hosts: ```nginx location ^~ /api/ { proxy_cache off; include conf.d/include/proxy.conf; } location ^~ /f/ { proxy_cache off; include conf.d/include/proxy.conf; } location ^~ /t/ { proxy_cache off; include conf.d/include/proxy.conf; } ``` Das `^~` ist der ganze Trick: eine gewöhnliche Präfix-Regel **verliert** gegen die Regex. In einem Wegwerf-Nginx nachgemessen. **Node 22 ist festgenagelt.** `better-sqlite3` 11.10.0 liefert fertige Binärdateien für ABI 115 (Node 20) und 127 (Node 22) — **Node 24 hat ABI 137 und fehlt**. Der Griff zur neuesten Version fällt auf `node-gyp` zurück und braucht python3, make und g++ im Abbild. --- ## Herkunft Der Vorgänger war **Fivemanage Lite** mit PostgreSQL, MinIO und ClickHouse. Der Stack lief; woran er gescheitert ist, steht oben unter *Die eine Regel* und ausführlich in `ROADMAP.md`. Von dem, was er über eine Dateiablage hinaus kann, haben wir nichts gebraucht und alles mitgeschleppt. Seine Compose-Dateien liegen in `legacy/`, als Beleg und wegen der Kommentare — nichts davon wird noch gestartet.