feat(ops): Sicherungsskripte mit pg_dump fuer Windows und Unix
Die JSON-Sicherung im Adminbereich eignet sich zum Umziehen. Zum Wiederherstellen einer kaputten Instanz braucht es dagegen einen echten Dump - Schema, Daten, Sequenzen, Indizes. backup.ps1 und backup.sh fuehren pg_dump im Container aus, es muss also nichts auf dem Rechner installiert sein. Benutzer und Datenbankname kommen aus der .env, damit sie nicht doppelt gepflegt werden. Der Dump entsteht mit --clean --if-exists und ist damit fuer sich allein einspielbar. restore.* fragt vor dem Ueberschreiben nach und laesst sich ueber -Database beziehungsweise NEBULA_DB_NAME auf eine andere Datenbank umlenken. Damit laesst sich eine Sicherung pruefen, ohne die echte anzufassen - eine Sicherung, die nie zurueckgespielt wurde, ist keine. /backups liegt im .gitignore: die Dateien enthalten alle Daten der Instanz. Geprueft am echten Bestand: Dump 30,9 KB mit allen neun Tabellen, Rueckspielung in eine Pruefdatenbank ergab dieselben Zahlen, das Original blieb unveraendert. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,9 @@ yarn-error.log*
|
|||||||
/data
|
/data
|
||||||
/postgres-data
|
/postgres-data
|
||||||
|
|
||||||
|
# Sicherungen aus scripts/backup.* – enthalten alle Daten der Instanz
|
||||||
|
/backups
|
||||||
|
|
||||||
# editors
|
# editors
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Byteweise Sicherung der Nebula-Datenbank mit pg_dump.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Ergaenzt den JSON-Export im Adminbereich um eine vollstaendige
|
||||||
|
Datenbanksicherung: Schema, Daten, Sequenzen, Indizes. Genau das, was
|
||||||
|
man zum Wiederherstellen einer kaputten Instanz braucht.
|
||||||
|
|
||||||
|
Laeuft die Datenbank im mitgelieferten Container, wird pg_dump dort
|
||||||
|
ausgefuehrt - auf dem Rechner selbst muss dann nichts installiert sein.
|
||||||
|
|
||||||
|
.PARAMETER OutDir
|
||||||
|
Zielverzeichnis. Vorgabe: .\backups
|
||||||
|
|
||||||
|
.PARAMETER Container
|
||||||
|
Name des Postgres-Containers. Vorgabe: nebula-db
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\scripts\backup.ps1
|
||||||
|
.\scripts\backup.ps1 -OutDir D:\Sicherungen
|
||||||
|
#>
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$OutDir = (Join-Path $PSScriptRoot "..\backups"),
|
||||||
|
[string]$Container = "nebula-db"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# .env einlesen, damit Benutzer und Datenbankname nicht doppelt gepflegt werden.
|
||||||
|
$envFile = Join-Path $PSScriptRoot "..\.env"
|
||||||
|
$settings = @{ POSTGRES_USER = "nebula"; POSTGRES_DB = "nebula" }
|
||||||
|
if (Test-Path $envFile) {
|
||||||
|
foreach ($line in Get-Content $envFile) {
|
||||||
|
if ($line -match '^\s*([A-Z_]+)\s*=\s*"?([^"#]*)"?\s*$') {
|
||||||
|
$settings[$Matches[1]] = $Matches[2].Trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $settings["POSTGRES_USER"]
|
||||||
|
$database = $settings["POSTGRES_DB"]
|
||||||
|
|
||||||
|
if (-not (Test-Path $OutDir)) { New-Item -ItemType Directory -Path $OutDir | Out-Null }
|
||||||
|
|
||||||
|
$stamp = Get-Date -Format "yyyy-MM-dd-HHmmss"
|
||||||
|
$target = Join-Path $OutDir "nebula-$stamp.sql"
|
||||||
|
|
||||||
|
$running = docker ps --filter "name=$Container" --format "{{.Names}}"
|
||||||
|
if ($running -notcontains $Container) {
|
||||||
|
Write-Error "Container '$Container' laeuft nicht. Zuerst 'docker compose up -d db' ausfuehren."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Sichere Datenbank '$database' aus Container '$Container'..."
|
||||||
|
|
||||||
|
# --clean --if-exists macht die Datei fuer sich allein einspielbar.
|
||||||
|
docker exec $Container pg_dump -U $user -d $database --clean --if-exists | Out-File -Encoding utf8 $target
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) { Write-Error "pg_dump ist fehlgeschlagen." }
|
||||||
|
|
||||||
|
$size = [math]::Round((Get-Item $target).Length / 1KB, 1)
|
||||||
|
Write-Host "Fertig: $target ($size KB)" -ForegroundColor Green
|
||||||
|
Write-Host "Wiederherstellen mit: .\scripts\restore.ps1 -File `"$target`""
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Byteweise Sicherung der Nebula-Datenbank mit pg_dump.
|
||||||
|
#
|
||||||
|
# Ergaenzt den JSON-Export im Adminbereich um eine vollstaendige
|
||||||
|
# Datenbanksicherung: Schema, Daten, Sequenzen, Indizes.
|
||||||
|
#
|
||||||
|
# Aufruf: ./scripts/backup.sh [Zielverzeichnis]
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
out_dir="${1:-$root/backups}"
|
||||||
|
container="${NEBULA_DB_CONTAINER:-nebula-db}"
|
||||||
|
|
||||||
|
user="nebula"
|
||||||
|
database="nebula"
|
||||||
|
if [ -f "$root/.env" ]; then
|
||||||
|
# Nur die beiden benoetigten Werte lesen, ohne die Datei auszufuehren.
|
||||||
|
value_of() {
|
||||||
|
sed -n "s/^[[:space:]]*$1[[:space:]]*=[[:space:]]*\"\{0,1\}\([^\"#]*\)\"\{0,1\}[[:space:]]*$/\1/p" \
|
||||||
|
"$root/.env" | tail -n 1
|
||||||
|
}
|
||||||
|
[ -n "$(value_of POSTGRES_USER)" ] && user="$(value_of POSTGRES_USER)"
|
||||||
|
[ -n "$(value_of POSTGRES_DB)" ] && database="$(value_of POSTGRES_DB)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! docker ps --filter "name=$container" --format '{{.Names}}' | grep -qx "$container"; then
|
||||||
|
echo "Container '$container' laeuft nicht. Zuerst 'docker compose up -d db' ausfuehren." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$out_dir"
|
||||||
|
stamp="$(date +%Y-%m-%d-%H%M%S)"
|
||||||
|
target="$out_dir/nebula-$stamp.sql"
|
||||||
|
|
||||||
|
echo "Sichere Datenbank '$database' aus Container '$container'..."
|
||||||
|
# --clean --if-exists macht die Datei fuer sich allein einspielbar.
|
||||||
|
docker exec "$container" pg_dump -U "$user" -d "$database" --clean --if-exists > "$target"
|
||||||
|
|
||||||
|
size="$(du -h "$target" | cut -f1)"
|
||||||
|
echo "Fertig: $target ($size)"
|
||||||
|
echo "Wiederherstellen mit: ./scripts/restore.sh \"$target\""
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Spielt eine mit backup.ps1 erzeugte Sicherung zurueck.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
ACHTUNG: Der aktuelle Inhalt der Datenbank wird dabei ersetzt. Das Skript
|
||||||
|
fragt vorher nach, es sei denn, -Force ist gesetzt.
|
||||||
|
|
||||||
|
.PARAMETER Database
|
||||||
|
Zieldatenbank. Vorgabe: der Wert aus der .env.
|
||||||
|
|
||||||
|
Tipp: Eine Sicherung laesst sich pruefen, ohne die echte Datenbank
|
||||||
|
anzufassen - in eine leere Testdatenbank einspielen und nachsehen:
|
||||||
|
|
||||||
|
docker exec nebula-db psql -U nebula -d postgres -c "CREATE DATABASE nebula_test"
|
||||||
|
.\scripts\restore.ps1 -File .\backups\... -Database nebula_test -Force
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\scripts\restore.ps1 -File .\backups\nebula-2026-07-26-101500.sql
|
||||||
|
#>
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$File,
|
||||||
|
[string]$Database,
|
||||||
|
[string]$Container = "nebula-db",
|
||||||
|
[switch]$Force
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
if (-not (Test-Path $File)) { Write-Error "Datei nicht gefunden: $File" }
|
||||||
|
|
||||||
|
$envFile = Join-Path $PSScriptRoot "..\.env"
|
||||||
|
$settings = @{ POSTGRES_USER = "nebula"; POSTGRES_DB = "nebula" }
|
||||||
|
if (Test-Path $envFile) {
|
||||||
|
foreach ($line in Get-Content $envFile) {
|
||||||
|
if ($line -match '^\s*([A-Z_]+)\s*=\s*"?([^"#]*)"?\s*$') {
|
||||||
|
$settings[$Matches[1]] = $Matches[2].Trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $settings["POSTGRES_USER"]
|
||||||
|
$database = if ($Database) { $Database } else { $settings["POSTGRES_DB"] }
|
||||||
|
|
||||||
|
if (-not $Force) {
|
||||||
|
Write-Host "Der gesamte Inhalt der Datenbank '$database' wird ersetzt." -ForegroundColor Yellow
|
||||||
|
$answer = Read-Host "Wirklich fortfahren? (ja/nein)"
|
||||||
|
if ($answer -ne "ja") { Write-Host "Abgebrochen."; exit 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
$running = docker ps --filter "name=$Container" --format "{{.Names}}"
|
||||||
|
if ($running -notcontains $Container) {
|
||||||
|
Write-Error "Container '$Container' laeuft nicht."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Spiele '$File' ein..."
|
||||||
|
Get-Content $File -Raw | docker exec -i $Container psql -U $user -d $database -v ON_ERROR_STOP=1
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) { Write-Error "Das Einspielen ist fehlgeschlagen." }
|
||||||
|
|
||||||
|
Write-Host "Fertig. Anwendung neu starten, damit sie eine frische Verbindung aufbaut." -ForegroundColor Green
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Spielt eine mit backup.sh erzeugte Sicherung zurueck.
|
||||||
|
#
|
||||||
|
# ACHTUNG: Der aktuelle Inhalt der Datenbank wird ersetzt.
|
||||||
|
#
|
||||||
|
# Aufruf: ./scripts/restore.sh <datei.sql> [--force]
|
||||||
|
#
|
||||||
|
# Zieldatenbank ueberschreiben mit NEBULA_DB_NAME. Damit laesst sich eine
|
||||||
|
# Sicherung pruefen, ohne die echte Datenbank anzufassen:
|
||||||
|
#
|
||||||
|
# docker exec nebula-db psql -U nebula -d postgres -c "CREATE DATABASE nebula_test"
|
||||||
|
# NEBULA_DB_NAME=nebula_test ./scripts/restore.sh backups/... --force
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
file="${1:-}"
|
||||||
|
force="${2:-}"
|
||||||
|
container="${NEBULA_DB_CONTAINER:-nebula-db}"
|
||||||
|
|
||||||
|
if [ -z "$file" ]; then
|
||||||
|
echo "Aufruf: ./scripts/restore.sh <datei.sql> [--force]" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ ! -f "$file" ]; then
|
||||||
|
echo "Datei nicht gefunden: $file" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
user="nebula"
|
||||||
|
database="nebula"
|
||||||
|
if [ -f "$root/.env" ]; then
|
||||||
|
value_of() {
|
||||||
|
sed -n "s/^[[:space:]]*$1[[:space:]]*=[[:space:]]*\"\{0,1\}\([^\"#]*\)\"\{0,1\}[[:space:]]*$/\1/p" \
|
||||||
|
"$root/.env" | tail -n 1
|
||||||
|
}
|
||||||
|
[ -n "$(value_of POSTGRES_USER)" ] && user="$(value_of POSTGRES_USER)"
|
||||||
|
[ -n "$(value_of POSTGRES_DB)" ] && database="$(value_of POSTGRES_DB)"
|
||||||
|
fi
|
||||||
|
database="${NEBULA_DB_NAME:-$database}"
|
||||||
|
|
||||||
|
if [ "$force" != "--force" ]; then
|
||||||
|
printf "Der gesamte Inhalt der Datenbank '%s' wird ersetzt.\nWirklich fortfahren? (ja/nein) " "$database"
|
||||||
|
read -r answer
|
||||||
|
[ "$answer" = "ja" ] || { echo "Abgebrochen."; exit 0; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! docker ps --filter "name=$container" --format '{{.Names}}' | grep -qx "$container"; then
|
||||||
|
echo "Container '$container' laeuft nicht." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Spiele '$file' ein..."
|
||||||
|
docker exec -i "$container" psql -U "$user" -d "$database" -v ON_ERROR_STOP=1 < "$file"
|
||||||
|
|
||||||
|
echo "Fertig. Anwendung neu starten, damit sie eine frische Verbindung aufbaut."
|
||||||
Reference in New Issue
Block a user