From 7a98c8852e742c7ef1fa8f03c16579188a388c2b Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Sun, 26 Jul 2026 00:33:57 +0200 Subject: [PATCH] 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) --- .gitignore | 3 +++ scripts/backup.ps1 | 64 +++++++++++++++++++++++++++++++++++++++++++++ scripts/backup.sh | 43 ++++++++++++++++++++++++++++++ scripts/restore.ps1 | 62 +++++++++++++++++++++++++++++++++++++++++++ scripts/restore.sh | 57 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 229 insertions(+) create mode 100644 scripts/backup.ps1 create mode 100644 scripts/backup.sh create mode 100644 scripts/restore.ps1 create mode 100644 scripts/restore.sh diff --git a/.gitignore b/.gitignore index c02a8c7..26ee177 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,9 @@ yarn-error.log* /data /postgres-data +# Sicherungen aus scripts/backup.* – enthalten alle Daten der Instanz +/backups + # editors .idea/ .vscode/* diff --git a/scripts/backup.ps1 b/scripts/backup.ps1 new file mode 100644 index 0000000..6eb6ba4 --- /dev/null +++ b/scripts/backup.ps1 @@ -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`"" diff --git a/scripts/backup.sh b/scripts/backup.sh new file mode 100644 index 0000000..9153959 --- /dev/null +++ b/scripts/backup.sh @@ -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\"" diff --git a/scripts/restore.ps1 b/scripts/restore.ps1 new file mode 100644 index 0000000..e3ff789 --- /dev/null +++ b/scripts/restore.ps1 @@ -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 diff --git a/scripts/restore.sh b/scripts/restore.sh new file mode 100644 index 0000000..45a35b1 --- /dev/null +++ b/scripts/restore.sh @@ -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 [--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 [--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."