OPS-1: fix backup/restore scripts — ASCII-only + array coerce for Count

- 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) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 07:09:53 +02:00
parent 44f306d18f
commit 6a37f9e46e
2 changed files with 278 additions and 0 deletions

View File

@@ -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"