Last active
May 17, 2025 16:04
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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