# Die Sicherung des Bots aus dem Volume HOLEN und auf eine zweite Platte legen. # # Aufruf: # pwsh -File tools\abholen.ps1 # pwsh -File tools\abholen.ps1 -Ziel E:\woanders -Behalten 30 # # WOZU, wo der Bot doch schon sichert. Er sichert in DASSELBE VOLUME, in dem # auch die Datenbank liegt (`/app/data/ecobot.db` und `/app/data/backups/`). # Geht das Volume verloren, geht beides zusammen verloren -- das ist keine # Sicherung, das ist eine Kopie. Dieses Skript ist der Weg hinaus. # # WAS ES NICHT TUT: sichern. Das Archiv baut der Bot selbst, mit SQLites # Online-Schnittstelle und seit dem 12.08.2026 mit eigener Gegenpruefung. Hier # wird nur ABGEHOLT -- und zwar so, dass am Ende feststeht, dass die Kopie heil # ist und nicht nur, dass ein Kopierbefehl keinen Fehler geworfen hat. # # Die Reihenfolge und warum sie so ist: # # 1. Das neueste Archiv im Container finden. # 2. Herausholen. `docker cp` und kein zweiter Container: der Bot laeuft # ohnehin, und ein Volume-Mount waere ein Umweg um eine offene Tuer. # 3. Pruefsumme vergleichen -- IM CONTAINER gerechnet gegen die Kopie hier. # Nicht die Dateigroesse: eine abgebrochene Kopie auf eine volle Platte hat # oft genau die richtige Laenge und trotzdem Nullen am Ende. # 4. Das Archiv AUSPACKEN und die Datenbank darin oeffnen -- mit dem # Bot-Abbild, weil dort better-sqlite3 schon liegt. Ein Archiv, das # niemand geoeffnet hat, ist eine Vermutung. # 5. Melden: `last_zweitziel` in die Einstellungen des Bots. Sein Wachhund # liest das und beschwert sich, wenn die Abholung ausbleibt -- sonst waere # dieses Skript das naechste, das still aufhoert zu laufen. # 6. Alte Kopien ausduennen. # # UND ES WARNT, WENN ES DIESELBE PLATTE IST. Ein zweiter Ordner auf C: sieht # genauso gruen aus wie eine echte zweite Platte und hilft gegen gar nichts; # zwei Partitionen derselben NVMe sterben zusammen. Verglichen wird deshalb die # PHYSISCHE Platte und nicht der Laufwerksbuchstabe. [CmdletBinding()] param( [string]$Ziel = 'D:\backup\d4rkbot', [string]$Container = 'd4rkbot', [string]$Abbild = 'd4rkbot:latest', [int]$Behalten = 14 ) $ErrorActionPreference = 'Stop' function Schritt($t) { Write-Host "==> $t" -ForegroundColor Cyan } function Gut($t) { Write-Host " $t" -ForegroundColor Green } function Schlecht($t) { Write-Host " $t" -ForegroundColor Red } # Das Ergebnis zurueck in die Datenbank des Bots. Schlaegt DAS fehl, ist es kein # Grund, die Abholung als gescheitert zu behandeln -- die Kopie liegt dann # trotzdem richtig. function Melde($ok, $name, $bytes, $fehler) { $eintrag = @{ at = [long]([DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()) ok = [bool]$ok datei = "$name" bytes = [long]$bytes ziel = "$Ziel" } if ($fehler) { $eintrag['fehler'] = "$fehler" } # Ueber die Standardeingabe und nicht als Argument: der JSON-Text enthaelt # Anfuehrungszeichen, und die ueberleben den Weg durch zwei Shells nicht. $skript = @" let roh = ''; process.stdin.on('data', (d) => (roh += d)).on('end', () => { const db = require('/app/node_modules/better-sqlite3')('/app/data/ecobot.db'); db.prepare('INSERT INTO settings (key, value) VALUES (?, ?) ' + 'ON CONFLICT(key) DO UPDATE SET value = excluded.value') .run('last_zweitziel', roh.trim()); }); "@ -replace "`r", '' ($eintrag | ConvertTo-Json -Compress) | docker exec -i $Container node -e $skript 2>&1 | Out-Null } # -- Vorbedingungen ------------------------------------------------------------ $laeuft = docker ps --filter "name=^/$Container$" --format '{{.Names}}' if ($laeuft -ne $Container) { Schlecht "Der Container '$Container' laeuft nicht." exit 1 } New-Item -ItemType Directory -Force -Path $Ziel | Out-Null # -- 1. Das neueste Archiv finden ---------------------------------------------- Schritt 'Das neueste Archiv im Container suchen' # `ls -1` und danach das letzte: die Namen tragen das Datum in ISO-Form, sind # also alphabetisch schon in der richtigen Reihenfolge. $name = (docker exec $Container sh -c 'ls -1 /app/data/backups/*.db.gz 2>/dev/null | tail -1') -join '' $name = $name.Trim() if (-not $name) { Schlecht 'Im Container liegt kein Archiv. Hat der Bot schon einmal gesichert?' Melde $false '' 0 'kein Archiv im Container' exit 1 } $kurz = Split-Path $name -Leaf Gut $kurz # Ist es ueberhaupt von heute? Ein Skript, das treu das Archiv von vorletzter # Woche abholt und "fertig" sagt, ist schlimmer als eines, das gar nicht laeuft. $alter = (docker exec $Container sh -c "find '$name' -mtime +1 | wc -l").Trim() if ($alter -ne '0') { Write-Host ' ACHTUNG: das neueste Archiv ist aelter als 24 Stunden.' -ForegroundColor Yellow Write-Host ' Die Abholung laeuft trotzdem -- aber die Sicherung des Bots stockt.' -ForegroundColor Yellow } # -- 2. Herausholen ------------------------------------------------------------ Schritt "Herausholen nach $Ziel" $dortHin = Join-Path $Ziel $kurz docker cp "${Container}:$name" $dortHin if ($LASTEXITCODE -ne 0) { Schlecht 'docker cp fehlgeschlagen.' Melde $false $kurz 0 'docker cp fehlgeschlagen' exit 1 } $bytes = (Get-Item $dortHin).Length Gut ("{0:N0} Bytes" -f $bytes) # Dieselbe Platte? Dann ist es keine Sicherung, sondern eine Kopie. function PlattenNummer($pfad) { # $null heisst "kein Urteil moeglich" (Netzfreigabe, ungewoehnliche # Einhaengung) und ist ausdruecklich nicht dasselbe wie "andere Platte". try { $wurzel = [System.IO.Path]::GetPathRoot((Resolve-Path $pfad).Path) $b = $wurzel.TrimEnd('\', ':') if ($b.Length -ne 1) { return $null } return (Get-Partition -DriveLetter $b -ErrorAction Stop).DiskNumber } catch { return $null } } # Die Daten des Bots liegen im Docker-Volume, und das liegt auf der Platte, auf # der Docker seine Volumes haelt -- unter Windows also im Systemlaufwerk. $hier = PlattenNummer $env:SystemDrive $dort = PlattenNummer $Ziel if ($null -ne $hier -and $hier -eq $dort) { Write-Host " ACHTUNG: $Ziel liegt auf DERSELBEN Platte (Nr. $hier) wie das Docker-Volume." -ForegroundColor Yellow Write-Host ' Gegen einen Plattenausfall hilft das nicht -- ein anderes Laufwerk waehlen.' -ForegroundColor Yellow } elseif ($null -ne $hier -and $null -ne $dort) { Gut "andere Platte: Nr. $dort statt Nr. $hier" } # -- 3. Pruefsumme ------------------------------------------------------------- Schritt 'Pruefsumme vergleichen' $drinnen = (docker exec $Container sh -c "sha256sum '$name' | cut -d' ' -f1").Trim() $draussen = (Get-FileHash $dortHin -Algorithm SHA256).Hash.ToLower() if (-not $drinnen) { Schlecht 'Im Container liess sich keine Pruefsumme bilden.' Melde $false $kurz $bytes 'sha256sum im Container fehlgeschlagen' exit 1 } if ($drinnen -ne $draussen) { Schlecht 'Die Kopie ist NICHT identisch.' Schlecht " im Container: $drinnen" Schlecht " hier: $draussen" Remove-Item $dortHin -Force -ErrorAction SilentlyContinue Melde $false $kurz $bytes 'Pruefsumme weicht ab' exit 1 } Gut "identisch ($($drinnen.Substring(0,12))...)" # -- 4. Auspacken und hineinsehen ---------------------------------------------- # # Mit dem Bot-Abbild, weil dort better-sqlite3 schon liegt. Der Ordner haengt # NUR LESEND -- eine Pruefung, die schreiben kann, kann auch kaputtmachen. Schritt 'Auspacken und die Datenbank darin oeffnen' # Das Pruefskript geht als DATEI hinein und nicht als `node -e`-Argument. Ein # mehrzeiliges Skript durch PowerShell, docker und die Shell des Containers zu # reichen, ist eine Anfuehrungszeichen-Frage mit drei Beteiligten -- und die # verliert man. $werkbank = Join-Path $env:TEMP "d4rkbot-pruefer-$PID" New-Item -ItemType Directory -Force -Path $werkbank | Out-Null $pruefer = @' const { createReadStream, createWriteStream } = require('node:fs'); const { pipeline } = require('node:stream/promises'); const { createGunzip } = require('node:zlib'); const Database = require('/app/node_modules/better-sqlite3'); (async () => { await pipeline(createReadStream(process.argv[2]), createGunzip(), createWriteStream('/tmp/p.db')); const db = new Database('/tmp/p.db', { readonly: true }); const urteil = db.pragma('integrity_check', { simple: true }); if (urteil !== 'ok') { console.error('integrity_check: ' + urteil); process.exit(1); } const t = db.prepare("SELECT COUNT(*) AS n FROM sqlite_master WHERE type='table'").get().n; const s = db.prepare('SELECT COUNT(*) AS n FROM settings').get().n; console.log(t + ' ' + s); })().catch((e) => { console.error(e.message); process.exit(1); }); '@ -replace "`r", '' # .cjs, weil das Bot-Abbild "type": "module" traegt -- eine .js waere dort ein # ES-Modul, und require() gaebe es darin nicht. Set-Content -Path (Join-Path $werkbank 'pruefer.cjs') -Value $pruefer -NoNewline -Encoding utf8 try { $ausgabe = docker run --rm ` -v "${Ziel}:/x:ro" -v "${werkbank}:/s:ro" ` --entrypoint node $Abbild /s/pruefer.cjs "/x/$kurz" 2>&1 $code = $LASTEXITCODE } finally { Remove-Item $werkbank -Recurse -Force -ErrorAction SilentlyContinue } if ($code -ne 0) { Schlecht "Das Archiv liess sich nicht oeffnen: $ausgabe" Remove-Item $dortHin -Force -ErrorAction SilentlyContinue Melde $false $kurz $bytes "Archiv unlesbar: $ausgabe" exit 1 } $zahlen = ($ausgabe -join ' ').Trim().Split(' ') Gut "in der Sicherung: $($zahlen[0]) Tabellen, $($zahlen[1]) Einstellungen" # -- 5. Melden ----------------------------------------------------------------- Melde $true $kurz $bytes $null # -- 6. Ausduennen ------------------------------------------------------------- $alle = Get-ChildItem $Ziel -Filter 'd4rkbot-*.db.gz' | Sort-Object Name -Descending if ($alle.Count -gt $Behalten) { Schritt "Alte Kopien entfernen (mehr als $Behalten)" $alle | Select-Object -Skip $Behalten | ForEach-Object { Remove-Item $_.FullName -Force Gut "entfernt: $($_.Name)" } } Write-Host '' Gut "Fertig: $dortHin" # NACH dem Ausduennen neu zaehlen -- $alle ist der Stand von davor, und eine # Zahl, die das Ausduennen nicht mitbekommt, ist eine falsche. $jetzt = @(Get-ChildItem $Ziel -Filter 'd4rkbot-*.db.gz') Write-Host " $($jetzt.Count) Kopien in $Ziel" -ForegroundColor DarkGray