#!/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