Skip to content

Instantly share code, notes, and snippets.

@dzogrim
Last active May 17, 2025 16:04
Show Gist options
  • Save dzogrim/d7e69bda4e7183ffd1fd928918289f6e to your computer and use it in GitHub Desktop.
Save dzogrim/d7e69bda4e7183ffd1fd928918289f6e to your computer and use it in GitHub Desktop.
Secure remote rsync backup script from a mounted volume with validation, timestamped logs
#!/usr/bin/env bash
# SPDX-License-Copyright: 2025 Sébastien L.
# SPDX-License-Identifier: MIT
#
# Secure remote rsync backup script from a mounted volume with validation, timestamped logs, and three modes: dryrun, drycheck, and apply.
# (!) Please make sure to configure a local PASSWORD_FILE with 0600 mode.
#
# This script requires a properly configured remote rsync module,
# with correct security settings already in place (e.g., password, access control).
# It assumes the destination rsync daemon is running and accessible over the network.
#
# Version 1.0 – 2025-05-17:
# - initial: prevents path errors, requires --apply for real execution, and logs duration and number of transferred files.
# Version 1.1 – 2025-05-17:
# - added explicit handling of rsync return codes
# - benign "vanished files (code 24)" warning is now properly ignored
# - non-zero exit codes are preserved only for critical errors
# Version 1.2 – 2025-05-17:
# - added file locking to prevent concurrent executions (/tmp-based)
# - added rsync timeout (5 min of inactivity)
# --- CONFIGURATION ---
DEVICE="/dev/mapper/identifier"
DEST_USER="backup-user-as-configured"
DEST_MODULE="rsync-remote-config-mod"
DEST_HOST="FQDN"
PASSWORD_FILE="/path/.rsync_backup.pwd"
# --- USAGE ---
usage() {
echo "Usage: $0 [--dryrun | --drycheck | --apply]"
echo " --dryrun : full simulation with standard logging"
echo " --drycheck : quick diff (lists only files that would be transferred)"
echo " --apply : actually runs the backup (required for real execution)"
exit 1
}
[[ $# -ne 1 ]] && usage
case "$1" in
--dryrun) MODE="dryrun" ;;
--drycheck) MODE="drycheck" ;;
--apply) MODE="run" ;;
*) usage ;;
esac
# --- VALIDATIONS ---
abort() { echo "Error: $1" >&2; exit "$2"; }
[[ -e "$DEVICE" ]] || abort "Device $DEVICE not found." 1
MOUNTPOINT=$(findmnt -n -o TARGET --source "$DEVICE")
[[ -n "$MOUNTPOINT" ]] || abort "Device $DEVICE is not mounted." 2
SOURCE="${MOUNTPOINT}/DataDrive"
[[ "${SOURCE: -1}" != "/" ]] || abort "SOURCE must not end with a trailing slash." 3
[[ -f "$PASSWORD_FILE" ]] || abort "Password file $PASSWORD_FILE not found." 4
[[ -d "/dev/shm" ]] || abort "/dev/shm does not exist on this system." 5
command -v rsync >/dev/null || abort "rsync is not installed." 6
command -v flock >/dev/null || abort "flock is not installed." 7
# --- SINGLE INSTANCE LOCK ---
# Prevent concurrent executions using a lockfile in /tmp
LOCKFILE="/tmp/$(basename "$0").lock"
exec 9>"$LOCKFILE"
if ! flock -n 9; then
echo "⚠️ Backup already running. Exiting."
exit 100
fi
# --- LOGGING ---
LOG_DIR="${SOURCE}/LOG_Rsync_Backup"
TIMESTAMP="$(date '+%Y%m%d_%H%M%S')"
LOG_FILE="${LOG_DIR}/Rsync_task_1_Backup.${TIMESTAMP}"
mkdir -p "$LOG_DIR"
# --- RSYNC EXCLUDES ---
RSYNC_EXCLUDES=(
--exclude='**/.AppleDouble/'
--exclude='.DS_Store'
--exclude='.cache/'
--exclude='*.swp'
--exclude='lost+found'
--exclude='tmp/'
)
echo "=== Task: Backup Data → $DEST_MODULE on $DEST_HOST ===" >> "$LOG_FILE"
echo "$(date '+%Y/%m/%d %H:%M:%S') [$$] === Backup started in mode: $MODE ===" >> "$LOG_FILE"
START_EPOCH=$(date +%s)
# --- INTERRUPT HANDLER (e.g. Ctrl+C) ---
trap 'echo -e "\n🛑 Interrupted by user."; exit 130' INT
# --- EXECUTION ---
EXIT_CODE=0
if [[ "$MODE" == "drycheck" ]]; then
TMP_OUTPUT=$(mktemp)
rsync -avnc --delete --temp-dir=/dev/shm \
"${RSYNC_EXCLUDES[@]}" \
--password-file="$PASSWORD_FILE" \
"$SOURCE" "${DEST_USER}@${DEST_HOST}::${DEST_MODULE}" >"$TMP_OUTPUT" || EXIT_CODE=$?
grep -v '/$' "$TMP_OUTPUT" | tee -a "$LOG_FILE"
rm -f "$TMP_OUTPUT"
else
RSYNC_OPTS=(-avz --progress --partial --inplace --temp-dir=/dev/shm --timeout=300)
[[ "$MODE" == "dryrun" ]] && RSYNC_OPTS+=(--dry-run)
rsync "${RSYNC_OPTS[@]}" "${RSYNC_EXCLUDES[@]}" \
--password-file="$PASSWORD_FILE" \
--log-file="$LOG_FILE" \
"$SOURCE" "${DEST_USER}@${DEST_HOST}::${DEST_MODULE}"
ret=$?
if [ $ret -eq 24 ]; then
echo "⚠️ Avertissement : certains fichiers ont disparu pendant la copie (code 24), on continue."
EXIT_CODE=0
elif [ $ret -ne 0 ]; then
echo "❌ Erreur fatale de rsync (code $ret), arrêt du script."
exit $ret
fi
fi
# --- SUMMARY ---
END_EPOCH=$(date +%s)
DURATION=$((END_EPOCH - START_EPOCH))
END_TIME="$(date '+%Y/%m/%d %H:%M:%S')"
NB_FILES=$(grep -c '^<f' "$LOG_FILE" || true)
[[ $EXIT_CODE -eq 0 ]] && STATUS="Backup completed successfully" || STATUS="Backup failed (exit code $EXIT_CODE)"
echo "$END_TIME [$$] === $STATUS ===" >> "$LOG_FILE"
printf "[Summary] Duration: %02dh%02dm%02ds — Files transferred: %d\n" \
$((DURATION / 3600)) $(((DURATION % 3600) / 60)) $((DURATION % 60)) "$NB_FILES" >> "$LOG_FILE"
exit $EXIT_CODE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment