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:
@@ -0,0 +1,142 @@
|
||||
# Sicherung von d4rk_media: Datenbank und Bilder in EIN Archiv.
|
||||
#
|
||||
# Aufruf:
|
||||
# pwsh -File tools\sichern.ps1
|
||||
# pwsh -File tools\sichern.ps1 -Ziel D:\woanders -Behalten 30
|
||||
#
|
||||
# Was passiert, und warum in dieser Reihenfolge:
|
||||
#
|
||||
# 1. Die Datenbank wird IM laufenden Container ueber SQLites eigene
|
||||
# Sicherungsschnittstelle als eine in sich stimmige Datei abgelegt
|
||||
# (dist/backup.js). Ein blosses Kopieren waere wegen des WAL still
|
||||
# unvollstaendig.
|
||||
# 2. Ein zweiter, kurzlebiger Container packt Bilder und diese Aufnahme in
|
||||
# ein tar.gz. Er haengt das Volume NUR LESEND ein — eine Sicherung, die
|
||||
# schreiben kann, kann auch kaputtmachen.
|
||||
# 3. Das Archiv wird GEPRUEFT: ausgepackt, die Datenbank geoeffnet, die
|
||||
# Eintraege gezaehlt und mit dem laufenden Dienst verglichen. Eine
|
||||
# Sicherung, die niemand nachzaehlt, ist eine Vermutung.
|
||||
# 4. Alte Archive fliegen raus, sobald mehr als -Behalten da sind.
|
||||
#
|
||||
# NICHT MIT DRIN sind die Vorschaubilder unter /data/thumbs, und das ist
|
||||
# Absicht: sie lassen sich aus den Bildern jederzeit neu rechnen (Dashboard →
|
||||
# Speicher → "Fehlende Vorschaubilder erzeugen"). Sie mitzusichern hiesse, jede
|
||||
# Nacht Abgeleitetes mitzuschleppen. Wer zurueckspielt, drueckt den Knopf.
|
||||
#
|
||||
# Der Dienst laeuft die ganze Zeit weiter. Nichts hier haelt ihn an.
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Ziel = 'C:\backup\d4rk_media',
|
||||
[string]$Container = 'd4rk-media',
|
||||
[string]$Volume = 'd4rk_media_media-data',
|
||||
[string]$Abbild = 'd4rk-media:latest',
|
||||
[int]$Behalten = 14
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Schritt($text) { Write-Host "==> $text" -ForegroundColor Cyan }
|
||||
function Gut($text) { Write-Host " $text" -ForegroundColor Green }
|
||||
function Schlecht($text) { Write-Host " $text" -ForegroundColor Red }
|
||||
|
||||
# -- Vorbedingungen ------------------------------------------------------------
|
||||
|
||||
$laeuft = docker ps --filter "name=^/$Container$" --format '{{.Names}}'
|
||||
if ($laeuft -ne $Container) {
|
||||
Schlecht "Der Container '$Container' laeuft nicht. Ohne ihn gibt es keine stimmige Aufnahme der Datenbank."
|
||||
exit 1
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $Ziel | Out-Null
|
||||
$stempel = Get-Date -Format 'yyyy-MM-dd-HHmm'
|
||||
$name = "d4rk_media-$stempel.tar.gz"
|
||||
$archiv = Join-Path $Ziel $name
|
||||
|
||||
# -- 1. Datenbank stimmig herausholen ------------------------------------------
|
||||
|
||||
Schritt 'Datenbank sichern (SQLite-Sicherungsschnittstelle im Container)'
|
||||
$ausgabe = docker exec $Container node dist/backup.js /data/.sicherung/media.db 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Schlecht "Fehlgeschlagen: $ausgabe"
|
||||
exit 1
|
||||
}
|
||||
Gut $ausgabe
|
||||
# Die erwartete Zeilenzahl steht in der Meldung -- sie ist der Massstab fuer
|
||||
# Schritt 3.
|
||||
$erwartet = if ($ausgabe -match '(\d+) Medieneintraege') { [int]$Matches[1] } else { -1 }
|
||||
|
||||
# -- 2. Alles in ein Archiv ----------------------------------------------------
|
||||
|
||||
Schritt 'Bilder und Datenbank packen'
|
||||
# Zwei -C: die Bilder kommen als files/... in das Archiv, die Aufnahme als
|
||||
# media.db daneben. Bewusst das eigene Abbild statt alpine -- dort ist GNU tar
|
||||
# drin, und das kann mehrere -C.
|
||||
docker run --rm `
|
||||
-v "${Volume}:/data:ro" `
|
||||
-v "${Ziel}:/out" `
|
||||
--entrypoint sh $Abbild -c "tar czf '/out/$name' -C /data files -C /data/.sicherung media.db"
|
||||
if ($LASTEXITCODE -ne 0) { Schlecht 'Packen fehlgeschlagen.'; exit 1 }
|
||||
|
||||
# Die Zwischenaufnahme im Volume wieder weg: sie gehoert nicht zum Bestand und
|
||||
# wuerde bei der naechsten Sicherung mitgezaehlt.
|
||||
docker exec $Container rm -rf /data/.sicherung | Out-Null
|
||||
|
||||
$groesse = (Get-Item $archiv).Length
|
||||
Gut "$name — $([math]::Round($groesse/1MB, 1)) MB"
|
||||
|
||||
# -- 3. Nachzaehlen ------------------------------------------------------------
|
||||
|
||||
Schritt 'Sicherung pruefen (auspacken, Datenbank oeffnen, zaehlen)'
|
||||
$pruefung = docker run --rm `
|
||||
-v "${Ziel}:/in:ro" `
|
||||
--entrypoint sh $Abbild -c @"
|
||||
set -e
|
||||
mkdir -p /tmp/p && cd /tmp/p
|
||||
tar xzf '/in/$name'
|
||||
# Aus /app heraus starten und nicht aus /tmp/p: Node sucht node_modules vom
|
||||
# Arbeitsverzeichnis aufwaerts, und ueber /tmp liegt kein better-sqlite3.
|
||||
cd /app
|
||||
node -e "
|
||||
const db = require('better-sqlite3')('/tmp/p/media.db', { readonly: true, fileMustExist: true });
|
||||
const m = db.prepare('SELECT COUNT(*) AS n FROM media').get().n;
|
||||
const t = db.prepare('SELECT COUNT(*) AS n FROM tokens').get().n;
|
||||
const u = db.prepare('SELECT COUNT(*) AS n FROM users').get().n;
|
||||
console.log(JSON.stringify({ media: m, tokens: t, users: u }));
|
||||
"
|
||||
find /tmp/p/files -type f | wc -l
|
||||
"@
|
||||
if ($LASTEXITCODE -ne 0) { Schlecht "Pruefung fehlgeschlagen: $pruefung"; exit 1 }
|
||||
|
||||
$zeilen = @($pruefung -split "`n" | Where-Object { $_.Trim() })
|
||||
$zahlen = $zeilen[0] | ConvertFrom-Json
|
||||
$dateien = [int]$zeilen[1].Trim()
|
||||
|
||||
Gut "in der Sicherung: $($zahlen.media) Medieneintraege, $dateien Dateien, $($zahlen.tokens) Token, $($zahlen.users) Benutzer"
|
||||
|
||||
if ($erwartet -ge 0 -and $zahlen.media -ne $erwartet) {
|
||||
Schlecht "PASST NICHT: erwartet waren $erwartet Eintraege."
|
||||
exit 1
|
||||
}
|
||||
# Eine Datei mehr oder weniger als Eintraege ist ein echter Befund und kein
|
||||
# Rundungsfehler: dann liegt eine Datei ohne Datensatz herum oder umgekehrt.
|
||||
if ($dateien -ne $zahlen.media) {
|
||||
Write-Host " Hinweis: $dateien Dateien zu $($zahlen.media) Eintraegen — das sollte gleich sein." -ForegroundColor Yellow
|
||||
}
|
||||
Gut 'Datenbank in der Sicherung ist lesbar und vollstaendig.'
|
||||
|
||||
# -- 4. Alte Sicherungen ausduennen --------------------------------------------
|
||||
|
||||
$alle = Get-ChildItem $Ziel -Filter 'd4rk_media-*.tar.gz' | Sort-Object Name -Descending
|
||||
if ($alle.Count -gt $Behalten) {
|
||||
Schritt "Alte Sicherungen entfernen (mehr als $Behalten)"
|
||||
$alle | Select-Object -Skip $Behalten | ForEach-Object {
|
||||
Remove-Item $_.FullName -Force
|
||||
Gut "entfernt: $($_.Name)"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Gut "Fertig: $archiv"
|
||||
Write-Host " Zurueckspielen: tar xzf <archiv>, dann media.db und files/ ins Volume legen," -ForegroundColor DarkGray
|
||||
Write-Host " waehrend der Container steht. Siehe ROADMAP." -ForegroundColor DarkGray
|
||||
Reference in New Issue
Block a user