From 6a37f9e46ec1694f3452b05519fae6c0da16c6e4 Mon Sep 17 00:00:00 2001 From: Gulum Date: Sat, 6 Jun 2026 07:09:53 +0200 Subject: [PATCH] =?UTF-8?q?OPS-1:=20fix=20backup/restore=20scripts=20?= =?UTF-8?q?=E2=80=94=20ASCII-only=20+=20array=20coerce=20for=20Count?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace em dashes (UTF-8 multi-byte) with ASCII hyphens; PowerShell 5.1 reads ps1 files as CP1252 by default so any non-ASCII in source causes cascading parse errors - Extract POSTGRES_PASSWORD from container env (Aspire uses scram-sha-256; peer/trust auth does not work) and pass as PGPASSWORD env var to pg_dump and psql in both Backup and Restore scripts - @() wrap on Get-ChildItem result to force array before .Count under Set-StrictMode -Version Latest (single-item returns a bare object in PS5) - Restore-test evidence: 73 ColorVarieties -> DELETE 12 -> 61 -> pg_restore -> 73 confirmed (run 2026-06-06 07:09 against postgres-gatkzrgq) Co-Authored-By: Claude Sonnet 4.6 (1M context) --- deploy/scripts/Backup-GerbilManager.ps1 | 126 +++++++++++++++++++ deploy/scripts/Restore-GerbilManager.ps1 | 152 +++++++++++++++++++++++ 2 files changed, 278 insertions(+) create mode 100644 deploy/scripts/Backup-GerbilManager.ps1 create mode 100644 deploy/scripts/Restore-GerbilManager.ps1 diff --git a/deploy/scripts/Backup-GerbilManager.ps1 b/deploy/scripts/Backup-GerbilManager.ps1 new file mode 100644 index 0000000..b2ea173 --- /dev/null +++ b/deploy/scripts/Backup-GerbilManager.ps1 @@ -0,0 +1,126 @@ +<# +.SYNOPSIS + Sichert die GerbilManager-Datenbank (pg_dump) und den Fotoordner. + +.DESCRIPTION + 1. Findet den laufenden Aspire-Postgres-Docker-Container. + 2. Fuehrt pg_dump aus und speichert das SQL-Dump in C:\gerbil-data\backups\. + 3. Kopiert den Fotoordner als ZIP ins Backup-Verzeichnis. + 4. Rotiert alte Backups: behaelt die letzten $KeepDays Tage. + +.PARAMETER BackupRoot + Pfad zum Backup-Verzeichnis. Standard: C:\gerbil-data\backups + +.PARAMETER PhotosRoot + Pfad zum Fotoordner. Standard: C:\gerbil-data\photos + +.PARAMETER KeepDays + Anzahl der Tage, die Backups aufbewahrt werden. Standard: 7 + +.EXAMPLE + .\Backup-GerbilManager.ps1 + .\Backup-GerbilManager.ps1 -KeepDays 14 -BackupRoot D:\Sicherungen +#> +param( + [string]$BackupRoot = "C:\gerbil-data\backups", + [string]$PhotosRoot = "C:\gerbil-data\photos", + [int]$KeepDays = 7 +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm" +$backupDir = Join-Path $BackupRoot $timestamp +$logFile = Join-Path $BackupRoot "backup.log" +$dbDumpFile = Join-Path $backupDir "gerbilmanager_$timestamp.sql" +$photoZip = Join-Path $backupDir "photos_$timestamp.zip" + +function Log([string]$msg) { + $line = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] $msg" + Write-Host $line + Add-Content -Path $logFile -Value $line -Encoding UTF8 +} + +# --- Voraussetzungen pruefen --- +if (-not (Get-Command "docker" -ErrorAction SilentlyContinue)) { + Log "FEHLER: Docker nicht gefunden. Backup abgebrochen." + exit 1 +} + +# --- Backup-Verzeichnis anlegen --- +New-Item -ItemType Directory -Force -Path $backupDir | Out-Null +Log "Backup-Verzeichnis: $backupDir" + +# --- Postgres-Container finden --- +Log "Suche Postgres-Container..." +$containers = docker ps --format "{{.Names}}" 2>&1 +$pgContainer = $containers -split "`n" | Where-Object { $_ -match "postgres" } | Select-Object -First 1 + +if (-not $pgContainer) { + Log "FEHLER: Kein laufender Postgres-Container gefunden. Ist GerbilManager gestartet?" + exit 1 +} +$pgContainer = $pgContainer.Trim() +Log "Gefundener Container: $pgContainer" + +# Aspire setzt scram-sha-256 Auth - Passwort aus Container-Env lesen +$pgPassword = (docker inspect $pgContainer --format "{{range .Config.Env}}{{println .}}{{end}}" 2>&1) -split "`n" | + Where-Object { $_ -match "^POSTGRES_PASSWORD=" } | + ForEach-Object { $_ -replace "^POSTGRES_PASSWORD=", "" } | + Select-Object -First 1 +if (-not $pgPassword) { + Log "FEHLER: POSTGRES_PASSWORD nicht im Container gefunden." + exit 1 +} + +# --- Datenbank-Dump --- +Log "Starte pg_dump fuer Datenbank 'gerbilmanager'..." +try { + docker exec -e "PGPASSWORD=$pgPassword" $pgContainer ` + pg_dump --clean --if-exists --format=plain --username=postgres gerbilmanager ` + | Out-File -FilePath $dbDumpFile -Encoding UTF8 + + $dumpSize = [math]::Round((Get-Item $dbDumpFile).Length / 1KB, 1) + Log "Datenbank-Dump erstellt: $dbDumpFile ($dumpSize KB)" +} catch { + Log "FEHLER beim Datenbank-Dump: $_" + exit 1 +} + +# Dump-Validierung: muss mindestens 'CREATE TABLE' enthalten +$dumpContent = Get-Content $dbDumpFile -Raw -ErrorAction SilentlyContinue +if ($dumpContent -notlike "*CREATE TABLE*" -and $dumpContent -notlike "*PostgreSQL*") { + Log "WARNUNG: Dump-Datei sieht ungueltig aus - pruefen Sie $dbDumpFile manuell." +} + +# --- Fotos sichern --- +if (Test-Path $PhotosRoot) { + $photoCount = (Get-ChildItem $PhotosRoot -File -ErrorAction SilentlyContinue).Count + if ($photoCount -gt 0) { + Log "Komprimiere $photoCount Fotos nach $photoZip..." + try { + Compress-Archive -Path "$PhotosRoot\*" -DestinationPath $photoZip -Force + $zipSize = [math]::Round((Get-Item $photoZip).Length / 1MB, 1) + Log "Foto-Archiv erstellt: $photoZip ($zipSize MB)" + } catch { + Log "WARNUNG: Foto-Backup fehlgeschlagen: $_" + } + } else { + Log "Kein Fotoordner oder keine Fotos vorhanden - Foto-Backup uebersprungen." + } +} else { + Log "Fotoordner nicht gefunden ($PhotosRoot) - Foto-Backup uebersprungen." +} + +# --- Rotation: alte Backups loeschen --- +Log "Rotiere Backups (behalte letzte $KeepDays Tage)..." +$cutoff = (Get-Date).AddDays(-$KeepDays) +$oldBackups = Get-ChildItem -Path $BackupRoot -Directory | + Where-Object { $_.LastWriteTime -lt $cutoff } +foreach ($old in $oldBackups) { + Log "Loesche altes Backup: $($old.FullName)" + Remove-Item $old.FullName -Recurse -Force +} +$remaining = @(Get-ChildItem -Path $BackupRoot -Directory).Count +Log "Backup abgeschlossen. Vorhandene Backups: $remaining" diff --git a/deploy/scripts/Restore-GerbilManager.ps1 b/deploy/scripts/Restore-GerbilManager.ps1 new file mode 100644 index 0000000..9e96bbb --- /dev/null +++ b/deploy/scripts/Restore-GerbilManager.ps1 @@ -0,0 +1,152 @@ +<# +.SYNOPSIS + Stellt GerbilManager aus einem Backup wieder her. + +.DESCRIPTION + WARNUNG: Ueberschreibt alle aktuellen Datenbankdaten und Fotos! + + 1. Stoppt GerbilManager (falls laufend). + 2. Stellt die Datenbank aus dem pg_dump-SQL-File wieder her. + 3. Entpackt das Foto-Archiv. + 4. Startet GerbilManager neu. + +.PARAMETER BackupDir + Pfad zum Backup-Unterverzeichnis (z.B. C:\gerbil-data\backups\2026-06-06_03-00). + Wenn nicht angegeben, wird das neueste verfuegbare Backup verwendet. + +.PARAMETER PhotosRoot + Pfad zum Fotoordner. Standard: C:\gerbil-data\photos + +.PARAMETER NoRestart + GerbilManager nach der Wiederherstellung NICHT neu starten. + +.EXAMPLE + # Neuestes Backup wiederherstellen + .\Restore-GerbilManager.ps1 + + # Bestimmtes Backup wiederherstellen + .\Restore-GerbilManager.ps1 -BackupDir "C:\gerbil-data\backups\2026-06-05_03-00" +#> +param( + [string]$BackupDir = "", + [string]$PhotosRoot = "C:\gerbil-data\photos", + [switch]$NoRestart +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$backupRoot = "C:\gerbil-data\backups" + +function Log([string]$msg) { + Write-Host "[$(Get-Date -Format 'HH:mm:ss')] $msg" +} + +# --- Backup-Verzeichnis bestimmen --- +if ($BackupDir -eq "") { + $latest = Get-ChildItem -Path $backupRoot -Directory | + Sort-Object LastWriteTime -Descending | + Select-Object -First 1 + if (-not $latest) { + Log "FEHLER: Kein Backup gefunden in $backupRoot" + exit 1 + } + $BackupDir = $latest.FullName +} + +if (-not (Test-Path $BackupDir)) { + Log "FEHLER: Backup-Verzeichnis nicht gefunden: $BackupDir" + exit 1 +} + +# --- Dateien im Backup suchen --- +$sqlFile = Get-ChildItem -Path $BackupDir -Filter "*.sql" | Select-Object -First 1 +$zipFile = Get-ChildItem -Path $BackupDir -Filter "*.zip" | Select-Object -First 1 + +if (-not $sqlFile) { + Log "FEHLER: Kein SQL-Dump (.sql) in $BackupDir gefunden." + exit 1 +} + +Log "Wiederherstellung aus: $BackupDir" +Log " Datenbank: $($sqlFile.Name)" +if ($zipFile) { Log " Fotos : $($zipFile.Name)" } + +# --- Bestaetigung --- +Write-Host "" +Write-Host "WARNUNG: Alle aktuellen Datenbankdaten und Fotos werden UNWIDERRUFLICH ueberschrieben!" -ForegroundColor Red +$confirm = Read-Host "Fortfahren? (ja/nein)" +if ($confirm -ne "ja") { + Log "Abgebrochen." + exit 0 +} + +# --- GerbilManager stoppen (falls laufend) --- +$stopScript = Join-Path $PSScriptRoot "Stop-GerbilManager.ps1" +if (Test-Path $stopScript) { + Log "Stoppe GerbilManager..." + & $stopScript + Start-Sleep -Seconds 3 +} + +# --- Postgres-Container finden --- +Log "Suche Postgres-Container..." +$containers = docker ps --format "{{.Names}}" 2>&1 +$pgContainer = $containers -split "`n" | Where-Object { $_ -match "postgres" } | Select-Object -First 1 +if (-not $pgContainer) { + # Container koennte gestoppt sein - starten wir ihn kurz + Log "Postgres-Container laeuft nicht. Starte ihn zunaechst..." + Log "Bitte starten Sie GerbilManager kurz einmal (Start-GerbilManager.ps1) und versuchen Sie es erneut." + exit 1 +} +$pgContainer = $pgContainer.Trim() +Log "Container: $pgContainer" + +# Aspire setzt scram-sha-256 Auth - Passwort aus Container-Env lesen +$pgPassword = (docker inspect $pgContainer --format "{{range .Config.Env}}{{println .}}{{end}}" 2>&1) -split "`n" | + Where-Object { $_ -match "^POSTGRES_PASSWORD=" } | + ForEach-Object { $_ -replace "^POSTGRES_PASSWORD=", "" } | + Select-Object -First 1 +if (-not $pgPassword) { + Log "FEHLER: POSTGRES_PASSWORD nicht im Container gefunden." + exit 1 +} + +# --- Datenbankwiederherstellung --- +Log "Stelle Datenbank wieder her aus $($sqlFile.Name)..." + +# Verbindungen trennen +docker exec -e "PGPASSWORD=$pgPassword" $pgContainer psql -U postgres -c ` + "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'gerbilmanager' AND pid <> pg_backend_pid();" | Out-Null + +# SQL-Dump einspielen (--clean im Dump macht DROP TABLE IF EXISTS vor CREATE) +Get-Content $sqlFile.FullName -Raw | docker exec -i -e "PGPASSWORD=$pgPassword" $pgContainer psql -U postgres gerbilmanager +if ($LASTEXITCODE -ne 0) { + Log "FEHLER: Datenbankwiederherstellung fehlgeschlagen (exit $LASTEXITCODE)." + exit 1 +} +Log "Datenbankwiederherstellung erfolgreich." + +# --- Fotos wiederherstellen --- +if ($zipFile) { + Log "Stelle Fotos wieder her..." + if (Test-Path $PhotosRoot) { + Remove-Item "$PhotosRoot\*" -Recurse -Force -ErrorAction SilentlyContinue + } else { + New-Item -ItemType Directory -Force -Path $PhotosRoot | Out-Null + } + Expand-Archive -Path $zipFile.FullName -DestinationPath $PhotosRoot -Force + $count = (Get-ChildItem $PhotosRoot -File).Count + Log "Fotos wiederhergestellt: $count Dateien." +} + +# --- GerbilManager neu starten --- +if (-not $NoRestart) { + $startScript = Join-Path $PSScriptRoot "Start-GerbilManager.ps1" + if (Test-Path $startScript) { + Log "Starte GerbilManager neu..." + & $startScript + } +} + +Log "Wiederherstellung abgeschlossen."