- 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>
127 lines
4.5 KiB
PowerShell
127 lines
4.5 KiB
PowerShell
<#
|
|
.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"
|