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:
1 parent
899eb11a2d
commit
fc26387aeb
1 file changed
+343
-190
+343
-190
@@ -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 <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
|
||||
|
||||
###############################################################################
|
||||
# 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 <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
|
||||
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
|
||||
Reference in new issue
Block a user