108 lines
3.1 KiB
Bash
Executable File
108 lines
3.1 KiB
Bash
Executable File
#!/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 ==="
|