feat: Sicherung, Anmeldebremse, Vorschaubilder -- und Schritt 5

SICHERUNG (tools/sichern.ps1, taeglich 04:30 als geplante Aufgabe).
Die Datenbank wird nicht kopiert, sondern ueber SQLites eigene
Sicherungsschnittstelle herausgeholt (server/src/backup.ts): im WAL-Modus
liegt das Zuletzte noch nicht in media.db, und selbst alle drei Dateien zu
kopieren ist nicht sicher, wenn waehrenddessen geschrieben wird. Die Bilder
kommen aus einem NUR LESEND eingehaengten Volume dazu.

Und sie prueft sich selbst: auspacken, Datenbank oeffnen, Medien/Token/
Benutzer zaehlen, mit dem laufenden Dienst vergleichen, sonst Fehler. Einmal
wirklich zurueckgespielt -- leeres Volume, zweiter Dienst, Anmeldung mit dem
echten Passwort, Bild abgerufen, Byte fuer Byte identisch. Eine Sicherung, die
nie zurueckgespielt wurde, ist eine Hoffnung.

ANMELDEBREMSE. Das Formular steht oeffentlich, und scrypt macht einen Versuch
teuer -- aber teuer ist nicht selten. Fuenf freie Versuche je Adresse, dann
Sperre ab 30 s mit Verdopplung bis 15 min, 429 samt Retry-After und einem Text,
der sagt wie lange. Erfolg setzt zurueck. Nur im Speicher: wer sich aussperrt,
startet den Container neu. Durchgemessen bis zur Erholung.

VORSCHAUBILDER. sharp erzeugt beim Upload eine 320er WebP-Fassung unter
/data/thumbs, ausgeliefert unter /t/<pfad>. Gemessen: 131502 -> 13078 Bytes,
Faktor 10; eine Galerieseite faellt von 7,5 MB auf 766 KB. KEINE Spalte in der
Datenbank -- ob es eine Vorschau gibt, sagt das Dateisystem, und die Galerie
faellt bei 404 aufs Vollbild zurueck. Eine zweite Wahrheit, die auseinander-
laufen kann, gibt es damit gar nicht erst. Ein Knopf zieht Fehlendes nach und
nennt Zahlen statt "fertig".

SCHRITT 5. Die Fivemanage-Dateien liegen in legacy/ mit Erklaerung; unsere
docker-compose.media.yml heisst jetzt docker-compose.yml. Drei Volumes und drei
Abbilder geloescht, rund 670 MB -- nachgezaehlt war vorher, dass kein Bild
darin lag.

Nebenbefund: `npm i sharp` scheitert auf diesem Rechner nicht an sharp, sondern
an better-sqlite3 -- der Host laeuft auf Node 24 (ABI 137), node-gyp uebernimmt
und findet keine Bauwerkzeuge. Genau die Falle aus dem Dockerfile. Eingetragen
wurde mit --package-lock-only, gebaut wird im Abbild auf Node 22.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 18:00:14 +02:00
co-authored by Claude Opus 5
parent 6c8ead8e15
commit aa2575bdf4
18 changed files with 1450 additions and 229 deletions
+84 -7
View File
@@ -201,7 +201,7 @@ Grundgerüst war nichts zu ändern; der Bau hat keinen einzigen Befund erzeugt.
```bash
export PUBLIC_URL=http://localhost:9102 ADMIN_PASSWORD=
docker compose -f docker-compose.media.yml up -d --build
docker compose up -d --build
```
**Was jetzt gemessen ist und vorher nur behauptet war:**
@@ -407,6 +407,81 @@ location ^~ /f/ {
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 15 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:
1. Den Schnipsel `location ^~ /f/ { proxy_cache off; … }` einsetzen (siehe
oben), sonst bleiben überschriebene Bilder 30 Minuten alt.
2. Den Proxy-Host `fivecdn.d4rkst3r.de` löschen — er zeigt auf den toten Port
9100 und gibt 502.
### ⬜ Als Nächstes
**5 · Umzug.** Root-`docker-compose.yml` wird unsere; die Fivemanage-Dateien
@@ -440,12 +515,14 @@ Leergewicht). **Löschen kostet nichts**, es ist nur noch nicht getan.
## Offene Entscheidungen
- **Sicherung.** Ein Volume mit Bildern und `media.db`. Restic gegen die
Nextcloud? Oder reicht ein `docker cp` vor größeren Änderungen?
**Was dabei feststeht:** wegen WAL gehören `media.db-wal` und `media.db-shm`
mit ins Kopieren, sonst fehlen die letzten Uploads. Sauberer wäre
`sqlite3 media.db ".backup …"` oder ein `wal_checkpoint(TRUNCATE)` davor —
beides ungeprüft, weil noch nicht entschieden ist, wie gesichert wird.
- ~~**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.