Auf dieser Maschine gibt es weder Docker noch WSL. Das Abbild ist also ungebaut und der Container nie gelaufen; das gehoert auf eine Maschine mit Docker, bevor jemand darauf baut. Was trotzdem geprueft ist: Node 22 ist festgenagelt, und das ist kein Zufall. better-sqlite3 11.10.0 liefert fertige Binaerdateien nur fuer ABI 108 (Node 20), 115, 127 (Node 22) und 131 -- Node 24 hat ABI 137 und fehlt. Der naheliegende Griff zur neuesten Version faellt damit auf node-gyp zurueck und braucht python3, make und g++ im Abbild. Aus den Veroeffentlichungen des Projekts abgelesen, nicht aus dem Gedaechtnis. node dist/index.js -- der Startbefehl des Abbilds -- laeuft und antwortet auf /health. Bis hierher war der Dienst nur je ueber tsx gestartet worden. Der Healthcheck gibt Exitcode 0 bei laufendem und 1 bei totem Dienst. Dazu eine Falle, die im Compose steht, weil sie teuer ist: NODE_ENV=production setzt das Sitzungs-Cookie auf Secure. Wer den Container ohne HTTPS aufruft, bekommt auf die Anmeldung 200 -- und ist trotzdem nicht angemeldet, weil der Browser das Cookie still verwirft. Die naechste Anfrage ist 401. Im Browser gegen eine LAN-Adresse nachgemessen. Die naheliegende Gegenprobe ueber localhost fuehrt in die Irre: das gilt Browsern als sicherer Kontext und funktioniert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
271 lines
13 KiB
Markdown
271 lines
13 KiB
Markdown
# d4rk_media — Roadmap
|
|
|
|
Eigener Medien-Dienst für alle FiveM-Ressourcen: Dateien annehmen, ablegen,
|
|
ausliefern, in einem Dashboard verwalten.
|
|
|
|
**Warum nicht Fivemanage Lite.** Der Stack lief am Ende — PostgreSQL, MinIO,
|
|
Dashboard, alles grün. Was an ihm nicht zu reparieren war, ist die Qualität
|
|
der Oberfläche: `http-util.ts` wirft jeden Fehler als `new Error(…)`, die
|
|
Hooks prüfen auf `instanceof ApiError`, und damit wird **jeder** Fehlschlag
|
|
lautlos verschluckt. `Create organization` gibt weder Erfolg noch Misserfolg
|
|
zurück — das Ergebnis waren 19 gleichnamige Organisationen, weil ein Knopf
|
|
ohne Rückmeldung wie ein kaputter Knopf aussieht. Löschen geht nicht, die
|
|
Route existiert nicht. Das ist kein Bedienfehler und keine fehlende Funktion,
|
|
das ist 0.1.0-beta.
|
|
|
|
Dazu kam: von dem, was Lite über eine Dateiablage hinaus kann — Mandanten,
|
|
strukturiertes Logging, ClickHouse — haben wir nichts gebraucht und alles
|
|
mitgeschleppt.
|
|
|
|
---
|
|
|
|
## Der Zuschnitt
|
|
|
|
**Ein Container.** SQLite statt PostgreSQL, Dateien auf einer Platte statt
|
|
MinIO. Das ist nicht die kleine Lösung, sondern die, die man nicht pflegt:
|
|
kein zweiter Dienst, kein Passwort dazwischen, und eine Sicherung ist ein
|
|
`cp -a` über ein Volume. Wenn daraus je Millionen Zeilen werden, wird
|
|
`server/src/db.ts` ausgetauscht — die Aufrufer merken davon nichts.
|
|
|
|
**Ein Name.** `fivecdn.d4rkst3r.de` und `fivemanage.d4rkst3r.de` fallen weg —
|
|
wir betreiben kein Fivemanage, also tragen unsere Adressen auch nicht dessen
|
|
Namen. Dashboard und API liegen an der Wurzel, die Dateien unter `/f/`:
|
|
|
|
```
|
|
https://media.d4rkst3r.de/ Dashboard und API
|
|
https://media.d4rkst3r.de/f/vehicles/adder.webp
|
|
```
|
|
|
|
Ein DNS-Eintrag, ein Host im Proxy. Aufgegeben ist damit eine Eigenschaft, die
|
|
zwei Namen mitbrächten: die öffentliche Adresse trägt jetzt auch das
|
|
Anmeldeformular.
|
|
|
|
Zwei Namen kann der Dienst weiterhin — `FILES_HOST` setzen, dann gibt es unter
|
|
diesem Namen ausschließlich Dateien und die URLs kommen ohne `/f/` aus. Beides
|
|
ist gemessen. Der Wechsel ist billig: die Adressen stehen **nirgends in der
|
|
Datenbank**, sie werden bei jeder Antwort aus `PUBLIC_URL` gebaut.
|
|
|
|
**Der Pfad bleibt in beiden Fällen der Schlüssel.** `vehicles/adder.webp`
|
|
bleibt `vehicles/adder.webp`, die Vorlage im Handy also `{model}.webp`. Nur der
|
|
Teil davor ändert sich, und der steht in einer Variablen.
|
|
|
|
**Der Pfad ist der Schlüssel.** `X-Path: vehicles/adder.webp` legt die Datei
|
|
genau dort ab. Vorhersagbare URLs sind für das Fotostudio die ganze
|
|
Voraussetzung — bei Lite hätten wir die zurückgegebenen Adressen in einer
|
|
`urls.json` mitschleppen müssen.
|
|
|
|
**Nicht nachgebaut wird das Logging.** Genau dafür schleppt Fivemanage
|
|
ClickHouse mit, und genau da ist der Wartungsaufwand echt. Wenn Logs kommen
|
|
sollen, ist das ein eigenes Projekt mit eigener Entscheidung.
|
|
|
|
---
|
|
|
|
## Stand
|
|
|
|
### ✅ Fertig — Server-Grundgerüst
|
|
|
|
```
|
|
server/src/config.ts Einstellungen, alle aus der Umgebung
|
|
server/src/db.ts SQLite: users, sessions, tokens, media
|
|
server/src/auth.ts scrypt, Sitzungen, Token-Hashes
|
|
server/src/storage.ts Pfadprüfung, atomares Schreiben, MIME
|
|
server/src/routes/upload.ts die API für Skripte
|
|
server/src/routes/dash.ts die API fürs Dashboard
|
|
server/src/index.ts Host-Weiche, Dateiauslieferung, Start
|
|
```
|
|
|
|
Die API für Skripte:
|
|
|
|
```
|
|
POST /api/upload Bearer-Token
|
|
X-Path vehicles/adder.webp (optional)
|
|
X-Encoding base64 (für Lua)
|
|
X-Overwrite false (Standard: ja)
|
|
→ { url, path, size, sha256, mime, replaced }
|
|
|
|
DELETE /api/media/<pfad> Token braucht can_delete
|
|
GET /api/exists/<pfad> für "nur fehlende" im Serienlauf
|
|
```
|
|
|
|
Drei Rumpfformen — `multipart/form-data`, Base64, rohe Bytes. Base64 ist für
|
|
FiveM: `PerformHttpRequest` schickt einen Lua-String, und im Fotostudio kommt
|
|
das Bild ohnehin als Data-URL aus dem NUI.
|
|
|
|
Die API fürs Dashboard: Anmeldung, Passwortwechsel, Medienliste mit Suche und
|
|
Paginierung, Einzel- und Sammellöschung, Statistik nach Ordnern,
|
|
Token-Verwaltung.
|
|
|
|
### ✅ Fertig — Schritt 1, der Smoke-Test
|
|
|
|
Gestartet, durchgemessen, repariert. Der Durchstich steht: anmelden → Token
|
|
anlegen → Bild hochladen → unter der zurückgegebenen URL abrufen, Byte für Byte
|
|
identisch, auch unter dem Dateihost.
|
|
|
|
**So läuft er lokal:**
|
|
|
|
```bash
|
|
cp server/.env.example server/.env
|
|
```
|
|
|
|
```bash
|
|
npm --prefix server ci && npm --prefix server run dev
|
|
```
|
|
|
|
> **Der Stolperstein, den es nicht mehr gibt.** Anfangs war der Standard für
|
|
> `FILES_HOST` der Host aus `PUBLIC_URL`. Bei zwei Namen ist das richtig, bei
|
|
> einem tödlich: dann galt *jede* Anfrage als Anfrage an den Dateiwirt,
|
|
> `/health` gab 404 und die Anmeldung 405 „hier gibt es nur Dateien". Nichts
|
|
> war kaputt, und niemand kommt darauf. Seit der Entscheidung für einen Namen
|
|
> ist der Ein-Namen-Betrieb der Standard und der zweite Name die Ansage —
|
|
> lokal läuft es damit ohne Sonderbehandlung. Welche Betriebsart gilt, sagt
|
|
> der Dienst in der zweiten Startzeile.
|
|
|
|
**Was der Test gefunden hat** — alles im Grundgerüst, alles repariert:
|
|
|
|
1. **Die Token-Prüfung hing an `'*'`** und galt damit auch für `/api/dash/*`,
|
|
das daneben liegt. Das Dashboard bekam „Token fehlt oder ist unbekannt" auf
|
|
die Anmeldung, obwohl es nie einen Token haben kann. Jetzt hängt sie an den
|
|
drei eigenen Pfaden.
|
|
2. **`DELETE /api/media/…` und `GET /api/exists/…` sahen am Ziel vorbei.**
|
|
`c.req.path` trägt den Einhängepunkt mit, das `replace(/^\/media\//)` schnitt
|
|
ihn nicht weg — aus `vehicles/adder.png` wurde `api/media/vehicles/adder.png`.
|
|
Löschen fand nie etwas, `exists` meldete immer `false`. Jetzt `:pfad{.+}`;
|
|
Hono liefert den Parameter fertig dekodiert (nachgemessen), ein zweites
|
|
`decodeURIComponent` wäre eine Dekodierung zu viel gewesen.
|
|
3. **Verzeichnisdurchstieg in der SPA-Rückfallroute.**
|
|
`GET /..%5Cpackage.json` hat unter Windows die Datei ausgeliefert: Hono
|
|
reicht `%5C` durch, `path.join` behandelt den Backslash dort als Trenner,
|
|
und eine Eindämmung gab es nicht. Unter Linux trägt genau dieser Angriff
|
|
nicht — Glück, keine Abwehr. Jetzt dieselbe `resolve`-Prüfung wie in
|
|
`storage.ts`.
|
|
4. **Die Auskunft „Oberfläche ist nicht gebaut" war unerreichbar.**
|
|
`createReadStream` meldet eine fehlende Datei *asynchron*; das `try/catch`
|
|
darum fing nichts. Ergebnis war ein leerer 200 samt ENOENT im Log. Jetzt
|
|
wird vorher nachgesehen.
|
|
5. **CSS und JS kamen als `application/octet-stream`** — die MIME-Tabelle kennt
|
|
nur Medientypen. Das Dashboard hätte weder Stylesheet noch Modul geladen.
|
|
Die Oberfläche hat jetzt eine **eigene** Tabelle: in der geteilten fehlt
|
|
`html` mit Absicht, sonst könnte jeder mit einem Upload-Token eine Seite
|
|
unter `fivecdn.d4rkst3r.de` veröffentlichen. Nachgemessen: eine hochgeladene
|
|
`.html` kommt weiterhin als `octet-stream` mit `nosniff`.
|
|
6. **Kaputtes JSON endete als nackter „Internal Server Error".** Ausgerechnet
|
|
das — ein 500 ohne ein Wort dazu ist der Fehler, wegen dem wir hier neu
|
|
bauen. Jetzt 400 mit Text.
|
|
|
|
**Was sonst nachgemessen wurde und trägt:** alle drei Rumpfformen (multipart,
|
|
Base64, rohe Bytes) liefern denselben SHA-256, auch mit `data:`-Vorspann · der
|
|
Hash-Pfad ohne `X-Path` · Präfix-Fessel und `can_delete` je Token · `..`,
|
|
versteckte Dateien und leerer Rumpf werden abgelehnt · `X-Overwrite: false`
|
|
gibt 409, Überschreiben `replaced: true` · 413 vor dem Einlesen · ETag/304,
|
|
HEAD, Bereichsanfragen und 416 · unter dem Dateihost gibt es weder API noch
|
|
`/f/`-Präfix · Medienliste, Suche, Statistik nach Ordnern.
|
|
|
|
Nicht getestet: Docker, das Dashboard (gibt es noch nicht), und echte Last.
|
|
|
|
### ✅ Fertig — ein Name statt zwei
|
|
|
|
`PUBLIC_URL` ist jetzt nur noch der Ursprung; das `/f` hängt der Dienst selbst
|
|
an. Wer es mitschriebe, bekäme Adressen mit `/f/f/` — und das fiele erst auf,
|
|
wenn das erste Bild fehlt. Prefix und Route werden deshalb an genau **einer**
|
|
Stelle entschieden (`config.filePrefix`), damit die zurückgegebene Adresse und
|
|
die Route, die sie ausliefert, nicht auseinanderlaufen können.
|
|
|
|
Beide Betriebsarten sind durchgemessen: mit einem Namen kommt
|
|
`…/f/vehicles/adder.png` zurück und liefert die Datei; mit gesetztem
|
|
`FILES_HOST` kommt sie ohne Präfix, liegt unter dem Dateihost, ist am
|
|
Dashboard-Namen zusätzlich unter `/f/` erreichbar, und unter dem Dateihost gibt
|
|
es weiterhin keine API.
|
|
|
|
### ⬜ Als Nächstes
|
|
|
|
**2 · Dockerfile und Compose — geschrieben, aber NIE GEBAUT.**
|
|
`server/Dockerfile` und `docker-compose.media.yml` stehen. Auf der Maschine,
|
|
auf der sie entstanden sind, gibt es weder Docker noch WSL — das Abbild ist
|
|
also ungebaut und der Container nie gelaufen. Das ist der nächste Handgriff,
|
|
und er gehört auf eine Maschine mit Docker.
|
|
|
|
Was daran trotzdem geprüft ist:
|
|
|
|
- **Node 22 ist festgenagelt, mit Grund.** `better-sqlite3` 11.10.0 liefert
|
|
fertige Binärdateien nur für ABI 108 (Node 20), 115, 127 (Node 22) und 131.
|
|
**Node 24 hat ABI 137 und fehlt** — der naheliegende Griff zur neuesten
|
|
Version fällt auf `node-gyp` zurück und braucht python3, make und g++ im
|
|
Abbild. Aus den Veröffentlichungen des Projekts abgelesen, nicht geraten.
|
|
- **`node dist/index.js`** — der Startbefehl des Abbilds — läuft und antwortet
|
|
auf `/health`. Bis dahin war der Dienst nur je über `tsx` gestartet.
|
|
- **Der Healthcheck** gibt Exitcode 0 bei laufendem und 1 bei totem Dienst.
|
|
|
|
Ungeprüft bleibt alles, wofür es Docker braucht: ob `npm ci` im Abbild die
|
|
Binärdatei zieht, ob das Volume greift, ob der Bau überhaupt durchläuft.
|
|
|
|
> **Beim Ausprobieren:** `NODE_ENV=production` setzt das Sitzungs-Cookie auf
|
|
> `Secure`. Wer den Container ohne HTTPS aufruft, bekommt auf die Anmeldung
|
|
> **200 — und ist trotzdem nicht angemeldet**, weil der Browser das Cookie
|
|
> still verwirft; die nächste Anfrage ist 401. Im Browser gegen eine
|
|
> LAN-Adresse nachgemessen. Die naheliegende Gegenprobe über `localhost`
|
|
> führt in die Irre: das gilt Browsern als sicherer Kontext und funktioniert.
|
|
|
|
In Portainer aus diesem Repo deploybar — **`Compose path` nimmt EINE Datei**,
|
|
Ergänzungen werden stillschweigend ignoriert. Das hat uns bei Lite einen
|
|
Nachmittag gekostet. Host-Port ist 9102, solange der Lite-Stack 9100/9101
|
|
belegt.
|
|
|
|
**3 · `server/upload.lua` im Fotostudio.** `Upload.put(pfad, bytes) → url`.
|
|
Token in `config.upload.lua`, server-only, gitignored, mit committetem
|
|
`.example` — nie im NUI, dort hat es jeder Spieler im Speicher. An den
|
|
Serienlauf hängen, damit freigestellte Bilder nach der Freigabe sofort
|
|
hochgehen.
|
|
|
|
Nach diesem Schritt ist das erreicht, worum es ursprünglich ging.
|
|
|
|
**4 · Dashboard.** React 18, TypeScript, Vite 5, Tailwind 4 (CSS-first),
|
|
Lucide, Zustand. **Kein Konsta** — das ist für die Handy-Oberfläche richtig,
|
|
für ein Dashboard am Monitor nicht.
|
|
|
|
Anmeldung · Galerie mit Vorschau, Suche, Mehrfachauswahl · Löschen ·
|
|
Speicherverbrauch nach Ordnern · Token-Verwaltung, Klartext genau einmal.
|
|
|
|
> **Die eine Regel:** jeder Knopf gibt Rückmeldung, auch der erfolgreiche.
|
|
> Das ist der Fehler, an dem wir bei Fivemanage einen Nachmittag verloren
|
|
> haben, und er ist billig zu vermeiden.
|
|
|
|
**5 · Umzug.** Root-`docker-compose.yml` wird unsere; die Fivemanage-Dateien
|
|
wandern nach `legacy/`. Erst wenn unserer trägt — bis dahin bleibt der
|
|
Lite-Stack als Vergleichsmaßstab laufen.
|
|
|
|
### ⬜ Später, wenn es sich lohnt
|
|
|
|
- Vorschaubilder serverseitig (spart dem Dashboard das Laden von 900 Vollbildern)
|
|
- Namensräume je Ressource, mit eigenem Token und eigener Statistik
|
|
- Spieler-Metadaten an einem Upload (wer, wann, welcher Server)
|
|
- Aufräumregeln: was älter als X ist und zu keinem Datensatz gehört
|
|
- Mehrere Benutzer statt nur `admin`
|
|
|
|
---
|
|
|
|
## Offene Entscheidungen
|
|
|
|
- **Sicherung.** Ein Volume mit Bildern und `media.db`. Restic gegen die
|
|
Nextcloud? Oder reicht ein `docker cp` vor größeren Änderungen?
|
|
- **Bilder aus dem Spiel** (Screenshots, Clips von Spielern) — dafür brauchte
|
|
es Größenbegrenzungen je Token und vermutlich eine Warteschlange. Erst
|
|
planen, wenn es ansteht.
|
|
|
|
---
|
|
|
|
## Was aus dem Fivemanage-Anlauf hängen bleibt
|
|
|
|
Steht ausführlich im README, hier nur die Kurzfassung — es sind alles Dinge,
|
|
die uns wieder begegnen:
|
|
|
|
1. **Portainer nimmt unter `Compose path` eine Datei.** Ergänzungen werden
|
|
stillschweigend übergangen: alle Container gesund, alles 502.
|
|
2. **Host-Ports an `0.0.0.0` binden, nicht an `127.0.0.1`.** NPM liegt im
|
|
Standard-Bridge-Netz und kommt über die Docker-Brücke, nicht über Loopback.
|
|
Zu macht der Port die Firewall.
|
|
3. **Belegt auf diesem Server:** 80, 81, 443, 2224, 3000, 3080, 3478, 8000,
|
|
8080, 8090, 9000, 9443, 11000. Unsere: 9100 und 9101.
|
|
4. **`9101:8080` ist keine Kollision.** Nur die linke Zahl muss einmalig sein.
|
|
5. **Ein Dienst, der ohne Konfiguration hochfährt, kostet mehr Zeit als
|
|
einer, der sich weigert.** Deshalb bricht `config.ts` beim Start ab.
|