#!/bin/sh # Sichert Datenbank UND Bilder der Kleiderboerse. # # Beides gehoert zusammen: eine Datenbank ohne die zugehoerigen Fotos ist # wertlos, und Fotos ohne Datenbank sind ein Haufen Dateien mit zufaelligen # Namen. Anders als bei der Kantone-App laesst sich hier nichts neu # importieren - was weg ist, ist weg. # # Warum nicht einfach cp: SQLite schreibt nicht atomar. Eine Kopie mitten im # Schreibvorgang kann unbrauchbar sein, und das faellt erst auf, wenn man sie # braucht. "VACUUM INTO" liefert auch bei laufendem Betrieb einen # konsistenten Stand. # # Aufruf (Container muss laufen): # ./sicherung.sh [zielordner] # # Taeglich um 03:00 per cron: # 0 3 * * * /pfad/zu/sicherung.sh /pfad/zu/sicherungen >> /var/log/kleiderboerse-sicherung.log 2>&1 set -eu CONTAINER="${KLEIDER_CONTAINER:-kleiderboerse-app-1}" ZIEL="${1:-./sicherungen}" BEHALTEN="${BEHALTEN:-7}" if ! docker ps --format '{{.Names}}' | grep -qx "$CONTAINER"; then echo "FEHLER: Container '$CONTAINER' laeuft nicht." >&2 echo "Anderer Name? Ueber KLEIDER_CONTAINER setzen." >&2 exit 1 fi mkdir -p "$ZIEL" STAND="$(date +%Y%m%d-%H%M%S)" ORDNER="$ZIEL/kleiderboerse-$STAND" mkdir -p "$ORDNER" # Erzeugen und pruefen laufen im Container: der Host braucht nur Docker. # Die Pruefung oeffnet bewusst die KOPIE, nicht das Original - nur dann sagt # die Zahl etwas ueber die Sicherung aus. ANZAHL="$(docker exec "$CONTAINER" python -c ' import os, sqlite3 from app.config import einstellungen # Nicht raten: die Anwendung kennt ihre Pfade, und sie koennen ueber # DATABASE_PATH / UPLOAD_PATH abweichen. quelle = str(einstellungen().datenbank_pfad) ziel = "/tmp/sicherung.sqlite" if os.path.exists(ziel): os.unlink(ziel) con = sqlite3.connect(quelle) con.execute("VACUUM INTO ?", (ziel,)) con.close() kopie = sqlite3.connect(ziel) print(kopie.execute("SELECT COUNT(*) FROM items").fetchone()[0]) kopie.close() ' 2>/dev/null)" if ! printf '%s' "${ANZAHL:-}" | grep -qE '^[0-9]+$'; then echo "FEHLER: Sicherung der Datenbank fehlgeschlagen (keine Zahl zurueck)." >&2 rmdir "$ORDNER" 2>/dev/null || true exit 1 fi docker cp "$CONTAINER:/tmp/sicherung.sqlite" "$ORDNER/kleiderboerse.sqlite" docker exec "$CONTAINER" rm -f /tmp/sicherung.sqlite # Die Bilder dazu. Ohne sie zeigt die wiederhergestellte Galerie nur Luecken. UPLOADS="$(docker exec "$CONTAINER" python -c ' from app.config import einstellungen print(einstellungen().upload_pfad) ')" docker cp "$CONTAINER:$UPLOADS" "$ORDNER/uploads" 2>/dev/null || mkdir -p "$ORDNER/uploads" BILDER="$(find "$ORDNER/uploads" -name '*.webp' | wc -l)" echo "$(date '+%d.%m.%Y %H:%M') $ORDNER ($ANZAHL Eintraege, $BILDER Bilder, $(du -sh "$ORDNER" | cut -f1))" # Warnen, wenn Eintraege da sind, aber keine Bilder - dann stimmt am Pfad # etwas nicht und die Sicherung waere halb. if [ "$ANZAHL" -gt 0 ] && [ "$BILDER" -eq 0 ]; then echo " WARNUNG: $ANZAHL Eintraege, aber kein einziges Bild gesichert. Upload-Pfad pruefen." >&2 fi # Alte Staende entfernen, neueste zuerst behalten. ls -1dt "$ZIEL"/kleiderboerse-*/ 2>/dev/null | tail -n "+$((BEHALTEN + 1))" | while read -r alt; do echo " entferne alten Stand: $(basename "$alt")" rm -rf "$alt" done