diff --git a/backup.sh b/backup.sh index fb0944f..c0e654d 100755 --- a/backup.sh +++ b/backup.sh @@ -11,6 +11,10 @@ readonly NOW=$(date +'%Y-%m-%d') readonly THIS_RUN="${BACKUP_BASE}/${NOW}" readonly LATEST="${BACKUP_BASE}/latest" +# Retention policy +readonly KEEP_DAILY=3 # keep this many daily snapshots +readonly KEEP_WEEKLY=2 # keep this many weekly (Sunday) snapshots + # Directories (or files) to back up (without DB volumes) declare -a SOURCE_ITEMS=( "/home/oster/server" @@ -37,20 +41,15 @@ RSYNC_OPTS=( "${EXCLUDES[@]}" ) +# Link-dest array — populated later; using an array avoids unquoted-variable issues +LINK_DEST=() + # ----------------------------------------------------------------------------- # LOGGING # ----------------------------------------------------------------------------- log() { echo "[$(date +'%F %T')] $*"; } die() { log "FATAL: $*"; exit 1; } -# Locking -lockfile="${BACKUP_BASE}/.backup.lock" -cleanup() { rm -f "$lockfile"; } -trap cleanup EXIT -if ! ( set -o noclobber; echo "$$" > "$lockfile" ) 2>/dev/null; then - die "Another backup is already running (lockfile exists at $lockfile)." -fi - # ----------------------------------------------------------------------------- # FUNCTIONS # ----------------------------------------------------------------------------- @@ -60,31 +59,30 @@ check_mount() { fi } -# Rotation: keep 7 daily and 4 weekly +# Rotation: keep KEEP_DAILY daily and KEEP_WEEKLY weekly snapshots rotate_old() { - log "Applying retention policy (7 daily, 4 weekly)." - cd "$BACKUP_BASE" || die "Cannot cd into $BACKUP_BASE" + log "Applying retention policy (${KEEP_DAILY} daily, ${KEEP_WEEKLY} weekly)." - # find snapshot dirs YYYY-MM-DD only - for dir in $(ls -1 | grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'); do - keep=0 + # Pre-compute the dates to keep so we don't fork date(1) inside every inner loop iteration + declare -A keep_dates=() + for i in $(seq 0 $(( KEEP_DAILY - 1 ))); do + keep_dates["$(date -d "-$i days" +%F)"]=1 + done + last_sunday=$(date -d "last sunday" +%F) + for i in $(seq 0 $(( KEEP_WEEKLY - 1 ))); do + keep_dates["$(date -d "${last_sunday} -$(( i * 7 )) days" +%F)"]=1 + done - # Keep last 7 daily - for i in $(seq 0 6); do - [ "$dir" = "$(date -d "-$i day" +%F)" ] && keep=1 - done + # Collect snapshot dirs matching YYYY-MM-DD using a glob (nullglob makes empty glob safe) + for dir in "$BACKUP_BASE"/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]; do + [ -d "$dir" ] || continue + name=$(basename "$dir") - # Keep last 4 Sundays (weeklies) - for i in $(seq 0 27); do - d=$(date -d "last sunday -$i week" +%F) - [ "$dir" = "$d" ] && keep=1 - done - - if [ $keep -eq 0 ]; then - log " Deleting expired snapshot $dir" - rm -rf "$dir" + if [[ -v keep_dates["$name"] ]]; then + log " Keeping snapshot $name" else - log " Keeping snapshot $dir" + log " Deleting expired snapshot $name" + rm -rf "${BACKUP_BASE:?}/$name" fi done } @@ -95,14 +93,34 @@ rotate_old() { log "=== Starting backup run: $NOW ===" check_mount -mkdir -p "$BACKUP_BASE" "$THIS_RUN" +# Fail early if the backup base directory doesn't exist (drive not mounted / path wrong) +[ -d "$BACKUP_BASE" ] || die "Backup base directory $BACKUP_BASE does not exist." + +# Locking — base dir is confirmed to exist, so the lockfile write will succeed +lockfile="${BACKUP_BASE}/.backup.lock" +cleanup() { rm -f "$lockfile"; } +trap cleanup EXIT +if ! ( set -o noclobber; echo "$$" > "$lockfile" ) 2>/dev/null; then + # Check if the PID in the lockfile is still alive; if not, it's a stale lock + stale_pid=$(cat "$lockfile" 2>/dev/null || true) + if [[ -n "$stale_pid" ]] && ! kill -0 "$stale_pid" 2>/dev/null; then + log "Removing stale lockfile (PID $stale_pid is no longer running)." + rm -f "$lockfile" + echo "$$" > "$lockfile" + else + die "Another backup is already running (PID ${stale_pid:-unknown}, lockfile: $lockfile)." + fi +fi + +mkdir -p "$THIS_RUN" # Incremental snapshot? -if [ -d "$LATEST" ]; then - LINK_DEST="--link-dest=$(readlink -f "$LATEST")" - log "Using link-dest for incremental snapshot → $LATEST" +if [ -L "$LATEST" ] && [ -d "$LATEST" ]; then + latest_real=$(readlink -f "$LATEST") + LINK_DEST=("--link-dest=${latest_real}") + log "Using link-dest for incremental snapshot → $latest_real" else - LINK_DEST="" + LINK_DEST=() log "No previous backup; creating full snapshot." fi @@ -113,7 +131,7 @@ for src in "${SOURCE_ITEMS[@]}"; do mkdir -p "$dst" log "Backing up $src → $dst" nice -n 10 ionice -c2 -n7 \ - rsync "${RSYNC_OPTS[@]}" $LINK_DEST \ + rsync "${RSYNC_OPTS[@]}" "${LINK_DEST[@]}" \ "$src"/ "$dst"/ done