From fc26387aebcbae40731d0da4ee65fbcc08e97c52 Mon Sep 17 00:00:00 2001 From: Charlie Silverman Date: Sun, 18 Oct 2026 13:44:59 -0700 Subject: [PATCH] Updated to version 4.1.0 # 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 --- rsync-monitor.sh | 533 ++++++++++++++++++++++++++++++----------------- 1 file changed, 343 insertions(+), 190 deletions(-) diff --git a/rsync-monitor.sh b/rsync-monitor.sh index b4e2174..2314c67 100644 --- a/rsync-monitor.sh +++ b/rsync-monitor.sh @@ -1,223 +1,376 @@ #!/usr/bin/env bash -SCRIPTNAME="$(basename "$0")" -VERSION=3.5.0 -FILEDATE="2026-07-26" -echo -e "\033[0m===============================================" -echo -e " $SCRIPTNAME - rsync progress monitor" -echo -e " Written by Charlie Silverman" -echo -e " Version: $VERSION" -echo -e " Release Date: $FILEDATE" -echo -e "===============================================" +############################################################################### +# 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 -HELP=false +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 -while [[ $# -gt 0 ]]; do - echo $1 - IFS="=" read -r command value <<< "$1" - echo "Command: $command" - echo "Value: $value" - echo "Value length: ${#value}" - case "$command" in - --source) - if [ "${#value})" -gt 0 ]; then - SRCFOLDER="$value" - shift - else - SRCFOLDER="$2" - shift 2 - fi - ;; - --destination) - if [ "${#value})" -gt 0 ]; then - DSTFOLDER="$value" - shift - else - DSTFOLDER="$2" - shift 2 - fi - ;; - --sleep) - if [ "${#value})" -gt 0 ]; then - SLEEP="$value" - shift - else - SLEEP="$2" - shift 2 - fi - ;; - --help) - HELP=true - shift - ;; - *) - echo "ERROR: Unknown argument: $1" - HELP=true - break - ;; - esac done +############################################################################### +# Startup +############################################################################### + +show_banner + +touch "$LOGFILE" 2>/dev/null || { + echo "Unable to write logfile: $LOGFILE" + exit 1 +} + +############################################################################### +# Validation +############################################################################### + ERROR=0 -if [[ "$HELP" != "true" ]]; then - # Check if source folder exists - if [[ ! -d "${SRCFOLDER}" ]]; then - echo -e "\n\033[0;31mE\e[4mRROR: Source Folder $SRCFOLDER does not exist.\e[0m" - ERROR=1 - fi - # Check if destination folder exists - if [[ ! -d "${DSTFOLDER}" ]]; then - echo -e "\n\033[0;31mE\e[4mRROR: Destination Folder $DSTFOLDER does not exist.\e[0m" - ERROR=1 - fi - # Verify that a sleep value was supplied - if [ -z "${SLEEP}" ]; then - echo -e "\n\033[0;31mE\e[4mRROR: Sleep delay not specified.\e[0m" - ERROR=1 - else - # Verify that the supplied sleep value is actually a Number - re='^[0-9]+$' - if ! [[ $SLEEP =~ $re ]]; then - echo -e "\n\033[0;31m\e[4mERROR: Sleep delay invalid.\e[0m" - ERROR=1 - fi - fi +if [[ -z "$SRCFOLDER" ]]; then + log_error "Source folder not specified." + ERROR=1 fi -# Help display -if $HELP; then - cat << EOF - -SYNOPSIS - ./$SCRIPTNAME --source --destination --sleep [OPTIONS]" - -OPTIONS - --source - Full or relative path to the source rsync folder to monitor - - --destination - Full or relative path to the destination rsync folder to monitor - - --sleep - The number of seconds to sleep between scanning passes. - Default: 60 - - --help - Display this help message and exit. - -EXAMPLE - ./$SCRIPTNAME --source /home/olduser --destination /home/newuser --sleep 300" -EOF +if [[ -z "$DSTFOLDER" ]]; then + log_error "Destination folder not specified." + ERROR=1 fi -echo "Calculating size of $SRCFOLDER..." - -# Getting the mount point that relates to the source path, stripping off -# the trailing slash if found -if [[ "${SRCFOLDER: -1}" = "/" ]]; then - SRCMOUNT=$(df ${SRCFOLDER%?} | sed -n '2p' | awk '{print $6}') -else - SRCMOUNT=$(df $SRCFOLDER | sed -n '2p' | awk '{print $6}') +if [[ ! -d "$SRCFOLDER" ]]; then + log_error "Source folder does not exist: $SRCFOLDER" + ERROR=1 fi -# Determining the size of the source data. If the source path -# matches a mount point from df, assuming that mount point data -# to be accurate. -if [ $SRCMOUNT != $SRCFOLDER ]; then - MEGS=$(du -sm $SRCFOLDER |awk '{print $1}') -else - MEGS=$(df $SRCFOLDER | sed -n '2p' | awk '{print $3}') - MEGS=$(($MEGS / 1024)) -fi -MEGSDISPLAY="${MEGS}MB" -MEGSGB=$(echo "scale=2;$MEGS/1024" | bc) -if [ $(echo "$MEGSGB > 0" | bc) -ne 0 ]; then - NUMFMT=$(printf "%'.3f\n" ${MEGSGB}) - MEGSDISPLAY="${NUMFMT}GB" +if [[ ! -d "$DSTFOLDER" ]]; then + log_error "Destination folder does not exist: $DSTFOLDER" + ERROR=1 fi -echo "Monitoring $DSTFOLDER..." - -# Getting the mount point that relates to the destination path, stripping off -# the trailing slash if found -if [[ "${$DSTFOLDER: -1}" = "/" ]]; then - DSTMOUNT=$(df ${DSTFOLDER%?} | sed -n '2p' | awk '{print $6}') -else - DSTMOUNT=$(df $DSTFOLDER | sed -n '2p' | awk '{print $6}') +if ! [[ "$SLEEP" =~ ^[0-9]+$ ]]; then + log_error "Sleep value must be numeric." + ERROR=1 fi -SECONDSLEFT=1 +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) -# Determining the current size of the destination data. If the -# destination path matches a mount point from df, assuming that -# mount point data to be accurate. -if [ $DSTMOUNT != $DSTFOLDER ]; then - FIRST=$(du -sm $DSTFOLDER |awk '{print $1}') +START="$FIRST" + +if (( $(echo "$MEGSGB > 0" | bc) )); then + CURRENTPCT=$(echo "scale=0;($FIRSTGB/$MEGSGB)*100" | bc) else - FIRST=$(df $DSTFOLDER | sed -n '2p' | awk '{print $3}') - FIRST=$(($FIRST / 1024)) + CURRENTPCT=0 fi -FIRSTDISPLAY="${FIRST}MB" -FIRSTGB=$(echo "scale=2;$FIRST/1024" | bc) -if [ $(echo "$FIRSTGB > 0" | bc) -ne 0 ]; then - NUMFMT=$(printf "%'.3f\n" ${FIRSTGB}) - FIRSTDISPLAY="${NUMFMT}GB" -fi - -CURRENTPCT=$(echo "scale=2;($FIRSTGB/$MEGSGB)*100" | bc | cut -d . -f 1) - -START=$FIRST -CMDTIME=0 CURRENTTIME=$(date '+%Y-%m-%d %H:%M:%S') -echo -e "[${CURRENTTIME}] \033[0;32m${FIRSTDISPLAY}\e[0m of \033[0;32m${MEGSDISPLAY}\e[0m (\033[0;32m${CURRENTPCT}%\e[0m)\tStarting\t\t| Sleeping ${SLEEP}s..." -while (( $SECONDSLEFT > 0 )); do - sleep $SLEEP - # Determining the current size of the destination data. If the - # destination path matches a mount point from df, assuming that - # mount point data to be accurate. - if [ $DSTMOUNT != $DSTFOLDER ]; then - END=$(du -sm $DSTFOLDER |awk '{print $1}') - else - END=$(df $DSTFOLDER | sed -n '2p' | awk '{print $3}') - END=$(($END / 1024)) - fi +echo -e "[${CURRENTTIME}] ${FIRSTDISPLAY} of ${MEGSDISPLAY} (${CURRENTPCT}%) Starting | Sleeping ${SLEEP}s..." - TIMENOW=$(date +%s) - TIMEDIFF=$(($TIMENOW-$STARTTIME)) - DOWNLOADED=$(( $END-$START )) - TOTALDOWNLOADED=$(( $END-$FIRST )) - if [[ $DOWNLOADED -lt 0 ]]; then - DOWNLOADED=0 - fi +############################################################################### +# Monitor Loop +############################################################################### - ENDDISPLAY="${END}MB" - ENDGB=$(echo "scale=2;$END/1024" | bc) - if [[ $(echo "$END > 0" | bc) -ne 0 ]]; then - NUMFMT=$(printf "%'.3f\n" ${ENDGB}) - ENDDISPLAY="${NUMFMT}GB" - fi +while true; do - CURRENTTIME=$(date '+%Y-%m0%d %H:%M:%S')/ - CURRENTPCT=$(echo "scale=2;($ENDGB/$MEGSGB)*100" | bc | cut -d . -f 1) + sleep "$SLEEP" - if [[ "$DOWNLOADED" == "0" ]]; then - echo -e "[${CURRENTTIME}] \033[0;32m${ENDDISPLAY}\e[0m of \033[0;32m${MEGSDISPLAY}\e[0m (\033[0;32m${CURRENTPCT%}\e[0m)\t| No change since last check | Sleeping ${THISSLEEP}s..." - continue - fi + END=$(get_directory_size_mb "$DSTFOLDER") + ENDDISPLAY=$(format_size "$END") + ENDGB=$(echo "scale=3;$END/1024" | bc) - OVERALLMEGSPERSECOND=$(( $TOTALDOWNLOADED/$TIMEDIFF )) - OVERALLSECONDSLEFT=$(( ($MEGS-$END)/$TOTALDOWNLOADED*$TIMEDIFF )) - OVERALLDAYS=$(( $OVERALLSECONDSLEFT/60/60/24 )) - OVERALLHOURS=$(( ($OVERALLSECONDSLEFT-$OVERALLDAYS*60*60*24)/60/60 )) - OVERALLMINUTES=$(( ($OVERALLSECONDSLEFT-$OVERALLDAYS*60*60*24-$OVERALLHOURS*60*60)/60 )) - OVERALLSECONDS=$(( $OVERALLSECONDSLEFT-$OVERALLDAYS*60*60*24-$OVERALLHOURS*60*60-$OVERALLMINUTES*60 )) - echo -e "[${CURRENTTIME}] \033[0;32m${ENDDISPLAY}\e[0m of \033[0;32m${MEGSDISPLAY}\e[0m (\033[0;32m${CURRENTPCT}%\e[0m)\t ~${OVERALLMEGSPERSECOND}MB/s\t| ${OVERALLDAYS}d ${OVERALLHOURS}h ${OVERALLMINUTES}m ${OVERALLSECONDS}s remaining\t| Sleeping ${SLEEP}s..." + TIMENOW=$(date +%s) + TIMEDIFF=$(( TIMENOW - STARTTIME )) - START=$END -done + 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 \ No newline at end of file