Skip to content

Instantly share code, notes, and snippets.

@seqis
Created April 25, 2025 18:31
Show Gist options
  • Select an option

  • Save seqis/fb43b874d37c69d29b6c4b0472b4e9d8 to your computer and use it in GitHub Desktop.

Select an option

Save seqis/fb43b874d37c69d29b6c4b0472b4e9d8 to your computer and use it in GitHub Desktop.
This bash script performs both mirrored and versioned backups every 30 minutes—ideal for running via cron or Task Scheduler—using rsync with hard-link deduplication, time-slot logic, and detailed logging, all while ensuring clean snapshots and efficient storage use.
#!/bin/bash
# ---------------------------------------------------------------------------
# CRITICAL CAPSULE BACKUP SCRIPT (PUBLISHABLE VERSION)
# ---------------------------------------------------------------------------
# This script performs two types of backups every 30 minutes:
#
# 1. **MIRRORED BACKUPS**: One-way sync that mirrors directories to a target.
# - No versioning; it's a direct snapshot of the source as it exists.
#
# 2. **VERSIONED BACKUPS**: Uses rsync with `--link-dest` to create efficient
# snapshots that hard-link unchanged files to the previous backup.
# - This enables full snapshots with minimal storage overhead.
# - Backups are named by hour and time slot: e.g., `1am/`, `1am_b/`, etc.
#
# ---------------------------------------------------------------------------
# ❖ HOW TO USE:
# - Recommended to run as a scheduled task:
# - **Linux (via crontab)**:
# `15,45 * * * * /home/username/backup_scripts/critical_capsule`
# - **Windows (via Task Scheduler)**:
# - Set to run at :15 and :45 of every hour.
#
# ❖ IF RUN MANUALLY:
# - The script determines the backup folder using the current minute:
# - :00–:30 → saved as `<hour>am/pm/`
# - :31–:59 → saved as `<hour>am/pm_b/`
# - This means:
# - A run at 9:22pm will back up to `9pm/`
# - A run at 9:35pm will back up to `9pm_b/`
# - A warning is logged if not run at :15 or :45, but the script will proceed.
#
# ❖ LOGGING:
# - All actions are logged to:
# `/home/username/logs/critical-capsule.log`
# ---------------------------------------------------------------------------
# Define source directories to mirror
SOURCES=(
"/mnt/data/projects"
"/home/username/Documents"
"/home/username/Downloads"
"/home/username/Pictures"
"/home/username/Videos"
"/home/username/Webcam"
"/home/username/code"
"/home/username/backup_scripts"
)
# Directories to version separately (with deduplication)
VERSIONED=(
"/mnt/data/projects/notes/Obsidian"
"/home/username/Documents/Personal/Obsidian-Vault"
"/home/username/code"
"/home/username/backup_scripts"
)
# Rsync exclude patterns
EXCLUDES=(
"--exclude=/mnt/data/projects/archived"
"--exclude=/home/username/Downloads/Temp"
"--exclude=/home/username/code/env"
"--exclude=*.tmp"
)
# Paths
TARGET_BASE="/mnt/backup-drive/critical-capsule"
MIRROR_DIR="$TARGET_BASE/mirror"
VERSION_BASE="$TARGET_BASE/versioned"
LOG_FILE="/home/username/logs/critical-capsule.log"
# Determine time slot
HOUR=$(date +"%I" | sed 's/^0*//')
MERIDIEM=$(date +"%P")
MINUTE=$(date +"%M")
# Determine suffix based on minute range
SUFFIX=""
if (( 10#$MINUTE >= 31 )); then
SUFFIX="_b"
fi
CURRENT_HOUR="${HOUR}${MERIDIEM}${SUFFIX}"
VERSION_HOUR_DIR="$VERSION_BASE/$CURRENT_HOUR"
# Warn if manual run not at 15 or 45
if [ "$MINUTE" != "15" ] && [ "$MINUTE" != "45" ]; then
echo "WARNING: Running at minute $MINUTE – outside expected :15 or :45 slots. Will back up to '$CURRENT_HOUR'." | tee -a "$LOG_FILE"
fi
# Find most recent prior version for link-dest reference
PREV_VERSION=$(find "$VERSION_BASE" -maxdepth 1 -mindepth 1 -type d -printf "%f\n" |
grep -E "^[0-9]{1,2}[ap]m(_b)?$" | sort | tail -n 1)
PREV_VERSION="$VERSION_BASE/$PREV_VERSION"
# Ensure mirror destination exists
mkdir -p "$MIRROR_DIR"
# ------------------------------
# 1. MIRROR SYNC
# ------------------------------
for SRC in "${SOURCES[@]}"; do
NAME=$(basename "$SRC")
DEST="$MIRROR_DIR/$NAME"
echo "Backing up $SRC to $DEST (mirror sync)..." | tee -a "$LOG_FILE"
if [ ! -d "$SRC" ]; then
echo "WARNING: Source directory $SRC does not exist. Skipping." | tee -a "$LOG_FILE"
continue
fi
if rsync -a --delete "${EXCLUDES[@]}" "$SRC/" "$DEST/" >>"$LOG_FILE" 2>&1; then
echo "SUCCESS: $SRC mirrored to $DEST" | tee -a "$LOG_FILE"
else
echo "ERROR: Failed to mirror $SRC to $DEST" | tee -a "$LOG_FILE"
fi
done
# ------------------------------
# 2. VERSIONED BACKUPS
# ------------------------------
rm -rf "$VERSION_HOUR_DIR"
mkdir -p "$VERSION_HOUR_DIR"
for VDIR in "${VERSIONED[@]}"; do
NAME=$(basename "$VDIR")
DEST="$VERSION_HOUR_DIR/$NAME"
LINK_DEST=""
if [[ -n "$PREV_VERSION" && -d "$PREV_VERSION/$NAME" ]]; then
LINK_DEST="--link-dest=$PREV_VERSION/$NAME"
fi
echo "Versioning $VDIR to $DEST using link-dest from $PREV_VERSION/$NAME..." | tee -a "$LOG_FILE"
if [ ! -d "$VDIR" ]; then
echo "WARNING: Versioned directory $VDIR does not exist. Skipping." | tee -a "$LOG_FILE"
continue
fi
if rsync -a --delete $LINK_DEST "$VDIR/" "$DEST/" >>"$LOG_FILE" 2>&1; then
echo "SUCCESS: $VDIR versioned to $DEST" | tee -a "$LOG_FILE"
else
echo "ERROR: Failed to version $VDIR to $DEST" | tee -a "$LOG_FILE"
fi
done
# ------------------------------
# Completion Timestamp
# ------------------------------
DATESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
echo "[$DATESTAMP] Mirror sync to $MIRROR_DIR and versioning to $VERSION_HOUR_DIR completed." | tee -a "$LOG_FILE"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment