#!/usr/bin/env bash ############################################################################### # Script Name : RsyncMonitor.sh # Author : Charlie Silverman # Version : 4.1.0 # Date : 2026-10-18 # # Description: # Monitor rsync copy progress by comparing source and destination sizes. # Calculates completion percentage, transfer rate, and estimated time # remaining. # # Revision History # ----------------------------------------------------------------------------- # v4.1.0 - 2026-10-18 # - Converted to getopts # - Added configurable logfile (-l) # - Added required option validation # - Improved error handling # # v4.0.0 - 2026-10-18 # - Added logging framework # - Added trap handlers # - Added standardized help # - Fixed syntax issues # # v3.5.0 - 2026-07-26 # - Original release ############################################################################### SCRIPT_NAME="$(basename "$0")" SCRIPT_VERSION="4.1.0" SCRIPT_DATE="2026-10-18" set -o pipefail ############################################################################### # Defaults ############################################################################### SRCFOLDER="" DSTFOLDER="" SLEEP=60 LOGFILE="/tmp/RsyncMonitor.log" ############################################################################### # Logging Functions ############################################################################### log_info() { local MESSAGE="$1" echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO ] $MESSAGE" | tee -a "$LOGFILE" } log_warn() { local MESSAGE="$1" echo "[$(date '+%Y-%m-%d %H:%M:%S')] [WARN ] $MESSAGE" | tee -a "$LOGFILE" } log_error() { local MESSAGE="$1" echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $MESSAGE" | tee -a "$LOGFILE" >&2 } ############################################################################### # Error Handling ############################################################################### error_handler() { local LINE="$1" log_error "Unexpected error on line ${LINE}" exit 1 } interrupt_handler() { log_warn "Execution interrupted." exit 130 } trap 'error_handler $LINENO' ERR trap interrupt_handler SIGINT SIGTERM ############################################################################### # Banner ############################################################################### show_banner() { cat << EOF ================================================== ${SCRIPT_NAME} rsync Progress Monitor Author : Charlie Silverman Version : ${SCRIPT_VERSION} Date : ${SCRIPT_DATE} ================================================== EOF } ############################################################################### # Help ############################################################################### show_help() { cat << EOF NAME ${SCRIPT_NAME} SYNOPSIS ${SCRIPT_NAME} -s -d [options] OPTIONS -s Source folder to monitor. -d Destination folder to monitor. -t Sleep interval between scans. Default: 60 -l Log file path. Default: /tmp/RsyncMonitor.log -h Display help. EXAMPLES ${SCRIPT_NAME} \\ -s /home/olduser \\ -d /home/newuser ${SCRIPT_NAME} \\ -s /source \ -d /destination \ -t 300 \ -l /var/log/RsyncMonitor.log EOF } ############################################################################### # Utilities ############################################################################### get_directory_size_mb() { local DIR="$1" local MOUNT if [[ "${DIR: -1}" == "/" ]]; then MOUNT=$(df "${DIR%?}" | awk 'NR==2 {print $6}') else MOUNT=$(df "$DIR" | awk 'NR==2 {print $6}') fi if [[ "$MOUNT" != "$DIR" ]]; then du -sm "$DIR" | awk '{print $1}' else local USED_KB USED_KB=$(df "$DIR" | awk 'NR==2 {print $3}') echo $(( USED_KB / 1024 )) fi } format_size() { local SIZE_MB="$1" if (( SIZE_MB >= 1024 )); then local SIZE_GB SIZE_GB=$(echo "scale=3;$SIZE_MB/1024" | bc) printf "%'.3fGB" "$SIZE_GB" else echo "${SIZE_MB}MB" fi } ############################################################################### # Option Processing ############################################################################### while getopts ":s:d:t:l:h" opt; do case "${opt}" in s) SRCFOLDER="$OPTARG" ;; d) DSTFOLDER="$OPTARG" ;; t) SLEEP="$OPTARG" ;; l) LOGFILE="$OPTARG" ;; h) show_help exit 0 ;; :) echo "Missing argument for -${OPTARG}" exit 1 ;; \?) echo "Invalid option: -${OPTARG}" exit 1 ;; esac done ############################################################################### # Startup ############################################################################### show_banner touch "$LOGFILE" 2>/dev/null || { echo "Unable to write logfile: $LOGFILE" exit 1 } ############################################################################### # Validation ############################################################################### ERROR=0 if [[ -z "$SRCFOLDER" ]]; then log_error "Source folder not specified." ERROR=1 fi if [[ -z "$DSTFOLDER" ]]; then log_error "Destination folder not specified." ERROR=1 fi if [[ ! -d "$SRCFOLDER" ]]; then log_error "Source folder does not exist: $SRCFOLDER" ERROR=1 fi if [[ ! -d "$DSTFOLDER" ]]; then log_error "Destination folder does not exist: $DSTFOLDER" ERROR=1 fi if ! [[ "$SLEEP" =~ ^[0-9]+$ ]]; then log_error "Sleep value must be numeric." ERROR=1 fi if (( ERROR > 0 )); then exit 1 fi ############################################################################### # Calculate Source Size ############################################################################### log_info "Calculating source size." MEGS=$(get_directory_size_mb "$SRCFOLDER") MEGSGB=$(echo "scale=3;$MEGS/1024" | bc) MEGSDISPLAY=$(format_size "$MEGS") log_info "Source size: $MEGSDISPLAY" ############################################################################### # Initialize Destination ############################################################################### FIRST=$(get_directory_size_mb "$DSTFOLDER") FIRSTGB=$(echo "scale=3;$FIRST/1024" | bc) FIRSTDISPLAY=$(format_size "$FIRST") STARTTIME=$(date +%s) START="$FIRST" if (( $(echo "$MEGSGB > 0" | bc) )); then CURRENTPCT=$(echo "scale=0;($FIRSTGB/$MEGSGB)*100" | bc) else CURRENTPCT=0 fi CURRENTTIME=$(date '+%Y-%m-%d %H:%M:%S') echo -e "[${CURRENTTIME}] ${FIRSTDISPLAY} of ${MEGSDISPLAY} (${CURRENTPCT}%) Starting | Sleeping ${SLEEP}s..." ############################################################################### # Monitor Loop ############################################################################### while true; do sleep "$SLEEP" END=$(get_directory_size_mb "$DSTFOLDER") ENDDISPLAY=$(format_size "$END") ENDGB=$(echo "scale=3;$END/1024" | bc) TIMENOW=$(date +%s) TIMEDIFF=$(( TIMENOW - STARTTIME )) DOWNLOADED=$(( END - START )) TOTALDOWNLOADED=$(( END - FIRST )) CURRENTTIME=$(date '+%Y-%m-%d %H:%M:%S') if (( DOWNLOADED <= 0 )); then echo "[${CURRENTTIME}] ${ENDDISPLAY} of ${MEGSDISPLAY} (${CURRENTPCT}%) No change detected | Sleeping ${SLEEP}s..." continue fi if (( TIMEDIFF <= 0 || TOTALDOWNLOADED <= 0 )); then START="$END" continue fi OVERALLMEGSPERSECOND=$(( TOTALDOWNLOADED / TIMEDIFF )) if (( OVERALLMEGSPERSECOND < 1 )); then OVERALLMEGSPERSECOND=1 fi REMAINING=$(( MEGS - END )) if (( REMAINING < 0 )); then REMAINING=0 fi OVERALLSECONDSLEFT=$(( REMAINING / OVERALLMEGSPERSECOND )) OVERALLDAYS=$(( OVERALLSECONDSLEFT / 86400 )) OVERALLHOURS=$(( (OVERALLSECONDSLEFT % 86400) / 3600 )) OVERALLMINUTES=$(( (OVERALLSECONDSLEFT % 3600) / 60 )) OVERALLSECONDS=$(( OVERALLSECONDSLEFT % 60 )) if (( $(echo "$MEGSGB > 0" | bc) )); then CURRENTPCT=$(echo "scale=0;($ENDGB/$MEGSGB)*100" | bc) else CURRENTPCT=0 fi echo "[${CURRENTTIME}] ${ENDDISPLAY} of ${MEGSDISPLAY} (${CURRENTPCT}%) ~${OVERALLMEGSPERSECOND}MB/s | ${OVERALLDAYS}d ${OVERALLHOURS}h ${OVERALLMINUTES}m ${OVERALLSECONDS}s remaining | Sleeping ${SLEEP}s..." START="$END" done