#!/usr/bin/env bash set -Eeuo pipefail shopt -s inherit_errexit nullglob # ----------------------------------------------------------------------------- # CONFIGURATION # ----------------------------------------------------------------------------- readonly BACKUP_BASE=/media/extension/backup/ubuntu readonly MOUNT_POINT=/media/extension readonly NOW=$(date +'%Y-%m-%d') readonly THIS_RUN="${BACKUP_BASE}/${NOW}" readonly LATEST="${BACKUP_BASE}/latest" # Directories (or files) to back up (without DB volumes) declare -a SOURCE_ITEMS=( "/home/oster/server" # "/home/oster/photo-lib" ) # Rsync exclusions — exclude DB-related dirs that are dumped separately + common junk declare -a EXCLUDES=( --exclude='.cache' --exclude='node_modules' --exclude='*.tmp' --exclude='**/postgresql' --exclude='**/mysql' --exclude='**/db' --exclude='**/database' ) # Rsync options RSYNC_OPTS=( -aAXHv # preserve everything, human-friendly, verbose --delete # mirror deletions --partial # keep partials if interrupted --stats # summary "${EXCLUDES[@]}" ) # ----------------------------------------------------------------------------- # 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 # ----------------------------------------------------------------------------- check_mount() { if ! mountpoint -q "$MOUNT_POINT"; then die "Backup mountpoint $MOUNT_POINT is NOT mounted." fi } # Rotation: keep 7 daily and 4 weekly rotate_old() { log "Applying retention policy (7 daily, 4 weekly)." cd "$BACKUP_BASE" || die "Cannot cd into $BACKUP_BASE" # 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 # Keep last 7 daily for i in $(seq 0 6); do [ "$dir" = "$(date -d "-$i day" +%F)" ] && keep=1 done # 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" else log " Keeping snapshot $dir" fi done } # ----------------------------------------------------------------------------- # MAIN # ----------------------------------------------------------------------------- log "=== Starting backup run: $NOW ===" check_mount mkdir -p "$BACKUP_BASE" "$THIS_RUN" # Incremental snapshot? if [ -d "$LATEST" ]; then LINK_DEST="--link-dest=$(readlink -f "$LATEST")" log "Using link-dest for incremental snapshot → $LATEST" else LINK_DEST="" log "No previous backup; creating full snapshot." fi # Run backups for src in "${SOURCE_ITEMS[@]}"; do base=$(basename "$src") dst="${THIS_RUN}/${base}" mkdir -p "$dst" log "Backing up $src → $dst" nice -n 10 ionice -c2 -n7 \ rsync "${RSYNC_OPTS[@]}" $LINK_DEST \ "$src"/ "$dst"/ done # Update latest symlink atomically ln -sfn "$(basename "$THIS_RUN")" "$BACKUP_BASE/.latest.tmp" mv -T "$BACKUP_BASE/.latest.tmp" "$LATEST" log "Snapshots done. Running rotation policy..." rotate_old log "=== Backup completed successfully ==="