# 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 , dann media.db und files/ ins Volume legen," -ForegroundColor DarkGray Write-Host " waehrend der Container steht. Siehe ROADMAP." -ForegroundColor DarkGray