# 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
376 lines
8.6 KiB
Bash
376 lines
8.6 KiB
Bash
#!/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 <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
|
|
|
|
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 |