Files
dogfather-universe/server-internal/sicherung.sh
T
DogFatherGitandClaude Opus 5 d62ff53062 Datensicherung: taeglicher Stand und geprueft Wiederherstellung
Bisher gab es keine Sicherung. Ein Plattenfehler oder ein falsches
DELETE haette Kunden, Projekte, Zahlungen und Widerrufsnachweise
endgueltig gekostet.

sicherung.sh legt taeglich einen Stand an -- mit SQLites eigenem
".backup", nicht mit "cp". Der Grund ist messbar: Die Datenbank ist
778 KB gross, ihr WAL 4,1 MB. Eine Kopie der .db allein waere also
nicht bloss veraltet, sondern weitgehend leer. Ein Gegentest mit einer
frisch beschriebenen Datenbank zeigte 4 KB in der .db gegen 2,1 MB im
WAL.

Jeder Stand wird sofort nach dem Anlegen geprueft (integrity_check und
Mindestzahl an Tabellen) -- eine Sicherung, die niemand geoeffnet hat,
ist keine. 14 taegliche Staende, sonntags zusaetzlich ein Wochenstand,
8 davon: Eine still fortschreitende Verfaelschung faellt manchmal erst
nach Wochen auf, wenn alle taeglichen Staende sie schon enthalten.

wiederherstellen.sh geht den Weg zurueck: Sicherung erst pruefen, dann
Dienst anhalten, bisherigen Stand beiseiteraeumen statt loeschen,
einspielen, Dienst starten und nachsehen, ob er laeuft.

Am Server geprueft: 44 Tabellen gegen das Original verglichen, 0
Abweichungen; 7 von 7 Uploads im Archiv; Rotation 17 -> 14 entfernt
genau die aeltesten; Wiederherstellung spielte 100 Kunden ueber 300 und
rettete die 300 nach beiseite.

Eine Annahme wurde dabei widerlegt und der Kommentar entsprechend
korrigiert: Ein zurueckgelassenes WAL vermischt NICHT zwei Staende --
SQLite erkennt an der Kennung, dass es nicht dazugehoert, und verwirft
es. Der echte Schutz ist das Anhalten des Dienstes.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-26 00:25:11 +02:00

126 lines
4.7 KiB
Bash
Executable File

#!/bin/bash
# =====================================================================
# Taegliche Sicherung des internen Bereichs.
#
# WARUM NICHT EINFACH "cp"
#
# SQLite schreibt im WAL-Betrieb nicht direkt in die .db-Datei, sondern
# zuerst in eine Begleitdatei (.db-wal). Auf diesem Server ist dieses
# WAL 4,1 MB gross -- also groesser als die Datenbank selbst mit
# 778 KB. Eine Kopie der .db allein waere damit nicht bloss veraltet,
# sondern unvollstaendig: Alles seit dem letzten Zusammenfuehren fehlt.
#
# Kopiert man beide Dateien nacheinander, waehrend geschrieben wird,
# passen sie ausserdem womoeglich nicht zusammen -- das Ergebnis ist
# eine Datei, die erst beim Wiederherstellen als kaputt auffaellt. Und
# das ist der schlechteste Zeitpunkt.
#
# ".backup" macht es richtig: Der Befehl kennt das WAL, sperrt sauber
# und liefert einen in sich stimmigen Stand, auch waehrend der Dienst
# weiterlaeuft.
#
# WAS GESICHERT WIRD
# - die Datenbank (Kunden, Projekte, Zahlungen, Widerrufsnachweise)
# - die hochgeladenen Dateien (rund 16 MB)
#
# Beides gehoert zusammen: Eine Datenbank, die auf Dateien verweist, die
# es nicht mehr gibt, ist nur die halbe Wiederherstellung.
# =====================================================================
set -uo pipefail
QUELLE_DB="/var/lib/dogfather-internal/dogfather-internal.db"
QUELLE_UPLOADS="/var/lib/dogfather-internal/uploads"
ZIEL="/var/backups/dogfather"
PROTOKOLL="$ZIEL/sicherung.log"
# Wie viele Staende aufbewahrt werden.
TAEGLICH=14
WOECHENTLICH=8
melde() {
echo "$(date '+%Y-%m-%d %H:%M:%S') $*" >> "$PROTOKOLL"
echo "$*"
}
fehler() {
melde "FEHLER: $*"
exit 1
}
mkdir -p "$ZIEL/taeglich" "$ZIEL/woechentlich" || fehler "Zielverzeichnis nicht anlegbar"
STEMPEL=$(date '+%Y-%m-%d')
ARBEIT="$ZIEL/taeglich/$STEMPEL"
mkdir -p "$ARBEIT"
# ---------------------------------------------------------------------
# 1. Datenbank
# ---------------------------------------------------------------------
[ -f "$QUELLE_DB" ] || fehler "Datenbank nicht gefunden: $QUELLE_DB"
sqlite3 "$QUELLE_DB" ".backup '$ARBEIT/dogfather-internal.db'" \
|| fehler "Sicherung der Datenbank fehlgeschlagen"
# Eine Sicherung, die niemand geprueft hat, ist keine Sicherung.
# Der Test kostet Sekunden und beantwortet die einzige Frage, auf die es
# im Ernstfall ankommt: Laesst sich die Datei ueberhaupt oeffnen?
PRUEFUNG=$(sqlite3 "$ARBEIT/dogfather-internal.db" "PRAGMA integrity_check;" 2>&1)
[ "$PRUEFUNG" = "ok" ] || fehler "Sicherung ist beschaedigt: $PRUEFUNG"
# Gegenprobe auf Inhalt: Eine formal heile, aber leere Datei waere der
# stillste aller Datenverluste.
TABELLEN=$(sqlite3 "$ARBEIT/dogfather-internal.db" \
"SELECT count(*) FROM sqlite_master WHERE type='table';" 2>/dev/null)
[ "${TABELLEN:-0}" -gt 5 ] || fehler "Sicherung enthaelt nur $TABELLEN Tabellen -- zu wenig"
# ---------------------------------------------------------------------
# 2. Hochgeladene Dateien
# ---------------------------------------------------------------------
if [ -d "$QUELLE_UPLOADS" ]; then
tar -czf "$ARBEIT/uploads.tar.gz" -C "$(dirname "$QUELLE_UPLOADS")" \
"$(basename "$QUELLE_UPLOADS")" 2>/dev/null \
|| fehler "Sicherung der Dateien fehlgeschlagen"
tar -tzf "$ARBEIT/uploads.tar.gz" > /dev/null 2>&1 \
|| fehler "Dateisicherung ist nicht lesbar"
else
melde "Hinweis: kein Upload-Verzeichnis vorhanden"
fi
GROESSE=$(du -sh "$ARBEIT" | cut -f1)
melde "Sicherung $STEMPEL angelegt ($GROESSE, $TABELLEN Tabellen)"
# ---------------------------------------------------------------------
# 3. Wochenstand
#
# Sonntags wandert eine Ausfertigung in die laengere Aufbewahrung. Der
# Grund ist nicht Platz, sondern Zeit: Ein Fehler, der eine Datenbank
# still verdirbt, faellt manchmal erst nach Wochen auf -- und dann sind
# vierzehn taegliche Staende alle schon vom selben Fehler betroffen.
# ---------------------------------------------------------------------
if [ "$(date '+%u')" = "7" ]; then
cp -r "$ARBEIT" "$ZIEL/woechentlich/$STEMPEL" 2>/dev/null \
&& melde "Wochenstand $STEMPEL abgelegt"
fi
# ---------------------------------------------------------------------
# 4. Alte Staende entfernen
# ---------------------------------------------------------------------
entferne_alte() {
local ordner="$1" behalten="$2"
local anzahl
anzahl=$(find "$ordner" -mindepth 1 -maxdepth 1 -type d | wc -l)
if [ "$anzahl" -gt "$behalten" ]; then
find "$ordner" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' \
| sort | head -n "$((anzahl - behalten))" \
| while read -r alt; do
rm -rf "${ordner:?}/$alt" && melde "alten Stand entfernt: $alt"
done
fi
}
entferne_alte "$ZIEL/taeglich" "$TAEGLICH"
entferne_alte "$ZIEL/woechentlich" "$WOECHENTLICH"
melde "fertig"
exit 0