From 3c5764ff6abbfcbbfbfa06c195b059e62f77379e Mon Sep 17 00:00:00 2001 From: Rasmus Wejlgaard Date: Fri, 5 Jun 2026 21:21:06 +0100 Subject: [PATCH] backup: keep deleted/overwritten versions instead of mirroring them away The nightly job runs 'rclone sync', which permanently deletes or overwrites objects at the B2 destination. That means an accidental deletion or a ransomware encryption on /hdd propagates straight to the backup on the next run, leaving no clean copy. Add --backup-dir so every superseded version is moved into a dated folder under _versions/ rather than thrown away, and prune that folder after 30 days so it doesn't grow unbounded. --- ansible/scripts/hdd-backup.sh | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/ansible/scripts/hdd-backup.sh b/ansible/scripts/hdd-backup.sh index 7385b18..a28a932 100755 --- a/ansible/scripts/hdd-backup.sh +++ b/ansible/scripts/hdd-backup.sh @@ -7,6 +7,15 @@ DIRS=(archive backups stash syncthing ftp) EMAIL="pez@pez.sh" SUBJECT="HDD Backup Report - $(date '+%Y-%m-%d %H:%M')" +# Versioning: a plain `rclone sync` permanently deletes/overwrites objects at +# the destination, so a deletion or ransomware encryption on /hdd would +# propagate to the backup on the next run. Instead, move every superseded +# version into a dated folder under $VERSIONS so it can be recovered, then +# prune anything older than $RETENTION_DAYS to cap storage. +STAMP="$(date '+%Y-%m-%d_%H%M%S')" +VERSIONS="$BUCKET/_versions" +RETENTION_DAYS=30 + failures=() report="" size_error="" @@ -16,7 +25,7 @@ for dir in "${DIRS[@]}"; do dst="$BUCKET/$dir" echo "Syncing $src -> $dst" - if output=$(rclone sync "$src" "$dst" -v 2>&1); then + if output=$(rclone sync "$src" "$dst" --backup-dir "$VERSIONS/$STAMP/$dir" -v 2>&1); then rc=0 else rc=$? @@ -28,6 +37,14 @@ for dir in "${DIRS[@]}"; do report+="=== $dir ===\n$output\n\n" done +# Prune versioned copies older than the retention window. +if prune_output=$(rclone delete "$VERSIONS" --min-age "${RETENTION_DAYS}d" -v 2>&1); then + : +else + failures+=("version-prune") + report+="=== Version Prune Error ===\n$prune_output\n\n" +fi + # Get bucket storage usage if bucket_usage=$(rclone size "$BUCKET" 2>&1); then :