#!/usr/bin/env bash set -Eeuo pipefail shopt -s inherit_errexit nullglob # ----------------------------------------------------------------------------- # CONFIGURATION — adjust to taste # ----------------------------------------------------------------------------- readonly RETENTION_DAYS=7 readonly BACKUP_BASE=/media/extension/backup/ubuntu readonly MOUNT_POINT=/media/extension # timestamp format — no spaces #readonly NOW=$(date +'%Y-%m-%d_%H-%M-%S') readonly NOW=$(date +'%Y-%m-%d') readonly THIS_RUN=${BACKUP_BASE}/${NOW} readonly LATEST=${BACKUP_BASE}/latest # directories (or files) to back up: declare -a SOURCE_ITEMS=( "/home/oster/server" "/home/oster/photo-lib" ) # rsync exclusions — add your own declare -a EXCLUDES=( --exclude='.cache' --exclude='node_modules' --exclude='*.tmp' ) # rsync niceties RSYNC_OPTS=( -aAXHv # archive, preserve ACLs/xattrs, human-readable, verbose --delete # mirror deletions --partial # keep partials if interrupted --stats # give stats at end "${EXCLUDES[@]}" ) # Dry-run? set to "--dry-run" to test without writing DRY_RUN="" # ----------------------------------------------------------------------------- # LOGGING & LOCKING / CLEANUP # ----------------------------------------------------------------------------- log() { echo "[$(date +'%F %T')] $*"; } die() { log "FATAL: $*"; exit 1; } lockfile="${BACKUP_BASE}/.backup.lock" #trap 'rm -rf "$lockfile"' EXIT #if ! touch "$lockfile" 2>/dev/null; then # die "Another backup is running (lockfile exists)." #fi # ----------------------------------------------------------------------------- # FUNCTIONS # ----------------------------------------------------------------------------- check_mount() { if ! mountpoint -q "$MOUNT_POINT"; then die "Backup mountpoint $MOUNT_POINT is not mounted." fi } rotate_old() { log "Rotating backups older than ${RETENTION_DAYS} days." find "$BACKUP_BASE" -maxdepth 1 -mindepth 1 -type d \ ! -name 'latest' ! -name "$(basename "$THIS_RUN")" \ -mtime +${RETENTION_DAYS} \ -print -exec rm -rf {} \; } # ----------------------------------------------------------------------------- # MAIN # ----------------------------------------------------------------------------- log "=== Starting backup run: $NOW ===" check_mount mkdir -p "$BACKUP_BASE" mkdir -p "$THIS_RUN" # Determine if we can do a hardlink-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; doing full snapshot." fi for src in "${SOURCE_ITEMS[@]}"; do base=$(basename "$src") dst="${THIS_RUN}/${base}" log "Backing up $src → $dst" nice -n 10 ionice -c2 -n7 \ rsync "${RSYNC_OPTS[@]}" $LINK_DEST $DRY_RUN \ "$src"/ "$dst"/ done # update the "latest" symlink atomically tmp_latest="${BACKUP_BASE}/.latest.tmp" rm -f "$tmp_latest" ln -sfn "$(basename "$THIS_RUN")" "$tmp_latest" mv -T "$tmp_latest" "$LATEST" rotate_old log "=== Backup completed successfully ==="