From e065f352c7db0f9c5079cb41f589ab04bd9d6d78 Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Sun, 12 Apr 2026 07:42:47 +0200 Subject: [PATCH] =?UTF-8?q?Fix:=20Backup-Script=20mit=20smarter=20Rotation?= =?UTF-8?q?=20und=20Lock-Schutz=20gegen=20parallele=20L=C3=A4ufe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Changes.md | 9 +++++ scripts/backup.sh | 94 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 93 insertions(+), 10 deletions(-) diff --git a/Changes.md b/Changes.md index 465a631..d3aba7c 100644 --- a/Changes.md +++ b/Changes.md @@ -2,6 +2,15 @@ --- +## 2026-04-12 – Backup: smarte Rotation und Lock-Schutz + +### Geändert +- `scripts/backup.sh`: Rotation auf 7 Tages- / 4 Wochen- / 12 Monats-Buckets umgestellt (statt pauschaler 90-Tage-Löschung) +- `scripts/backup.sh`: `flock`-Lock verhindert parallele Läufe bei Verzögerungen +- `scripts/backup.sh`: Pfade sind absolut (Script funktioniert auch aus Cron ohne Verzeichniswechsel) + +--- + ## 2026-04-12 – Uploads: named Volume → Bind Mount ### Geändert diff --git a/scripts/backup.sh b/scripts/backup.sh index 4e49b98..7ab0aaa 100755 --- a/scripts/backup.sh +++ b/scripts/backup.sh @@ -1,18 +1,37 @@ #!/bin/bash -# Steuerkram – Backup-Script +# Steuerkram – Backup-Script mit smarter Rotation # GoBD: Datensicherung muss regelmäßig erfolgen. # Usage: ./scripts/backup.sh +# +# Retention: +# - letzte 7 Tage: täglich (ein Backup pro Tag) +# - Tage 8 – 28: wöchentlich +# - Tage 29 – 365: monatlich +# - älter als 1 Jahr: werden gelöscht -set -e +set -eo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +BACKUP_DIR="$PROJECT_DIR/backups" +LOCK_FILE="/tmp/steuerkram-backup.lock" + +# Verhindert mehrfachen gleichzeitigen Start (z.B. bei Verzögerung durch langen Backup-Lauf) +exec 9>"$LOCK_FILE" +if ! flock -n 9; then + echo "$(date '+%Y-%m-%d %H:%M:%S') WARN: Backup läuft bereits (Lock: $LOCK_FILE). Abbruch." >&2 + exit 1 +fi TIMESTAMP=$(date +%Y%m%d_%H%M%S) -BACKUP_DIR="./backups" mkdir -p "$BACKUP_DIR" -echo "Erstelle Backup $TIMESTAMP..." +echo "$(date '+%Y-%m-%d %H:%M:%S') Erstelle Backup $TIMESTAMP..." # MariaDB Dump (aus dem Container) -docker compose exec -T db mariadb-dump \ +docker compose -f "$PROJECT_DIR/docker-compose.yml" \ + --project-directory "$PROJECT_DIR" \ + exec -T db mariadb-dump \ --user=steuerkram \ --password=steuerkram \ --single-transaction \ @@ -21,12 +40,67 @@ docker compose exec -T db mariadb-dump \ steuerkram > "$BACKUP_DIR/steuerkram_db_$TIMESTAMP.sql" # Uploads sichern -tar -czf "$BACKUP_DIR/steuerkram_uploads_$TIMESTAMP.tar.gz" ./uploads/ +tar -czf "$BACKUP_DIR/steuerkram_uploads_$TIMESTAMP.tar.gz" \ + -C "$PROJECT_DIR" uploads/ -echo "Backup abgeschlossen:" echo " DB: $BACKUP_DIR/steuerkram_db_$TIMESTAMP.sql" echo " Uploads: $BACKUP_DIR/steuerkram_uploads_$TIMESTAMP.tar.gz" -# Alte Backups löschen (älter als 90 Tage) -find "$BACKUP_DIR" -name "steuerkram_*" -mtime +90 -delete -echo "Alte Backups bereinigt (>90 Tage)." +# --- Smarte Rotation --- +echo "" +echo "Starte Rotation..." + +NOW=$(date +%s) +declare -A keep_daily # YYYYMMDD → timestamp (letzte 7 Tage, täglich) +declare -A keep_weekly # Wochennr. → timestamp (Tage 8–28, wöchentlich) +declare -A keep_monthly # YYYYMM → timestamp (Tage 29–365, monatlich) + +# Alle vorhandenen Timestamps ermitteln, älteste zuerst +# (neuere Einträge überschreiben Buckets → neuestes Backup pro Periode bleibt) +mapfile -t TIMESTAMPS < <( + for f in "$BACKUP_DIR"/steuerkram_db_????????_??????.sql; do + [ -f "$f" ] || continue + basename "$f" .sql | sed 's/steuerkram_db_//' + done | sort +) + +for TS in "${TIMESTAMPS[@]}"; do + DATE_PART="${TS:0:8}" # YYYYMMDD + YEAR="${TS:0:4}" + MONTH="${TS:4:2}" + DAY="${TS:6:2}" + + FILE_EPOCH=$(date -d "${YEAR}-${MONTH}-${DAY}" +%s) + AGE_DAYS=$(( (NOW - FILE_EPOCH) / 86400 )) + WEEK_BUCKET=$(( AGE_DAYS / 7 )) + + if [ "$AGE_DAYS" -le 7 ]; then keep_daily["$DATE_PART"]="$TS" + elif [ "$AGE_DAYS" -le 28 ]; then keep_weekly["$WEEK_BUCKET"]="$TS" + elif [ "$AGE_DAYS" -le 365 ]; then keep_monthly["${YEAR}${MONTH}"]="$TS" + fi + # Älter als 365 Tage → kein Bucket → wird weiter unten gelöscht +done + +# Keep-Set aus allen Buckets zusammenführen +declare -A KEEP +for TS in \ + $([ ${#keep_daily[@]} -gt 0 ] && echo "${keep_daily[@]}") \ + $([ ${#keep_weekly[@]} -gt 0 ] && echo "${keep_weekly[@]}") \ + $([ ${#keep_monthly[@]} -gt 0 ] && echo "${keep_monthly[@]}") ; do + KEEP["$TS"]=1 +done + +# Backups löschen, die in keinem Bucket landen +DELETED=0 +for TS in "${TIMESTAMPS[@]}"; do + if [ -z "${KEEP[$TS]+x}" ]; then + rm -f "$BACKUP_DIR/steuerkram_db_$TS.sql" + rm -f "$BACKUP_DIR/steuerkram_uploads_$TS.tar.gz" + echo " Gelöscht: $TS" + DELETED=$((DELETED + 1)) + fi +done + +echo " Behalten: ${#KEEP[@]} | täglich: ${#keep_daily[@]} | wöchentlich: ${#keep_weekly[@]} | monatlich: ${#keep_monthly[@]}" +echo " Gelöscht: $DELETED" +echo "$(date '+%Y-%m-%d %H:%M:%S') Backup & Rotation abgeschlossen."