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
This commit is contained in:
ackitsme committed 2026-10-18 13:44:59 -07:00
1 parent 899eb11a2d
commit fc26387aeb
1 file changed
+343 -190
+343 -190
View File
@@ -1,223 +1,376 @@
#!/usr/bin/env bash #!/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" # Script Name : RsyncMonitor.sh
echo -e " Written by Charlie Silverman" # Author : Charlie Silverman
echo -e " Version: $VERSION" # Version : 4.1.0
echo -e " Release Date: $FILEDATE" # Date : 2026-10-18
echo -e "===============================================" #
# 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="" SRCFOLDER=""
DSTFOLDER="" DSTFOLDER=""
SLEEP=60 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 <source> -d <destination> [options]
OPTIONS
-s <path>
Source folder to monitor.
-d <path>
Destination folder to monitor.
-t <seconds>
Sleep interval between scans.
Default: 60
-l <file>
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 done
###############################################################################
# Startup
###############################################################################
show_banner
touch "$LOGFILE" 2>/dev/null || {
echo "Unable to write logfile: $LOGFILE"
exit 1
}
###############################################################################
# Validation
###############################################################################
ERROR=0 ERROR=0
if [[ "$HELP" != "true" ]]; then if [[ -z "$SRCFOLDER" ]]; then
# Check if source folder exists log_error "Source folder not specified."
if [[ ! -d "${SRCFOLDER}" ]]; then ERROR=1
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
fi fi
# Help display if [[ -z "$DSTFOLDER" ]]; then
if $HELP; then log_error "Destination folder not specified."
cat << EOF ERROR=1
SYNOPSIS
./$SCRIPTNAME --source <SRCFOLDER> --destination <DSTFOLDER> --sleep <SECONDS> [OPTIONS]"
OPTIONS
--source <SRCFOLDER>
Full or relative path to the source rsync folder to monitor
--destination <DSTFOLDER>
Full or relative path to the destination rsync folder to monitor
--sleep <SECONDS>
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
fi fi
echo "Calculating size of $SRCFOLDER..." if [[ ! -d "$SRCFOLDER" ]]; then
log_error "Source folder does not exist: $SRCFOLDER"
# Getting the mount point that relates to the source path, stripping off ERROR=1
# 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}')
fi fi
# Determining the size of the source data. If the source path if [[ ! -d "$DSTFOLDER" ]]; then
# matches a mount point from df, assuming that mount point data log_error "Destination folder does not exist: $DSTFOLDER"
# to be accurate. ERROR=1
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"
fi fi
echo "Monitoring $DSTFOLDER..." if ! [[ "$SLEEP" =~ ^[0-9]+$ ]]; then
log_error "Sleep value must be numeric."
# Getting the mount point that relates to the destination path, stripping off ERROR=1
# 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}')
fi 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) STARTTIME=$(date +%s)
# Determining the current size of the destination data. If the START="$FIRST"
# destination path matches a mount point from df, assuming that
# mount point data to be accurate. if (( $(echo "$MEGSGB > 0" | bc) )); then
if [ $DSTMOUNT != $DSTFOLDER ]; then CURRENTPCT=$(echo "scale=0;($FIRSTGB/$MEGSGB)*100" | bc)
FIRST=$(du -sm $DSTFOLDER |awk '{print $1}')
else else
FIRST=$(df $DSTFOLDER | sed -n '2p' | awk '{print $3}') CURRENTPCT=0
FIRST=$(($FIRST / 1024))
fi 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') 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 echo -e "[${CURRENTTIME}] ${FIRSTDISPLAY} of ${MEGSDISPLAY} (${CURRENTPCT}%) Starting | Sleeping ${SLEEP}s..."
# 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
TIMENOW=$(date +%s) ###############################################################################
TIMEDIFF=$(($TIMENOW-$STARTTIME)) # Monitor Loop
DOWNLOADED=$(( $END-$START )) ###############################################################################
TOTALDOWNLOADED=$(( $END-$FIRST ))
if [[ $DOWNLOADED -lt 0 ]]; then
DOWNLOADED=0
fi
ENDDISPLAY="${END}MB" while true; do
ENDGB=$(echo "scale=2;$END/1024" | bc)
if [[ $(echo "$END > 0" | bc) -ne 0 ]]; then
NUMFMT=$(printf "%'.3f\n" ${ENDGB})
ENDDISPLAY="${NUMFMT}GB"
fi
CURRENTTIME=$(date '+%Y-%m0%d %H:%M:%S')/ sleep "$SLEEP"
CURRENTPCT=$(echo "scale=2;($ENDGB/$MEGSGB)*100" | bc | cut -d . -f 1)
if [[ "$DOWNLOADED" == "0" ]]; then END=$(get_directory_size_mb "$DSTFOLDER")
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..." ENDDISPLAY=$(format_size "$END")
continue ENDGB=$(echo "scale=3;$END/1024" | bc)
fi
OVERALLMEGSPERSECOND=$(( $TOTALDOWNLOADED/$TIMEDIFF )) TIMENOW=$(date +%s)
OVERALLSECONDSLEFT=$(( ($MEGS-$END)/$TOTALDOWNLOADED*$TIMEDIFF )) TIMEDIFF=$(( TIMENOW - STARTTIME ))
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..."
START=$END DOWNLOADED=$(( END - START ))
done 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