UMBENENNEN gab es gar nicht -- verschieben ja, umbenennen nirgends. Jetzt fuer
Datei und Ordner. Die WARNUNG ist dabei der eigentliche Teil: beim Verschieben
wandert eine Datei, beim Umbenennen aendert sich IHRE ADRESSE, und die steht
womoeglich in einem Skript, das niemand mehr im Kopf hat. Der Dialog zeigt
deshalb die Abrufzahl und beim Ordner die Zahl der betroffenen Dateien, BEVOR
gedrueckt wird -- eine Datei mit viertausend Abrufen umzubenennen ist etwas
anderes als eine mit null, und der Dienst ist die einzige Stelle, die den
Unterschied kennt.
Am echten Fall geprueft, dem Kollisionsfund: items/coiloverss.png (hielt die
+-Fassung) -> items/coiloverss-plus.png, alte Adresse 404, neue 200 mit 4375 B,
Vorschau mitgewandert. Ordner: probe -> beispiele, 5 Dateien, alle Pfade in
einer Transaktion umgeschrieben.
Ein Fallstrick dabei: thumbPath haengt ".webp" an. Fuer eine Datei richtig, fuer
einen ORDNER Unsinn -- der Vorschauordner heisst thumbs/vehicles und nicht
thumbs/vehicles.webp. Dafuer gibt es jetzt moveFolder.
DIE 93 KOLLISIONEN SIND ERLEDIGT. tools/kollisionen.ps1 rechnet dieselbe
Zaehmung auf der Quelle nach und zeigt, was zusammenfaellt -- es aendert nichts,
damit die Entscheidung auf Zahlen steht. Ergebnis: 91 von 93 sind dieselbe Datei
zweimal (WEAPON_SMG.png und weapon_smg.png, byteweise gleich gross), da fehlte
nichts. Echt verloren waren ZWEI, beide mit + im Namen. Beide nachgeholt.
FIVEMANAGE-SPRACHE. POST /api/image|video|audio (v1) und POST /api/v3/file (v3),
Schluessel nackt im Authorization-Kopf. Auf einem laufenden Server stecken die
Fivemanage-Aufrufe in einem Dutzend fremder Ressourcen; sie alle umzuschreiben
tut niemand, und deshalb bliebe dieser Dienst ungenutzt daneben stehen. So ist
der Umzug eine Zeile je Skript: die Adresse.
Beide Formen sind AUS ECHTEM CODE abgelesen und nicht geraten -- fivemanage/sdk
fuer v3, Awleks/Devm-Camera fuer den aelteren Weg ueber screenshot-basic.
Gemessen: v1 -> {url,id,path}, v3 -> {status:"ok",data:{id,url}}, ohne
Schluessel 401, ein Video an /api/image -> 415 mit Grund.
Dabei bin ich in eine Falle gelaufen, vor der im eigenen Repo ein Kommentar
warnt: die Token-Wache hing an use('*'), und der Einhaengepunkt ist /api -- also
galt sie auch fuer /api/dash daneben. Das Dashboard bekam 401 auf die ANMELDUNG.
Wortwoertlich derselbe Fehler steht in upload.ts als Kommentar, weil er dort
schon einmal passiert ist. Gemerkt hat es der Gegentest, nicht der Kopf.
FREIGABE-LINKS. /s/<schluessel> zeigt einen Ordner ohne Anmeldung. Was dabei
ausdruecklich dabeisteht, in der Karte und in der Rueckfrage vor dem
Zurueckziehen: FREIGEGEBEN WIRD DIE LISTE, NICHT DER INHALT. Die Dateien sind
ohnehin oeffentlich; ein zurueckgezogener Link macht sie nicht wieder privat, er
nimmt nur die Uebersicht weg. Ohne diesen Satz zieht jemand einen Link zurueck
und glaubt, etwas sei verschwunden.
Die Antwort ist abgemessen: Name, Groesse, Art, Adresse, Laenge. NICHT Hash,
Token, Zeitpunkte, Abrufzahlen, IDs -- nichts davon braucht, wer einen Katalog
ansieht, und jedes davon waere eine Auskunft ueber den Betrieb. Ein unbekannter
Schluessel und ein zurueckgezogener geben dieselbe Antwort.
ZAHLEN, DIE SCHON DA WAREN. Sortieren nach Abrufen und nach "zuletzt geholt";
die Kachel zeigt dann auch diese Zahl statt Groesse und Datum, denn nach etwas
zu ordnen, das man nirgends sieht, ist eine Reihenfolge ohne Begruendung. Und je
Token, was damit abgelegt wurde: media.token_id wird seit dem ersten Tag
geschrieben und war NIRGENDS zu sehen. Gemessen: d4rk_photostudio haelt 810
Dateien / 99,5 MB, 3640 liegen ohne Token da.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
789 lines
40 KiB
Markdown
789 lines
40 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.** Dashboard und API liegen an der Wurzel, die Dateien unter `/f/`:
|
||
|
||
```
|
||
https://fivemanage.d4rkst3r.de/ Dashboard und API
|
||
https://fivemanage.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.
|
||
|
||
> **Und der Name ist `fivemanage`, obwohl hier lange das Gegenteil stand.**
|
||
> Der ursprüngliche Satz war: `fivecdn` und `fivemanage` fallen weg, wir
|
||
> betreiben kein Fivemanage und tragen deshalb nicht dessen Namen. Das Argument
|
||
> stimmt immer noch — es ist nur billiger als das, was dagegen steht, und das
|
||
> ist nachgemessen: der Name **war schon fertig verkabelt**. DNS-Eintrag da,
|
||
> Host in NPM da, Let's-Encrypt-Zertifikat gültig bis 09.11.2026, und er zeigte
|
||
> auf `host.docker.internal:9101` — einen Port, der frei wurde, als die
|
||
> Lite-Container gelöscht wurden. Unser Stack auf 9101 heißt: **kein
|
||
> DNS-Eintrag, kein Klick in NPM, kein neues Zertifikat.**
|
||
>
|
||
> Der Preis ist ein Name, der nach einem fremden Produkt klingt. Der Wechsel
|
||
> bleibt billig — die Adressen stehen nirgends in der Datenbank, sie werden bei
|
||
> jeder Antwort aus `PUBLIC_URL` gebaut. Ein späterer Umzug auf
|
||
> `media.d4rkst3r.de` ist ein DNS-Eintrag, ein NPM-Host und eine geänderte
|
||
> Variable.
|
||
|
||
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: 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.
|
||
|
||
### ✅ Fertig — Schritt 2, das Abbild ist gebaut und gelaufen
|
||
|
||
Gebaut, gestartet, durchgemessen — auf einer Maschine mit Docker (Desktop
|
||
29.6.2, WSL2). Derselbe Durchstich wie in Schritt 1, diesmal gegen den
|
||
**Container** statt gegen `tsx`: 24 Prüfungen, keine durchgefallen. Am
|
||
Grundgerüst war nichts zu ändern; der Bau hat keinen einzigen Befund erzeugt.
|
||
|
||
**So läuft er:**
|
||
|
||
```bash
|
||
export PUBLIC_URL=http://localhost:9102 ADMIN_PASSWORD=…
|
||
docker compose up -d --build
|
||
```
|
||
|
||
**Was jetzt gemessen ist und vorher nur behauptet war:**
|
||
|
||
- **`npm ci --omit=dev` zieht die fertige Binärdatei.** Im Abbild liegt
|
||
`better-sqlite3/build/Release/better_sqlite3.node`, datiert Mai 2025 —
|
||
heruntergeladen, nicht übersetzt; ein `Makefile` oder `obj.target` von
|
||
`node-gyp` steht nirgends daneben. `require('better-sqlite3')` läuft im
|
||
Laufzeit-Abbild, Node 22.23.2, ABI 127. **Die Festnagelung trägt.**
|
||
- **Der Bau läuft durch**, alle drei Stufen. Das Abbild: 373 MB auf der
|
||
Platte, 88,4 MB Inhalt.
|
||
- **Der Healthcheck greift.** Docker meldet `healthy`, Exitcode 0, kein
|
||
Fehlschlag — ohne `curl` im Abbild, `fetch` aus Node reicht.
|
||
- **Das Volume greift.** Container weggeworfen, einen neuen an dasselbe
|
||
Volume gehängt: die Datei aus dem ersten kommt weiter unter ihrer URL, und
|
||
`admin` wird nicht noch einmal angelegt. Nebenbei bestätigt, was im Compose
|
||
steht: ein geändertes `ADMIN_PASSWORD` setzt nichts zurück — die alte
|
||
Anmeldung gilt (200), die neue nicht (401).
|
||
- **Compose bricht ohne Variablen ab**, wie das `:?` verspricht, und zwar mit
|
||
dem Text, der danebensteht — erst `PUBLIC_URL`, dann `ADMIN_PASSWORD`. Mit
|
||
beiden fährt der Stack hoch und bindet `0.0.0.0:9102->8080`.
|
||
- **`Secure` steht wirklich im Cookie**, wenn `NODE_ENV=production` gilt:
|
||
`sid=…; Path=/; HttpOnly; Secure; SameSite=Lax`. Ohne Cookie oder mit einem
|
||
erfundenen gibt jede Dashboard-Route 401 — auch `/auth/me`.
|
||
|
||
**Neu aufgefallen, weil der Container es sichtbar macht:** im Volume liegen
|
||
**vier** Dinge, nicht zwei — `files/`, `media.db`, `media.db-shm` und
|
||
`media.db-wal`. SQLite läuft im WAL-Modus (`db.ts`), und das WAL war nach
|
||
wenigen Uploads 152 KB groß, mit Daten, die in `media.db` noch nicht standen.
|
||
Eine Sicherung, die nur `media.db` mitnimmt, ist deshalb unvollständig. Siehe
|
||
*Offene Entscheidungen*.
|
||
|
||
> **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.
|
||
> Und `curl` taugt hier nicht als Zeuge — der schickt ein Secure-Cookie auch
|
||
> über `http` und meldet fröhlich Erfolg. Was zählt, ist der Browser.
|
||
|
||
In Portainer aus diesem Repo deploybar — **`Compose path` nimmt EINE Datei**,
|
||
Ergänzungen werden stillschweigend ignoriert. Das hat uns bei Lite einen
|
||
Nachmittag gekostet.
|
||
|
||
> **Und noch eine Annahme, die beim Nachsehen fiel: es gibt gar keine zweite
|
||
> Maschine.** Gebaut und gelaufen ist auf **dem Server** — hier laufen Gitea,
|
||
> Portainer, NPM und die Nextcloud. Die alte Notiz „auf dieser Maschine gibt
|
||
> es weder Docker noch WSL" galt vor der Installation von Docker Desktop und
|
||
> ist überholt. Was zum Betrieb fehlt, ist deshalb nicht der Umzug auf einen
|
||
> anderen Rechner, sondern nur der **öffentliche Auftritt**: DNS-Eintrag für
|
||
> `media.d4rkst3r.de`, ein Host in NPM mit TLS, ein echtes Passwort.
|
||
|
||
Gemessen am 11.08.2026 belegt: 80, 81, 443, 2224, 3000, 3080, 3478, 8000,
|
||
8080, **8443**, 8090, 9000, 9443, 11000 — 8443 fehlte in der alten Liste.
|
||
**Der Host-Port ist 9101**, nicht die 9102 aus dem Compose-Standard: darauf
|
||
zeigt der fertige NPM-Host (siehe oben). Gesetzt wird er über `HOST_PORT` in
|
||
der `.env`, der Standard im Compose bleibt 9102.
|
||
|
||
### ✅ Fertig — der Dienst steht öffentlich
|
||
|
||
Seit dem 11.08.2026 läuft er unter **`https://fivemanage.d4rkst3r.de`**, hinter
|
||
NPM mit Let's-Encrypt-Zertifikat, Container auf 9101. Die Werte stehen in einer
|
||
`.env` neben dem Compose (gitignored); dieselben Werte sind in Portainer die
|
||
Stack-Variablen.
|
||
|
||
Durchgemessen über die öffentliche Adresse, nicht über localhost:
|
||
|
||
- Anmeldung durch echtes HTTPS — **das ist der Fall, den `curl` bisher nicht
|
||
bezeugen konnte**: `Secure` am Cookie stört jetzt nicht mehr, weil die
|
||
Verbindung wirklich TLS ist. 200, Cookie da, `/auth/me` 200.
|
||
- Token mit Präfix-Fessel `vehicles/` und ohne Löschrecht angelegt. Upload nach
|
||
`anderswo/` → **403 mit Text**, Löschen → **403 mit Text**.
|
||
- Upload und Abruf über TLS und Proxy: **Byte für Byte identisch.**
|
||
|
||
> **Der 502, der keiner war.** Der erste Aufruf gab 502 in 78 ms. Die
|
||
> naheliegende Erklärung — „ein Moment, gleich geht's" — war falsch: der
|
||
> NPM-Host zeigte in dem Moment auf 9102, unser Container liegt auf 9101, also
|
||
> sofortige Ablehnung. Kein Timeout, keine DNS-Frage, kein Zufall. Ein 502
|
||
> binnen Millisekunden ist immer ein *Connection refused*, und dann stimmt eine
|
||
> Portnummer nicht.
|
||
|
||
### ✅ Fertig — Schritt 3, der Fotostudio lädt selbst hoch
|
||
|
||
**Am 11.08.2026 im Spiel gelaufen.** `/studio shot adder`, und die Konsole sagte
|
||
in zwei Zeilen, was passiert ist:
|
||
|
||
```
|
||
[photostudio] Upload nach https://fivemanage.d4rkst3r.de — Dienst antwortet.
|
||
[photostudio] adder freigestellt abgelegt (129 KB)
|
||
[photostudio] adder hochgeladen: https://fivemanage.d4rkst3r.de/f/vehicles/adder.webp
|
||
```
|
||
|
||
Von der Dienstseite gegengeprüft, statt der Logzeile zu glauben: 132 908 Bytes
|
||
hier wie dort, **derselbe SHA-256** (`f14dfc5b…5515e`), ausgeliefert als
|
||
`image/webp`, und die ersten Bytes sind `RIFF….WEBP` — ein echtes WebP und
|
||
kein falsch benanntes PNG.
|
||
|
||
Damit ist auch die letzte offene Frage aus dem Prüfstand beantwortet:
|
||
`PerformHttpRequest` verhält sich in FiveM wie die Fälschung, Kopfzeilen und
|
||
base64-Rumpf kommen unverändert an, und der Rückruf erreicht den
|
||
Event-Handler.
|
||
|
||
**Was noch niemand gemessen hat:** ein ganzer Serienlauf über 900 Fahrzeuge —
|
||
also 900 Uploads hintereinander, mit dem, was dabei an Fristen und
|
||
Gleichzeitigkeit auftreten kann. Und `Upload.put`, die wartende Form, ist
|
||
ungenutzt: der Lauf nimmt `Upload.send`. Sie steht da, weil die ROADMAP sie so
|
||
benannt hat, und ist damit ungeprüfter Code.
|
||
|
||
Der Quelltext liegt in `d4rk_photostudio` (eigenes Repo, `D:\FXServer\txData\…`):
|
||
|
||
```
|
||
server/upload.lua Upload.send(pfad, bytes, cb) und Upload.put(pfad, bytes) → url
|
||
config.upload.example.lua committet
|
||
config.upload.lua gitignored, enthaelt den Token
|
||
```
|
||
|
||
Hängt an `d4rk_photostudio:saveKeyed` — also genau an der Freigabe: das Bild
|
||
geht **erst auf die Platte, dann in den Dienst**, und ein Fehlschlag bricht
|
||
nichts ab, sondern landet im Protokoll des Laufs. Ein Lauf über 900 Fahrzeuge,
|
||
der an einem Netzhuster stirbt, wäre der teuerste denkbare Fehler.
|
||
|
||
Gemessen ist alles, was sich ohne FiveM messen lässt — und das ist mehr, als es
|
||
zunächst schien: `upload.lua` wurde in einer **echten Lua-5.3-VM** (fengari,
|
||
über Node) mit gefälschten FiveM-Funktionen ausgeführt. 30 Prüfungen, keine
|
||
durchgefallen: fehlende, kaputte und unvollständige `config.upload.lua`;
|
||
Adresse, Methode und alle vier Kopfzeilen; der Fehlertext des Dienstes statt
|
||
einer nackten Zahl; Status 0; und dass die Frist den Rückruf nicht ein zweites
|
||
Mal auslöst. Der Rumpf, den die VM erzeugt hat, ging danach **unverändert an
|
||
den echten Dienst** — hochgeladen, abgerufen, Byte für Byte identisch.
|
||
|
||
**Damit ist erreicht, worum es ursprünglich ging.** Ein Bild entsteht im Spiel,
|
||
wird freigestellt, landet auf der Platte und liegt Sekunden später unter einer
|
||
vorhersagbaren Adresse — ohne Fivemanage, ohne PostgreSQL, ohne MinIO, ohne
|
||
einen einzigen Handgriff von Hand dazwischen.
|
||
|
||
### ✅ Fertig — Schritt 4, das Dashboard
|
||
|
||
Liegt in `server/ui`, gebaut nach `server/web` (gitignored), im Abbild von
|
||
einer eigenen Docker-Stufe. React 18, TypeScript, Vite 5, Tailwind 4
|
||
(CSS-first), Lucide, Zustand — **kein Konsta**, wie geplant.
|
||
|
||
Anmeldung · Galerie mit Vorschau, Suche, Mehrfachauswahl, Blättern · Einzel-
|
||
und Sammellöschung mit Nachfrage, die die Pfade zeigt · Speicherverbrauch nach
|
||
Ordnern · Token-Verwaltung mit Klartext genau einmal · Passwortwechsel.
|
||
|
||
> **Vite 5 bleibt, aber `@vitejs/plugin-react` ist auf `^4` festgenagelt.**
|
||
> Die 6 verlangt `vite ^8`. In den `peerDependencies` nachgesehen, nicht
|
||
> geraten: `@tailwindcss/vite@4` nimmt `^5.2 || ^6 || ^7 || ^8`, und
|
||
> `plugin-react@4` nimmt `^4.2 || ^5 || ^6 || ^7`. Vite 5.4.21 liegt in beiden
|
||
> Schnittmengen.
|
||
|
||
**Wie die eine Regel durchgesetzt wird, und nicht nur gemeint ist.** In
|
||
`api.ts` steht der einzige `fetch` und genau eine Fehlerklasse — der
|
||
Fivemanage-Fehler war, zwei zu haben (`new Error` geworfen, `instanceof
|
||
ApiError` geprüft, jeder Fehlschlag verschluckt). In `store.ts` steht `run()`:
|
||
es nimmt die Aktion und den Erfolgstext, meldet bei Erfolg diesen und bei
|
||
Fehlschlag den Text **des Dienstes**. Wer darüber geht, kann keinen stillen
|
||
Knopf bauen. Jeder Knopf, der wartet, zeigt einen Kreisel und ist gesperrt —
|
||
der zweite Klick eine Sekunde später war der Weg zu neunzehn gleichnamigen
|
||
Organisationen.
|
||
|
||
Nachgemessen am laufenden Container: `/` liefert die Oberfläche als
|
||
`text/html`, `assets/*.js` als `text/javascript`, `assets/*.css` als
|
||
`text/css` (die MIME-Falle aus Schritt 1 trägt also), und `/tokens`,
|
||
`/speicher`, `/konto` geben dieselbe `index.html` zurück — die
|
||
SPA-Rückfallroute tut, wofür sie gebaut wurde.
|
||
|
||
### ⚠️ Der Proxy hält Bilder fest — 30 Minuten lang
|
||
|
||
**Beim Nachmessen der Zwischenspeicher-Kopfzeilen gefunden, und es trifft
|
||
genau den Fotostudio-Ablauf.** NPMs `assets.conf` greift über eine Regex auf
|
||
*jede* URL, die auf `.css .js .png .webp …` endet — also auch auf
|
||
`/f/vehicles/adder.webp`. Sie setzt `proxy_cache public-cache`,
|
||
`proxy_cache_valid any 30m` und wirft unsere Kopfzeilen weg
|
||
(`proxy_ignore_headers Cache-Control`, `proxy_hide_header Cache-Control`).
|
||
|
||
Reproduziert, nicht vermutet:
|
||
|
||
```
|
||
Bild hochladen → abrufen: "ERSTE-FASSUNG"
|
||
dieselbe Datei neu → abrufen über NPM: "ERSTE-FASSUNG" ← alt
|
||
abrufen am Dienst: "ZWEITE-FASSUNG" ← neu
|
||
```
|
||
|
||
**Ein neu fotografiertes Fahrzeug sieht bis zu 30 Minuten lang aus wie das
|
||
alte**, obwohl Upload, Protokoll und Datenbank alle „fertig" sagen. Das ist
|
||
die Sorte Fehler, die man drei Ebenen tiefer sucht.
|
||
|
||
**Behoben am 11.08.2026** und nachgemessen: die Datei trägt jetzt wieder
|
||
unsere eigene Kopfzeile (`cache-control: public, max-age=300`, kein `Expires`
|
||
von NPM mehr), und ein überschriebenes Bild kommt **sofort** durch. Der
|
||
Schnipsel steht im Feld *Custom Nginx Configuration* des Proxy-Hosts — NPM
|
||
setzt ihn dort innerhalb des `server`-Blocks ein (`{{ advanced_config }}`,
|
||
Zeile 28 von 52 in der Vorlage), eine `location` ist dort also gültig:
|
||
|
||
```nginx
|
||
# Dateien nicht zwischenspeichern -- sie koennen sich unter derselben
|
||
# Adresse aendern. Das ^~ ist der ganze Trick: eine gewoehnliche
|
||
# Praefix-Location VERLIERT gegen die Regex aus assets.conf.
|
||
location ^~ /f/ {
|
||
proxy_cache off;
|
||
include conf.d/include/proxy.conf;
|
||
}
|
||
```
|
||
|
||
Dass `^~` nötig ist und ein blankes `location /f/` nicht reicht, ist in einem
|
||
Wegwerf-nginx nachgemessen: ohne `^~` gewinnt die Regex, mit `^~` das Präfix.
|
||
|
||
### ✅ Fertig — Sicherung, Anmeldebremse, Vorschaubilder
|
||
|
||
**Sicherung.** `tools/sichern.ps1`, täglich um 04:30 als geplante Aufgabe
|
||
(nur bei angemeldetem Benutzer — Docker Desktop läuft ohnehin nur dann).
|
||
|
||
Die Datenbank wird **nicht kopiert**, sondern über SQLites eigene
|
||
Sicherungsschnittstelle herausgeholt (`server/src/backup.ts`, `db.backup()`).
|
||
Der Grund steht weiter oben: im WAL-Modus liegt das Zuletzte noch nicht in
|
||
`media.db`, und selbst alle drei Dateien zu kopieren ist nicht sicher, wenn
|
||
währenddessen geschrieben wird. Die Bilder kommen aus einem **nur lesend**
|
||
eingehängten Volume dazu, alles in ein `tar.gz`.
|
||
|
||
**Und die Sicherung prüft sich selbst:** sie wird ausgepackt, die Datenbank
|
||
geöffnet, Medien, Token und Benutzer gezählt und mit dem laufenden Dienst
|
||
verglichen. Stimmt es nicht, endet das Skript mit Fehler.
|
||
|
||
**Einmal wirklich zurückgespielt**, nicht nur behauptet: in ein leeres
|
||
Wegwerf-Volume ausgepackt, ein zweiter Dienst darauf gestartet, mit dem echten
|
||
Passwort angemeldet, 21 Dateien vorgefunden, ein Bild abgerufen — Byte für Byte
|
||
identisch mit dem Betrieb.
|
||
|
||
Nicht mitgesichert werden die Vorschaubilder: sie sind abgeleitet und lassen
|
||
sich mit einem Knopf neu rechnen.
|
||
|
||
**Anmeldebremse.** Das Formular steht öffentlich; `scrypt` macht einen Versuch
|
||
teuer, aber teuer ist nicht selten. Jetzt: fünf freie Versuche je Adresse, dann
|
||
Sperre ab 30 Sekunden mit Verdopplung bis 15 Minuten, `429` samt `Retry-After`
|
||
und einem Text, der sagt, wie lange noch. Ein Erfolg setzt den Zähler zurück.
|
||
Bewusst nur im Speicher — wer sich aussperrt, startet den Container neu.
|
||
|
||
Durchgemessen: Versuche 1–5 geben 401, ab dem sechsten 429 mit „noch 30
|
||
Sekunden warten", nach Ablauf geht das richtige Passwort, und danach gibt ein
|
||
Fehlversuch wieder 401 statt 429.
|
||
|
||
> Die Adresse kommt aus `X-Real-IP`, den NPM mit `$remote_addr`
|
||
> **überschreibt**. Wer den Container direkt auf seinem Port erreicht, kann sie
|
||
> sich ausdenken — genau deshalb gehört 9101 hinter die Firewall.
|
||
|
||
**Vorschaubilder.** `sharp` erzeugt beim Upload eine 320 Pixel breite
|
||
WebP-Fassung unter `/data/thumbs`, ausgeliefert unter `/t/<pfad>`. Gemessen an
|
||
`vehicles/sultan.webp`: **131 502 → 13 078 Bytes, Faktor 10.** Eine
|
||
Galerieseite mit 60 Kacheln fällt damit von 7,5 MB auf 766 KB.
|
||
|
||
Keine Spalte in der Datenbank: ob es eine Vorschau gibt, sagt das Dateisystem,
|
||
und die Galerie fällt bei 404 auf das Vollbild zurück. Eine zweite Wahrheit,
|
||
die auseinanderlaufen kann, gibt es damit gar nicht erst. Was vor dieser
|
||
Funktion hochgeladen wurde, zieht ein Knopf unter *Speicher* nach — er nennt
|
||
Zahlen, nicht nur „fertig" (21 erzeugt, 0 Fehler beim ersten Lauf).
|
||
|
||
Das Abbild wächst dadurch von 373 auf 457 MB. `sharp` bringt fertige
|
||
Binärdateien für `linux-x64` mit; im Abbild nachgemessen: libvips 8.18.3,
|
||
ein 800×600-PNG wird zu einem 320er WebP.
|
||
|
||
> **Nebenbefund beim Einbauen:** `npm i sharp` scheitert auf diesem Rechner —
|
||
> aber nicht an sharp, sondern an **better-sqlite3**. Der Host läuft auf Node
|
||
> 24 (ABI 137), und dafür gibt es keine fertige Binärdatei; `node-gyp`
|
||
> übernimmt und findet keine Bauwerkzeuge. Genau die Falle, die im Dockerfile
|
||
> steht. Umgehung: `npm i sharp --package-lock-only` trägt es nur ein, gebaut
|
||
> wird im Abbild auf Node 22.
|
||
|
||
### ✅ Fertig — Schritt 5, der Umzug
|
||
|
||
Die Fivemanage-Dateien liegen in `legacy/` mit einer Erklärung daneben; unsere
|
||
`docker-compose.media.yml` heißt jetzt schlicht **`docker-compose.yml`** (in
|
||
Portainer also `Compose path: docker-compose.yml`). Gelöscht: drei Volumes und
|
||
drei Abbilder, rund **670 MB**. Nachgezählt war vorher, dass kein einziges Bild
|
||
darin lag.
|
||
|
||
**Was noch bei dir liegt** — an NPM komme ich ohne Zugang nicht.
|
||
|
||
Nachgemessen am 11.08.2026, nicht vermutet:
|
||
|
||
| Pfad | gemessene `Cache-Control` | heißt |
|
||
|---|---|---|
|
||
| `/f/…` | `public, max-age=300` (unsere) | Block sitzt ✅ |
|
||
| `/t/…` | `max-age=17293` (NPMs) | **Block fehlt** ⬜ |
|
||
|
||
Also noch einzutragen:
|
||
|
||
```nginx
|
||
location ^~ /t/ { proxy_cache off; include conf.d/include/proxy.conf; }
|
||
```
|
||
|
||
Ohne ihn zeigt die Galerie nach einem erneuten Upload **stundenlang die alte
|
||
Vorschau** — die Datei selbst ist längst neu, nur das Kachelbild nicht. Kein
|
||
Datenverlust, aber genau die Sorte Abweichung, die man dem Dienst anlastet.
|
||
|
||
Außerdem: den Proxy-Host `fivecdn.d4rkst3r.de` löschen — er zeigt auf den toten
|
||
Port 9100 und gibt 502.
|
||
|
||
### ✅ Fertig — Papierkorb, Abrufzähler, sparsame Fassungen, Archive
|
||
|
||
Vier Dinge, gemessen und im Betrieb:
|
||
|
||
**Papierkorb.** Löschen war endgültig, und das bei einem Knopf „alle 3861
|
||
wählen" direkt neben „löschen". Jetzt wandert die Datei nach `/data/papierkorb`
|
||
und der Datensatz in eine eigene Tabelle; nach **30 Tagen** räumt der Dienst
|
||
stündlich selbst auf. Ende zu Ende geprüft: löschen → öffentlich 404, Eintrag
|
||
im Papierkorb, zurückholen → 200 samt neu gerechneter Vorschau.
|
||
|
||
**Abrufzähler.** `media.abrufe` und `zuletzt_abgerufen`. Gezählt wird im
|
||
Speicher und alle 30 Sekunden weggeschrieben — ein `UPDATE` je Kachel wären
|
||
bei einer Galerieseite 120 Schreibvorgänge. Der 304 zählt mit (der Aufrufer
|
||
*wollte* die Datei), `HEAD` nicht. **Was vor dem Einbau lag, steht als „nie
|
||
geholt" da, auch wenn es täglich benutzt wurde** — das sagt die Karte im
|
||
Speicherbericht auch dazu.
|
||
|
||
**Sparsame Fassungen ohne Adressänderung.** Neben jedem PNG/JPEG liegt eine
|
||
WebP-Fassung, ausgeliefert unter *derselben* Adresse je nach `Accept`.
|
||
Nachgemessen: 6448 → 2758 Bytes für einen Browser, unverändert 6448 für `curl`.
|
||
Mit `Vary: Accept` und getrennten ETags (`…-w`). Details in `docs/API.md`.
|
||
|
||
Über den ganzen Bestand nachgerechnet: **3606 Fassungen, 199,3 MB → 33,1 MB,
|
||
gespart 166,2 MB (83 %).** Bei **29** lohnt WebP nicht — dort bleibt es beim
|
||
Original, und das ist kein Fehler, sondern die Regel „nur wenn es kleiner wird".
|
||
|
||
**Und ein Knopf, der gelogen hat.** Der erste Nachrüstlauf über alle Dateien
|
||
brauchte mehr als 90 Sekunden — genau da gibt Nginx Proxy Manager auf
|
||
(`proxy_read_timeout`). Ergebnis: **504 im Browser, während die Arbeit im
|
||
Hintergrund weiterlief und fertig wurde.** Ein Knopf, der Erfolg als Fehler
|
||
meldet, ist schlimmer als einer ohne Rückmeldung.
|
||
|
||
Behoben, indem beide Wartungsknöpfe jetzt in **Runden zu 300 Stück** laufen:
|
||
der Server meldet `offen` und `fertig`, die Oberfläche ruft erneut auf und
|
||
zeigt dabei „280 erzeugt, noch 129 offen …". Nachgemessen: 7,8 s je Runde
|
||
statt 90+ am Stück. Der Vorschau-Knopf hatte dieselbe Wand und wurde
|
||
mitgezogen, obwohl er noch nicht dagegengelaufen war.
|
||
|
||
**ZIP hochladen.** Ein Archiv wird ausgepackt statt abgelegt, die Ordner darin
|
||
bleiben erhalten und hängen sich hinter den gewählten Zielordner. `fflate`,
|
||
also reines JavaScript — dieses Projekt hat schon einen halben Abend an einer
|
||
ABI-Nummer verloren. Grenzen: 5000 Einträge, 256 MB entpackt, geprüft **vor**
|
||
dem Entpacken (Zip-Bombe). Zip-Slip nachgemessen: `../../../../etc/passwd` und
|
||
`..\..\windows\hosts` abgelehnt, die harmlose Datei im selben Archiv abgelegt.
|
||
|
||
### ✅ Fertig — Betrachter für Lesbares, und ein richtiger Player
|
||
|
||
Bild, Video, Ton und PDF konnte der Betrachter schon. Was fehlte, war
|
||
ausgerechnet die Art, die man am ehesten nur lesen will: eine `.md` landete im
|
||
Zweig „lässt sich hier nicht anzeigen".
|
||
|
||
**Jetzt lesbar:** Markdown gesetzt, `.lua`/`.json` eingefärbt (derselbe
|
||
Tokenizer wie auf der API-Seite, nicht ein zweiter), CSV als Tabelle — mit
|
||
Anführungszeichen und doppelten darin, das ist der Teil, den ein
|
||
`split(';')` falsch macht —, alles Übrige mit Zeilennummern in einer eigenen
|
||
Spalte, damit ein Kopieren sie nicht mitnimmt.
|
||
|
||
**Nicht alles wird geholt.** Die Größe steht in der Datenbank, also wissen wir
|
||
vorher, worauf wir uns einlassen: über 512 KB kommt per Bereichsanfrage nur der
|
||
Anfang, und das steht auch da. Gemessen: `Range: bytes=0-49` → `206`, 50 Bytes.
|
||
|
||
**Markdown ohne Bibliothek.** `marked` + `DOMPurify` enden bei
|
||
`dangerouslySetInnerHTML`, und die Sicherheit hängt dann an der Frage, ob die
|
||
Filterliste vollständig ist. Hier entsteht **nie eine HTML-Zeichenkette** —
|
||
Text wird zu React-Knoten. Im ganzen `ui/` steht kein einziges
|
||
`dangerouslySetInnerHTML` (außer in den zwei Kommentarzeilen, die erklären,
|
||
warum).
|
||
|
||
Die eine Lücke, die React nicht schließt, ist `href`: `[klick](javascript:…)`
|
||
käme durch. Zwölf Fälle in Node durchgeprüft, alle bestanden — `http`, `https`,
|
||
`mailto` und relative Ziele durch; `javascript:`, `JavaScript:`, mit
|
||
Leerzeichen davor, `data:`, `vbscript:`, `file:` abgelehnt und als Text stehen
|
||
gelassen.
|
||
|
||
**Der Player.** Die Bedienelemente bleiben die des Browsers — ein selbstgebauter
|
||
Schieber sieht in jedem Browser anders falsch aus und kann keine Tastatur.
|
||
Drumherum kam, was der Browser nicht mitbringt:
|
||
|
||
- **Lautstärke über Dateien hinweg.** `<video>` setzt sie bei jedem neuen
|
||
Element auf 1 zurück; wer zwanzig Clips durchsieht, stellt sie sonst
|
||
zwanzigmal leise — und beim einundzwanzigsten fährt sie ungefragt hoch.
|
||
- **Weiter zum nächsten Stück**, abschaltbar. Und nur, wenn das Nächste auch
|
||
etwas zum Abspielen ist: nach einem Lied ungefragt bei einem Bild zu landen
|
||
wäre schlechter als stehenzubleiben.
|
||
- **Tastatur:** Leertaste hält an, ←/→ springen fünf Sekunden, Umschalt+←/→
|
||
wechselt die Datei. Bei allem anderen blättern die Pfeile wie bisher. Die
|
||
Belegung steht als Zeile unter dem Bild — eine unsichtbare Sonderregel fühlt
|
||
sich an wie ein Fehler.
|
||
|
||
**Server:** die Typtabelle kennt jetzt `md`, `csv`, `lua`, `xml`, `yml`, `toml`,
|
||
`ini`, `cfg`, `sql`, `log` — **alle als `text/plain`**. Nicht als `text/html`,
|
||
und `html`/`css`/`js` stehen weiterhin *nicht* drin: wer einen Upload-Token
|
||
hat, könnte sonst eine Seite unter unserem Namen veröffentlichen.
|
||
|
||
Der Betrachter entscheidet trotzdem nach der **Endung** und erst danach nach der
|
||
gemeldeten Art. Grund: `items/readme.md` lag schon da und trug
|
||
`application/octet-stream`, weil sie vor der erweiterten Tabelle hochgeladen
|
||
wurde. Wer nur auf die Art schaut, zeigt die alte README nicht an und eine neue
|
||
schon — und sucht den Unterschied an der falschen Stelle.
|
||
|
||
### ✅ Fertig — Vorschau für Video und Ton, und zwei Ordner-Lücken
|
||
|
||
Mit den ersten echten Clips fiel auf, was bei 3600 Bildern nie auffiel: alles,
|
||
was kein Bild ist, bekam ein **Fragezeichen** auf die Kachel — also ausgerechnet
|
||
das Zeichen für „unbekannt" auf einer völlig gesunden MP4.
|
||
|
||
**Jetzt:** Standbild aus dem Clip (ein Zehntel hinein, weil viele mit Schwarz
|
||
beginnen), bei Ton das eingebettete Titelbild, dazu ein Abspielzeichen und die
|
||
**Laufzeit** in der Ecke. Fehlt eine Vorschau, kommt ein Zeichen, das die *Art*
|
||
benennt, statt eines Fragezeichens.
|
||
|
||
**Der Preis: ffmpeg im Abbild** — und der wurde zweimal gemessen und zweimal
|
||
gedrückt:
|
||
|
||
| Weg | Abbild |
|
||
|---|---|
|
||
| vorher | 460 MB |
|
||
| `apt-get install ffmpeg` | **1,11 GB** |
|
||
| statisches ffmpeg **+** ffprobe | 835 MB |
|
||
| statisches ffmpeg **allein** | **648 MB** |
|
||
|
||
Debians Paket schaltet jeden Filter frei und zieht die Abhängigkeiten mit:
|
||
`libllvm15` (114 MB), Mesa samt `libgl1-mesa-dri`, `libz3`, und `libflite1` —
|
||
ein **Sprachsynthesizer**. Alles für Filter, die ein Dienst ohne Bildschirm nie
|
||
anfasst; `--no-install-recommends` half nicht, es sind harte Abhängigkeiten.
|
||
|
||
`ffprobe` flog danach auch noch raus: 135 MB für eine einzige Aufgabe, die
|
||
Länge zu nennen — die steht auch im Kopf, den `ffmpeg` beim Öffnen druckt.
|
||
Gegengeprüft an beiden Dateien, auf zwei Nachkommastellen identisch:
|
||
|
||
```
|
||
ffprobe 29.767967 | ffmpeg Duration: 00:00:29.77
|
||
ffprobe 645.642449 | ffmpeg Duration: 00:10:45.64
|
||
```
|
||
|
||
**Ein Fehler, der zweimal derselbe war.** „Keine Bildspur" ist bei Ton der
|
||
Normalfall — die meisten MP3 tragen kein Titelbild. Der erste Anlauf erkannte
|
||
das am *Wortlaut* der Fehlermeldung, und der ist zwischen den Fassungen anders:
|
||
|
||
```
|
||
ffmpeg 5.1 "Stream map '0:v:0' matches no streams."
|
||
ffmpeg 7.1 "Failed to set value '0:v:0' for option 'map'"
|
||
```
|
||
|
||
Ergebnis: eine gesunde MP3 wurde als **„1 fehlgeschlagen"** gezählt. Gelesen
|
||
wird jetzt die *Spurenliste* (`Stream #0:1: Video:`), die seit jeher so
|
||
dasteht — und die kommt aus demselben Aufruf wie die Länge, also ein Blick in
|
||
die Datei statt drei. Gemessen: `fehler: 0, ohneBild: 1`.
|
||
|
||
Beide Wege durchgeprüft — Video: 320×180, Durchschnittsfarbe RGB(97,140,155),
|
||
also echtes Bild und kein schwarzer Vorspann. Ton mit angehängtem Titelbild:
|
||
Vorschau erzeugt, `dauer: 8.05` → Kachel zeigt `0:08`.
|
||
|
||
**Und zwei Ordner-Lücken, beide vom Benutzer gefunden:**
|
||
|
||
1. **„Wie lege ich neue Ordner an?"** — Gar nicht, außer man wusste es: der
|
||
Knopf saß **nur im Verschieben-Dialog**. Wer einen leeren Ordner wollte,
|
||
musste also erst Dateien auswählen und „Verschieben" drücken. Eine Funktion,
|
||
die man nur über eine andere Funktion erreicht, ist für den, der sie sucht,
|
||
nicht vorhanden. Jetzt steht sie unter dem Ordnerbaum — mit der Angabe, *in*
|
||
welchem Ordner sie entsteht.
|
||
2. **Zielordner beim Hochladen.** Vorher galt stillschweigend der in der
|
||
Galerie angeklickte Ordner. Richtig, solange man es weiß — und ein
|
||
Fallstrick, sobald man es nicht weiß: wer nach einer Suche hochlädt, steht
|
||
in „Alle" und legt an der Wurzel ab. Der angeklickte Ordner bleibt die
|
||
Voreinstellung, ist jetzt aber sichtbar und änderbar, samt „neuer Ordner"
|
||
direkt daneben.
|
||
|
||
### ✅ Fertig — Umbenennen, Fivemanage-Sprache, Freigabe-Links, sichtbare Zahlen
|
||
|
||
**Umbenennen** gab es gar nicht — verschieben ja, umbenennen nirgends. Jetzt für
|
||
Datei *und* Ordner. Die Warnung ist dabei der eigentliche Teil: beim Umbenennen
|
||
ändert sich **die Adresse**, und die steht womöglich in einem Skript, das
|
||
niemand mehr im Kopf hat. Der Dialog zeigt deshalb die Abrufzahl („diese Datei
|
||
wurde 4000× geholt") und beim Ordner die Zahl der betroffenen Dateien, **bevor**
|
||
gedrückt wird.
|
||
|
||
Am echten Fall geprüft — der Kollisionsfund von oben: `items/coiloverss.png`
|
||
(hielt die `+`-Fassung) → `items/coiloverss-plus.png`, alte Adresse 404, neue
|
||
200 mit 4375 B, Vorschau mitgewandert. Ordner: `probe` → `beispiele`, 5 Dateien,
|
||
alle Datenbankpfade in einer Transaktion umgeschrieben.
|
||
|
||
> **Und der Fallstrick beim Ordner:** `thumbPath` hängt `.webp` an. Für eine
|
||
> Datei richtig, für einen Ordner Unsinn — der Vorschauordner heißt
|
||
> `thumbs/vehicles` und nicht `thumbs/vehicles.webp`. Dafür gibt es jetzt
|
||
> `moveFolder`.
|
||
|
||
**Die 93 Kollisionen sind erledigt.** `tools/kollisionen.ps1` rechnet dieselbe
|
||
Zähmung auf der Quelle nach und zeigt, was zusammenfällt. Ergebnis: **91 von 93
|
||
sind dieselbe Datei zweimal** — `WEAPON_SMG.png` und `weapon_smg.png`, byteweise
|
||
gleich groß. Da fehlte nichts. **Echt verloren waren zwei**, beide mit `+` im
|
||
Namen (das `+` wird zum Strich und dann abgeschnitten). Beide nachgeholt.
|
||
|
||
**Fivemanage-Sprache.** `POST /api/image|video|audio` (v1) und `POST /api/v3/file`
|
||
(v3), Schlüssel nackt im `Authorization`-Kopf. Der Umzug fremder Skripte ist
|
||
damit eine Zeile: die Adresse. Beide Formen sind aus echtem Code abgelesen —
|
||
`fivemanage/sdk` und `Awleks/Devm-Camera` —, nicht geraten. Gemessen: v1 →
|
||
`{url,id,path}`, v3 → `{status:"ok",data:{id,url}}`, ohne Schlüssel 401, Video
|
||
an `/api/image` → 415 mit Grund.
|
||
|
||
> **Dabei bin ich in eine Falle gelaufen, vor der im eigenen Repo ein Kommentar
|
||
> warnt.** Die Token-Wache hing an `use('*')` — und der Einhängepunkt ist
|
||
> `/api`, also galt sie auch für `/api/dash` daneben. Das Dashboard bekam 401
|
||
> auf die *Anmeldung*. Wortwörtlich derselbe Fehler steht seit Monaten in
|
||
> `upload.ts` als Kommentar, weil er dort schon einmal passiert ist. Gemerkt hat
|
||
> es der Gegentest, nicht der Kopf.
|
||
|
||
**Freigabe-Links.** `/s/<schlüssel>` zeigt einen Ordner ohne Anmeldung — ein
|
||
Katalog für Spieler. Was dabei ausdrücklich dabeisteht, in der Karte *und* in
|
||
der Rückfrage vor dem Zurückziehen: **freigegeben wird die Liste, nicht der
|
||
Inhalt.** Die Dateien sind ohnehin öffentlich; ein zurückgezogener Link macht
|
||
sie nicht wieder privat, er nimmt nur die Übersicht weg. Ohne diesen Satz zieht
|
||
jemand einen Link zurück und glaubt, etwas sei verschwunden.
|
||
|
||
Die Antwort ist abgemessen: Name, Größe, Art, Adresse, Länge. **Nicht**: Hash,
|
||
Token, Zeitpunkte, Abrufzahlen, IDs. Ein unbekannter Schlüssel und ein
|
||
zurückgezogener geben dieselbe Antwort — ein Unterschied wäre eine Auskunft an
|
||
jemanden, der Schlüssel durchprobiert.
|
||
|
||
**Zahlen, die schon da waren.** Sortieren nach Abrufen und nach „zuletzt geholt"
|
||
(die Kachel zeigt dann auch diese Zahl statt Größe und Datum — nach etwas zu
|
||
ordnen, das man nirgends sieht, ist eine Reihenfolge ohne Begründung). Und je
|
||
Token, was damit abgelegt wurde: `media.token_id` wird seit dem ersten Tag
|
||
geschrieben und war **nirgends** zu sehen. Gemessen: `d4rk_photostudio` hält 810
|
||
Dateien / 99,5 MB, 3640 Dateien liegen ohne Token da (aus dem Dashboard).
|
||
|
||
### ⬜ Als Nächstes
|
||
|
||
**Der Vergleichsmaßstab ist weg.** Am 11.08.2026 sind die Lite-Container
|
||
gelöscht worden — mit ihnen die Möglichkeit, im Zweifel nachzusehen, wie es
|
||
dort aussah. Was noch liegt und eine Entscheidung braucht:
|
||
|
||
```
|
||
Volumes fivemanager_db fivemanager_minio fivemanager_pgdata
|
||
Abbilder ghcr.io/fivemanage/lite:0.1.0-beta.23 minio/minio minio/mc
|
||
```
|
||
|
||
**Nachgesehen, bevor jemand fragt: es ist nichts drin.** Alle drei Volumes
|
||
schreibgeschützt eingehängt und durchgezählt — **kein einziges Bild**.
|
||
`fivemanager_minio` sind 244 KB und besteht ausschließlich aus `.minio.sys`;
|
||
der Eimer `media` hat Metadaten, aber kein einziges Objekt. Die anderen beiden
|
||
sind leere Datenbanken (MySQL 196 MB, PostgreSQL 47 MB — das ist ihr
|
||
Leergewicht). **Löschen kostet nichts**, es ist nur noch nicht getan.
|
||
|
||
### ⬜ 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.**~~ **Entschieden und gebaut** — `tools/sichern.ps1`, täglich
|
||
um 04:30, siehe oben. Weder Restic noch `docker cp`, sondern SQLites eigene
|
||
Sicherungsschnittstelle plus ein `tar.gz` aus einem nur lesend eingehängten
|
||
Volume. Die Vermutung von damals war richtig: ein blosses Kopieren wäre wegen
|
||
des WAL still unvollständig gewesen.
|
||
**Offen bleibt das Ziel:** die Archive liegen auf derselben Platte wie die
|
||
Daten. Gegen einen Plattenausfall hilft das nicht — dafür müssten sie in die
|
||
Nextcloud oder auf eine andere Maschine.
|
||
- **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.
|