From 16bf248308ec034817c3a2e74f6936a2fac80acb Mon Sep 17 00:00:00 2001 From: Stefan Ostermann Date: Wed, 23 Jul 2025 14:04:24 +0000 Subject: [PATCH] backup script --- backup.sh | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100755 backup.sh diff --git a/backup.sh b/backup.sh new file mode 100755 index 0000000..405d7d8 --- /dev/null +++ b/backup.sh @@ -0,0 +1,107 @@ +#!/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 ==="