Compare commits

...
14 Commits
Author SHA1 Message Date
ackitsme 3c0639f940 Initial upload
first build
2026-10-31 13:55:28 -07:00
ackitsme ea149aa082 Update to version 20261031-01
#   20261031-01 - Consolidated the two divergent 1.7 versions into a single
#                 script. Adopted YYYYMMDD-xx versioning scheme. Version is now
#                 defined once (SCRIPT_VERSION) and used in help and logs.
#                 Removed SFTP upload; backups always go to Restic.
#                 Restic snapshot details (ID, parent, tags, file/dir counts,
#                 data added, snapshot size, duration) are now written to the
#                 logs. restic backup now runs with --json and the details are
#                 read from its backup summary. Added required-command check.
#                 Temporary restic output is removed on failure or interrupt.
2026-10-31 13:44:01 -07:00
ackitsme fac0d3202b Updated to version 2.1.4
# 2.1.2 - Added EMAIL_TO and EMAIL_FROM variables for reports.
# 2.1.3 - Added DOCKERUSER variable for account to use for launching contaners
# 2.1.4 - Restic snapshot details (ID, parent, file/dir counts, data added,
#         total size, duration) are now written to the logs.
#         restic backup now runs with --json; snapshot size is read from the
#         backup summary instead of parsing 'restic snapshots' table output,
#         so it always refers to this run's snapshot.
#         Removed dependency on bc (awk used for arithmetic).
#         Added total new data uploaded to end of log.
2026-10-26 15:14:13 -07:00
ackitsme fc26387aeb 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
2026-10-18 13:44:59 -07:00
ackitsme 899eb11a2d Updated to version 2.1.1
- Added email delivery of current execution log.
- Added success/failure/interruption notifications and run-specific logs.
- Added automatic cleanup of staging directories on backup failure or interruption.
- Added cleanup_staging() helper function.
- Added CURRENT_STAGING_DIR tracking.
- Added automatic RUN_LOG cleanup during failure and interruption. Improved housekeeping to prevent orphaned staging data after unexpected termination.
2026-10-18 13:38:15 -07:00
ackitsme baedfe9eea Updated to version 1.7
Added email delivery of current execution log on success, failure, or interruption.
2026-10-18 13:36:53 -07:00
ackitsme 0e07c9ec97 Updated to version 2.0.1
- Modified to save all files to a folder tree for backup rather than creating a single monolithic archive.  This will allow restic to properly manage snapshots.
- Changed exception lookup to case-insensitive
2026-10-18 12:26:59 -07:00
ackitsme e79d26435b Update to version 1.6
Added logging, error handline, command line argument names, and command line help.  Overall script cleanup.
2026-10-18 12:26:09 -07:00
ackitsme 5b15ba3125 Fixed date 2026-08-05 09:12:10 -07:00
ackitsme 69d5dc162b Fixed total display 2026-08-05 08:51:13 -07:00
ackitsme 5a1320c22a Update dockerbackup.sh 2026-08-04 20:42:00 -07:00
ackitsme 09cbb11e86 Update README.md 2026-07-26 14:54:53 -07:00
ackitsme 871e9c9d4c Merge branch 'main' of https://gitea.ackcentuate.com/ackitsme/BashScripts 2026-07-26 14:53:53 -07:00
ackitsme 7b28156af8 Update README.md 2026-07-26 14:52:43 -07:00
5 changed files with 1462 additions and 220 deletions

No files matched your search

+552
View File
@@ -0,0 +1,552 @@
#!/bin/bash
#
# Script Name: AppBackup-restic.sh
# Description:
# Creates a backup of a web application and optionally its MySQL database.
# The script archives the application home directory locally and backs it
# up to a Backblaze B2 repository using Restic.
#
# Usage:
# AppBackup-restic.sh --appname <name> --appuser <user> [--apppassword <password>]
#
# Author: Charlie Silverman
# Version: 20261031-01
#
# Requirements:
# restic, jq, awk, tar, mysqldump (when --apppassword is used),
# mailx or mail (for email notifications), numfmt (optional, for
# human-readable sizes).
# Restic environment file: /root/.restic-env
#
# Versioning:
# Versions use the format YYYYMMDD-xx, where YYYYMMDD is the release date
# and xx is the release number for that day, starting at 01.
#
# Revision History:
# 1.0 - Initial version.
# 1.1 - Added logging to file.
# 1.2 - Added strict error handling, input validation, and FQDN support.
# 1.3 - Added trap-based error logging and improved operational visibility.
# 1.4 - Added application-specific log files, interrupt handling,
# and enhanced logging.
# 1.5 - Added command-line help.
# 1.6 - Replaced positional parameters with named command-line options
# and added support for short/long option formats.
# 1.7 (a) - Added --no-tablespaces to mysqldump. Added --single-transaction
# to eliminate the need to lock tables during backup.
# 1.7 (b) - Added email delivery of current execution log on success,
# failure, or interruption.
# 20261031-01 - Consolidated the two divergent 1.7 versions into a single
# script. Adopted YYYYMMDD-xx versioning scheme. Version is now
# defined once (SCRIPT_VERSION) and used in help and logs.
# Removed SFTP upload; backups always go to Restic.
# Restic snapshot details (ID, parent, tags, file/dir counts,
# data added, snapshot size, duration) are now written to the
# logs. restic backup now runs with --json and the details are
# read from its backup summary. Added required-command check.
# Temporary restic output is removed on failure or interrupt.
#
set -o errexit
set -o nounset
set -o pipefail
SCRIPT_VERSION="20261031-01"
NOTIFY_EMAIL="ackitsme3@gmail.com"
#------------------------------------------------------------------------------
# Help Function
#------------------------------------------------------------------------------
show_help() {
cat << EOF
AppBackup-restic.sh v${SCRIPT_VERSION}
DESCRIPTION
Creates a backup of a web application's files and optionally its
MySQL database. The backup is sent to a Backblaze B2 repository
using Restic.
USAGE
AppBackup-restic.sh --appname <name> --appuser <user>
[--apppassword <password>]
OPTIONS
-a, --appname <name>
Name of the application and MySQL database.
Required.
-u, --appuser <user>
Linux account that owns the application files.
Required.
-p, --apppassword <password>
MySQL password. If omitted, the database backup is skipped.
Optional.
-h, --help
Display this help and exit.
EXAMPLES
AppBackup-restic.sh \\
--appname KISNetManager \\
--appuser kisuser \\
--apppassword MySecurePassword
AppBackup-restic.sh \\
-a CustomerPortal \\
-u portaluser
LOG FILES
Historical Log:
/tmp/AppBackup-<ApplicationName>.log
Current Run Log:
/tmp/AppBackup-<ApplicationName>-YYYYMMDD_HHMMSS.log
EMAIL NOTIFICATIONS
Current execution logs are emailed to:
${NOTIFY_EMAIL}
Success, failure, and interruption events each generate
a separate email.
AUTHOR
Charlie Silverman
VERSION
${SCRIPT_VERSION}
EOF
}
#------------------------------------------------------------------------------
# Process Command Line Arguments
#------------------------------------------------------------------------------
AppName=""
AppUser=""
AppPassword=""
while [[ $# -gt 0 ]]; do
case "$1" in
-a|--appname)
AppName="$2"
shift 2
;;
--appname=*)
AppName="${1#*=}"
shift
;;
-u|--appuser)
AppUser="$2"
shift 2
;;
--appuser=*)
AppUser="${1#*=}"
shift
;;
-p|--apppassword)
AppPassword="$2"
shift 2
;;
--apppassword=*)
AppPassword="${1#*=}"
shift
;;
-h|--help)
show_help
exit 0
;;
*)
echo "ERROR: Unknown option '$1'"
echo
show_help
exit 1
;;
esac
done
#------------------------------------------------------------------------------
# Validate Required Parameters
#------------------------------------------------------------------------------
if [[ -z "$AppName" || -z "$AppUser" ]]; then
echo "ERROR: --appname and --appuser are required."
echo
show_help
exit 1
fi
#------------------------------------------------------------------------------
# Runtime Variables
#------------------------------------------------------------------------------
DATE=$(date "+%Y%m%d_%H%M%S")
HOSTNAME_FQDN="$(hostname -f)"
SAFE_APPNAME=$(echo "$AppName" | tr -cs '[:alnum:]_-' '_')
LOGFILE="/tmp/AppBackup-${SAFE_APPNAME}.log"
RUNLOG="/tmp/AppBackup-${SAFE_APPNAME}-${DATE}.log"
RESTIC_OUTPUT=""
touch "$RUNLOG"
#------------------------------------------------------------------------------
# Logging Function
#------------------------------------------------------------------------------
log() {
local msg
msg="$(date '+%Y-%m-%d %H:%M:%S') [$$] $1"
echo "$msg" | tee -a "$RUNLOG"
}
#------------------------------------------------------------------------------
# Email Function
#------------------------------------------------------------------------------
send_email_log() {
local subject="$1"
if command -v mailx >/dev/null 2>&1; then
mailx -s "$subject" "$NOTIFY_EMAIL" < "$RUNLOG"
elif command -v mail >/dev/null 2>&1; then
mail -s "$subject" "$NOTIFY_EMAIL" < "$RUNLOG"
else
log "WARNING: mailx/mail command not found. Email not sent."
fi
}
#------------------------------------------------------------------------------
# Convert a byte count to a human-readable size (e.g. 1.23GiB)
#------------------------------------------------------------------------------
human_bytes() {
local bytes="${1:-0}"
if command -v numfmt >/dev/null 2>&1; then
numfmt --to=iec-i --suffix=B --format="%.2f" "$bytes" 2>/dev/null \
|| echo "${bytes} B"
else
echo "${bytes} B"
fi
}
#------------------------------------------------------------------------------
# Write Restic snapshot details from a backup summary (JSON) to the log
#------------------------------------------------------------------------------
log_snapshot_info() {
local summary="$1"
local snapshot_id parent_id
local files_new files_changed files_unmodified
local dirs_new dirs_changed dirs_unmodified
local data_added data_added_packed
local total_files total_bytes total_duration
snapshot_id=$(jq -r '.snapshot_id // "none"' <<< "$summary")
parent_id=$(restic snapshots "$snapshot_id" --json 2>/dev/null \
| jq -r '.[0].parent // "none"' 2>/dev/null || echo "unknown")
files_new=$(jq -r '.files_new // 0' <<< "$summary")
files_changed=$(jq -r '.files_changed // 0' <<< "$summary")
files_unmodified=$(jq -r '.files_unmodified // 0' <<< "$summary")
dirs_new=$(jq -r '.dirs_new // 0' <<< "$summary")
dirs_changed=$(jq -r '.dirs_changed // 0' <<< "$summary")
dirs_unmodified=$(jq -r '.dirs_unmodified // 0' <<< "$summary")
data_added=$(jq -r '.data_added // 0' <<< "$summary")
data_added_packed=$(jq -r '.data_added_packed // empty' <<< "$summary")
total_files=$(jq -r '.total_files_processed // 0' <<< "$summary")
total_bytes=$(jq -r '.total_bytes_processed // 0' <<< "$summary")
total_duration=$(jq -r '.total_duration // 0' <<< "$summary")
log "---------------- Restic Snapshot -----------------"
log "Snapshot ID: ${snapshot_id:0:8} (${snapshot_id})"
log "Parent snapshot: ${parent_id:0:8}"
log "Tags: apache, website, ${AppName}, ${HOSTNAME_FQDN}"
log "Files: ${files_new} new, ${files_changed} changed, ${files_unmodified} unmodified"
log "Directories: ${dirs_new} new, ${dirs_changed} changed, ${dirs_unmodified} unmodified"
if [[ -n "$data_added_packed" ]]; then
log "Data added: $(human_bytes "$data_added") ($(human_bytes "$data_added_packed") stored)"
else
log "Data added: $(human_bytes "$data_added")"
fi
log "Snapshot size: $(human_bytes "$total_bytes") in ${total_files} files"
log "Duration: $(awk -v d="$total_duration" 'BEGIN{printf "%.1f", d}') seconds"
log "--------------------------------------------------"
}
#------------------------------------------------------------------------------
# Remove temporary Restic output
#------------------------------------------------------------------------------
cleanup_restic_output() {
if [[ -n "${RESTIC_OUTPUT:-}" ]]; then
rm -f "${RESTIC_OUTPUT}"
RESTIC_OUTPUT=""
fi
}
#------------------------------------------------------------------------------
# Error Handling
#------------------------------------------------------------------------------
error_handler() {
local exit_code=$?
local line_number=$1
log "ERROR: Backup failed on line ${line_number} with exit code ${exit_code}"
cleanup_restic_output
send_email_log \
"FAILED: AppBackup ${AppName} (${HOSTNAME_FQDN})"
exit "${exit_code}"
}
interrupt_handler() {
log "ERROR: Backup interrupted by signal"
cleanup_restic_output
send_email_log \
"INTERRUPTED: AppBackup ${AppName} (${HOSTNAME_FQDN})"
exit 1
}
trap 'error_handler $LINENO' ERR
trap interrupt_handler SIGINT SIGTERM
#------------------------------------------------------------------------------
# Startup Logging
#------------------------------------------------------------------------------
log "================================================================="
log "Starting backup job (AppBackup-restic.sh v${SCRIPT_VERSION})"
log "Application: $AppName"
log "Application User: $AppUser"
log "Host: $HOSTNAME_FQDN"
log "Historical Log: $LOGFILE"
log "Run Log: $RUNLOG"
log "================================================================="
#------------------------------------------------------------------------------
# Verify Requirements
#------------------------------------------------------------------------------
for cmd in restic jq awk tar; do
if ! command -v "$cmd" >/dev/null 2>&1; then
log "ERROR: Missing required command: $cmd"
send_email_log \
"FAILED: AppBackup ${AppName} (${HOSTNAME_FQDN})"
exit 1
fi
done
#------------------------------------------------------------------------------
# Restic Repository Configuration
#------------------------------------------------------------------------------
export RESTIC_REPOSITORY="s3:https://s3.us-west-004.backblazeb2.com/ackitsbucket/docker"
export RESTIC_PASSWORD_FILE="/root/.restic-password"
#------------------------------------------------------------------------------
# Host Specific Configuration
#------------------------------------------------------------------------------
HostPrefix=""
if [[ "$HOSTNAME_FQDN" == "ackitserver.ackcentuate.com" ]]; then
if [[ "$AppName" == "KISNetManager" ]]; then
HostPrefix="dev-"
fi
fi
ArchiveFile="${AppName}-${DATE}.tar.bz2"
#------------------------------------------------------------------------------
# Prepare Backup Workspace
#------------------------------------------------------------------------------
log "Changing to backup directory"
cd /home/Backups
#------------------------------------------------------------------------------
# Database Backup
#------------------------------------------------------------------------------
if [[ -n "$AppPassword" ]]; then
log "Creating MySQL database backup"
mkdir -p "/home/${AppUser}/SQLBackup"
mysqldump \
--no-tablespaces \
--single-transaction \
-u "$AppUser" \
--password="$AppPassword" \
"$AppName" \
> "/home/${AppUser}/SQLBackup/${AppName}.sqldump"
log "MySQL dump completed successfully"
else
log "Database password not supplied. Skipping MySQL backup."
fi
#------------------------------------------------------------------------------
# Remove Previous Backup Archives
#------------------------------------------------------------------------------
log "Removing previous backup archives"
rm -f /home/Backups/"${AppName}"*.tar.bz* 2>/dev/null || true
rm -f /home/Backups/"${HostPrefix}${AppName}"*.tar.bz* 2>/dev/null || true
#------------------------------------------------------------------------------
# Create Archive
#------------------------------------------------------------------------------
log "Creating archive ${HostPrefix}${ArchiveFile}"
tar cjf \
"/home/Backups/${HostPrefix}${ArchiveFile}" \
"/home/${AppUser}/"
log "Archive created successfully"
#------------------------------------------------------------------------------
# Upload Backup
#------------------------------------------------------------------------------
log "Loading Restic environment"
source /root/.restic-env
log "Starting Restic backup"
# Capture restic's JSON output in a temporary file. Errors and warnings
# still go to the run log via stderr.
RESTIC_OUTPUT="$(mktemp /tmp/restic-backup-XXXXXX.json)"
restic backup "/home/${AppUser}/" \
--json \
--tag apache \
--tag website \
--tag "$AppName" \
--tag "$HOSTNAME_FQDN" \
> "$RESTIC_OUTPUT" 2>> "$RUNLOG"
log "Restic backup completed successfully"
#------------------------------------------------------------------------------
# Log Restic Snapshot Details
#------------------------------------------------------------------------------
summary=$(jq -cR 'fromjson? | select(.message_type == "summary")' \
"$RESTIC_OUTPUT" | tail -n 1)
cleanup_restic_output
if [[ -n "$summary" ]]; then
log_snapshot_info "$summary"
else
log "WARNING: Restic returned no snapshot summary for ${AppName}"
fi
#------------------------------------------------------------------------------
# Cleanup
#------------------------------------------------------------------------------
if [[ -n "$AppPassword" ]]; then
log "Removing temporary SQL backup files"
rm -rf "/home/${AppUser}/SQLBackup"
fi
#------------------------------------------------------------------------------
# Completion
#------------------------------------------------------------------------------
log "Backup completed successfully"
log "Application: $AppName"
log "Archive: ${HostPrefix}${ArchiveFile}"
log "Host: $HOSTNAME_FQDN"
log "================================================================="
#------------------------------------------------------------------------------
# Append Current Run Log To Historical Log
#------------------------------------------------------------------------------
cat "$RUNLOG" >> "$LOGFILE"
#------------------------------------------------------------------------------
# Email Current Run Log
#------------------------------------------------------------------------------
send_email_log \
"SUCCESS: AppBackup ${AppName} (${HOSTNAME_FQDN})"
#------------------------------------------------------------------------------
# Cleanup Run Log
#------------------------------------------------------------------------------
rm -f "$RUNLOG"
exit 0
+13 -5
View File
@@ -1,8 +1,16 @@
# BashScripts # AckItsMe's Bash Scripts
This repository contains various bash utility scripts that I have written.
AppBackup.sh - A script used to back up websites hosted on apache with MySQL database backends. This repository contains various bash utility scripts.
dockerbackup.sh - A script to back up all docker compose stacks under a given subtree and upload the backups to a Restic bucket ## Scripts
- AppBackup.sh - A script used to back up websites hosted on apache with MySQL database backends.
- dockerbackup.sh - A script to back up all docker compose stacks under a given subtree and upload the backups to a Restic bucket
- rsync-monitor.sh - Script for monitoring an rsync operation. The script will attempt to extrapolate data transfer rate, percentage of completion, and estimated time remaining.
## Installation
Each script is used on its own, though some may have requirements for other modules. If the other prerequisites are available from the internet, the script will attempt to install them as needed.
## Contact
Charlie Silverman - ackitsme3@gmail.com
Project Link: https://gitea.ackcentuate.com/ackitsme/BashScripts.git
rsync-monitor.sh - Script for monitoring an rsync operation. The script will attempt to extrapolate data transfer rate, percentage of completion, and estimated time remaining.
+249 -42
View File
@@ -3,8 +3,8 @@
### Docker Stack Backup Script ### Docker Stack Backup Script
### Created by Charlie Silverman - ackitsme3@gmail.com ### Created by Charlie Silverman - ackitsme3@gmail.com
############################################################################### ###############################################################################
### Version: 1.1.0 ### Version: 2.1.4
### Updated: 2017-07-26 ### Updated: 2026-10-26
### Purpose: Backs up all docker stacks in a given directory to Restic bucket ### Purpose: Backs up all docker stacks in a given directory to Restic bucket
############################################################################### ###############################################################################
### Requirements: ### Requirements:
@@ -22,6 +22,31 @@
# Version History: # Version History:
# 1.0.0 - Initial build # 1.0.0 - Initial build
# 1.1.0 - Added exception list via text file # 1.1.0 - Added exception list via text file
# 1.1.1 - Added snapshot size to log
# 1.1.2 - Added aggregate backup size to end of log
# 1.1.3 - Added GiB to total backup size information
# 2.0.0 - Modified to save all files to a folder tree for backup rather than
# creating a single monolithic archive. This will allow restic to
# properly manage snapshots
# 2.0.1 - Changed exception lookup to case-insensitive
# 2.1.0 - Added email delivery of current execution log. Added
# success/failure/interruption notifications and run-specific logs.
# 2.1.1 - Added automatic cleanup of staging directories on backup failure
# or interruption.
# Added cleanup_staging() helper function.
# Added CURRENT_STAGING_DIR tracking.
# Added automatic RUN_LOG cleanup during failure and interruption.
# Improved housekeeping to prevent orphaned staging data after
# unexpected termination.
# 2.1.2 - Added EMAIL_TO and EMAIL_FROM variables for reports.
# 2.1.3 - Added DOCKERUSER variable for account to use for launching contaners
# 2.1.4 - Restic snapshot details (ID, parent, file/dir counts, data added,
# total size, duration) are now written to the logs.
# restic backup now runs with --json; snapshot size is read from the
# backup summary instead of parsing 'restic snapshots' table output,
# so it always refers to this run's snapshot.
# Removed dependency on bc (awk used for arithmetic).
# Added total new data uploaded to end of log.
set -Eeuo pipefail set -Eeuo pipefail
@@ -29,23 +54,47 @@ set -Eeuo pipefail
# Configuration # Configuration
############################################################################### ###############################################################################
COMPOSE_ROOT="/home/ackitsme/docker" COMPOSE_ROOT="/home/docker"
LOG_DIR="/home/ackitsme/docker/backuplogs" LOG_DIR="/home/docker/backuplogs"
mkdir -p "${LOG_DIR}"
LOG_FILE="${LOG_DIR}/docker-backup_$(date +%Y%m%d-%H%M%S).log" LOG_FILE="${LOG_DIR}/docker-backup_$(date +%Y%m%d-%H%M%S).log"
EMAIL_TO="charlie@kiscc.com"
EMAIL_FROM="kisprodweb.dockerbackup@kiscc.com"
DOCKERUSER="csilverman"
# Restic repository configuration # Restic repository configuration
export RESTIC_REPOSITORY="s3:https://s3.us-west-004.backblazeb2.com/ackitsbucket/docker"
export RESTIC_ENV_FILE="/root/.restic-env" export RESTIC_ENV_FILE="/root/.restic-env"
# Load Restic environment # Load Restic environment
source $RESTIC_ENV_FILE source "$RESTIC_ENV_FILE"
HOSTNAME_SHORT="$(hostname -s)" HOSTNAME_SHORT="$(hostname -s)"
TIMESTAMP="$(date +%Y%m%d-%H%M%S)" TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
RUN_LOG="${LOG_DIR}/docker-backup_run_${TIMESTAMP}.log"
CURRENT_STAGING_DIR=""
RESTIC_OUTPUT=""
touch "${RUN_LOG}"
###############################################################################
# Verify Requirements
###############################################################################
for cmd in docker restic tar gzip jq awk; do
command -v "$cmd" >/dev/null 2>&1 || {
echo "Missing required command: $cmd"
exit 1
}
done
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Load exceptions, if any # Load exceptions, if any
if [ -f "dockerbackup-exception.list" ]; then if [ -f "${SCRIPT_DIR}/dockerbackup-exception.list" ]; then
mapfile -t SKIP_PROJECTS < dockerbackup-exception.list mapfile -t SKIP_PROJECTS < "${SCRIPT_DIR}/dockerbackup-exception.list"
else
SKIP_PROJECTS=()
fi fi
@@ -55,56 +104,170 @@ fi
## Log to screen and log file ## Log to screen and log file
log() { log() {
local message="[$(date '+%F %T')] $*" local message
message="[$(date '+%F %T')] $*"
echo "$message" echo "$message"
echo "$message" >> "$LOG_FILE" echo "$message" >> "$LOG_FILE"
echo "$message" >> "$RUN_LOG"
}
## Convert a byte count to a human-readable size (e.g. 1.23GiB)
human_bytes() {
local bytes="${1:-0}"
if command -v numfmt >/dev/null 2>&1; then
numfmt --to=iec-i --suffix=B --format="%.2f" "$bytes" 2>/dev/null \
|| echo "${bytes} B"
else
echo "${bytes} B"
fi
}
## Write Restic snapshot details from a backup summary (JSON) to the log
log_snapshot_info() {
local summary="$1"
local snapshot_id parent_id
local files_new files_changed files_unmodified
local dirs_new dirs_changed dirs_unmodified
local data_added data_added_packed
local total_files total_bytes total_duration
snapshot_id=$(jq -r '.snapshot_id // "none"' <<< "$summary")
parent_id=$(restic snapshots "$snapshot_id" --json 2>/dev/null \
| jq -r '.[0].parent // "none"' 2>/dev/null || echo "unknown")
files_new=$(jq -r '.files_new // 0' <<< "$summary")
files_changed=$(jq -r '.files_changed // 0' <<< "$summary")
files_unmodified=$(jq -r '.files_unmodified // 0' <<< "$summary")
dirs_new=$(jq -r '.dirs_new // 0' <<< "$summary")
dirs_changed=$(jq -r '.dirs_changed // 0' <<< "$summary")
dirs_unmodified=$(jq -r '.dirs_unmodified // 0' <<< "$summary")
data_added=$(jq -r '.data_added // 0' <<< "$summary")
data_added_packed=$(jq -r '.data_added_packed // empty' <<< "$summary")
total_files=$(jq -r '.total_files_processed // 0' <<< "$summary")
total_bytes=$(jq -r '.total_bytes_processed // 0' <<< "$summary")
total_duration=$(jq -r '.total_duration // 0' <<< "$summary")
log "---------------- Restic Snapshot -----------------"
log "Snapshot ID: ${snapshot_id:0:8} (${snapshot_id})"
log "Parent snapshot: ${parent_id:0:8}"
log "Tags: docker, compose, ${project_name}, ${HOSTNAME_SHORT}"
log "Files: ${files_new} new, ${files_changed} changed, ${files_unmodified} unmodified"
log "Directories: ${dirs_new} new, ${dirs_changed} changed, ${dirs_unmodified} unmodified"
if [[ -n "$data_added_packed" ]]; then
log "Data added: $(human_bytes "$data_added") ($(human_bytes "$data_added_packed") stored)"
else
log "Data added: $(human_bytes "$data_added")"
fi
log "Snapshot size: $(human_bytes "$total_bytes") in ${total_files} files"
log "Duration: $(awk -v d="$total_duration" 'BEGIN{printf "%.1f", d}') seconds"
log "--------------------------------------------------"
} }
## Check if specified project is in the exception list ## Check if specified project is in the exception list
should_skip() { should_skip() {
local project="$1" local project="$1"
shopt -s nocasematch
for pattern in "${SKIP_PROJECTS[@]}"; do for pattern in "${SKIP_PROJECTS[@]}"; do
if [[ "$project" == $pattern ]]; then if [[ "$project" == $pattern ]]; then
shopt -u nocasematch
return 0 return 0
fi fi
done done
shopt -u nocasematch
return 1 return 1
} }
## Check if docker container is running
is_container_running() {
local container_name="$1"
docker inspect -f '{{.State.Running}}' "$container_name" 2>/dev/null | grep -q '^true$'
}
## Restart composer stack ## Restart composer stack
restart_stack() { restart_stack() {
local compose_dir="$1" local compose_dir="$1"
log "Restarting stack: ${compose_dir}" log "Restarting stack: ${compose_dir}"
( su - $DOCKERUSER -c \
sudo -u ackitsme docker compose up -d "cd '${compose_dir}' && docker compose up -d" \
su - ackitsme -c "cd ${compose_dir};docker compose up -d" || log "WARNING: Failed to restart stack ${compose_dir}"
}
) || log "WARNING: Failed to restart stack ${compose_dir}" ## Send E-mail Notification
send_email_log() {
local subject="$1"
mailx -s "$subject" -r "$EMAIL_FROM" "$EMAIL_TO" < "$RUN_LOG"
}
## Cleanup Function
cleanup_staging() {
if [[ -n "${CURRENT_STAGING_DIR:-}" ]] && \
[[ -d "${CURRENT_STAGING_DIR}" ]]; then
log "Cleaning up staging directory: ${CURRENT_STAGING_DIR}"
rm -rf "${CURRENT_STAGING_DIR}" || \
log "WARNING: Failed to remove ${CURRENT_STAGING_DIR}"
fi
if [[ -n "${RESTIC_OUTPUT:-}" ]]; then
rm -f "${RESTIC_OUTPUT}"
fi
}
## Error Handler
error_handler() {
local exit_code=$?
trap - ERR
local line_number=$1
log "ERROR: Backup failed on line ${line_number} (exit code ${exit_code})"
cleanup_staging
send_email_log \
"FAILED: Docker Backup (${HOSTNAME_SHORT})"
rm -f "${RUN_LOG}"
exit "${exit_code}"
}
## Interrupt Handler
interrupt_handler() {
trap - SIGINT SIGTERM
log "ERROR: Backup interrupted"
cleanup_staging
send_email_log \
"INTERRUPTED: Docker Backup (${HOSTNAME_SHORT})"
rm -f "${RUN_LOG}"
exit 1
} }
############################################################################### ###############################################################################
# Verify Requirements # Trap Handlers
############################################################################### ###############################################################################
trap 'error_handler $LINENO' ERR
trap interrupt_handler SIGINT SIGTERM
for cmd in docker restic tar gzip; do log "===================================================="
command -v "$cmd" >/dev/null 2>&1 || { log "Docker Backup Starting"
echo "Missing required command: $cmd" log "Host: ${HOSTNAME_SHORT}"
exit 1 log "Historical Log: ${LOG_FILE}"
} log "Run Log: ${RUN_LOG}"
done log "===================================================="
############################################################################### ###############################################################################
# Locate Compose Files # Locate Compose Files
@@ -121,6 +284,11 @@ mapfile -t COMPOSE_FILES < <(
if [ "${#COMPOSE_FILES[@]}" -eq 0 ]; then if [ "${#COMPOSE_FILES[@]}" -eq 0 ]; then
log "No compose files found." log "No compose files found."
send_email_log \
"Docker Backup - No Compose Files (${HOSTNAME_SHORT})"
rm -f "${RUN_LOG}"
exit 0 exit 0
fi fi
@@ -128,6 +296,9 @@ fi
# Process Each Compose Project # Process Each Compose Project
############################################################################### ###############################################################################
total_size=0.00
total_added=0
for compose_file in "${COMPOSE_FILES[@]}"; do for compose_file in "${COMPOSE_FILES[@]}"; do
compose_dir="$(dirname "$compose_file")" compose_dir="$(dirname "$compose_file")"
@@ -143,7 +314,8 @@ for compose_file in "${COMPOSE_FILES[@]}"; do
log "Compose file: ${compose_file}" log "Compose file: ${compose_file}"
log "==================================================" log "=================================================="
STAGING_DIR="/tmp/docker-backup-${project_name}-${TIMESTAMP}" STAGING_DIR="/home/Backups/docker-backup-${project_name}"
CURRENT_STAGING_DIR="${STAGING_DIR}"
mkdir -p \ mkdir -p \
"${STAGING_DIR}/images" \ "${STAGING_DIR}/images" \
@@ -193,11 +365,11 @@ for compose_file in "${COMPOSE_FILES[@]}"; do
################################################### ###################################################
log "Backing up compose files..." log "Backing up compose files..."
mkdir -p "${STAGING_DIR}/compose"
tar -czf \ cp -a \
"${STAGING_DIR}/${project_name}-compose.tar.gz" \ "${compose_dir}" \
-C "$(dirname "$compose_dir")" \ "${STAGING_DIR}/compose/"
"$(basename "$compose_dir")"
################################################### ###################################################
# Backup Images # Backup Images
@@ -206,18 +378,15 @@ for compose_file in "${COMPOSE_FILES[@]}"; do
log "Backing up images..." log "Backing up images..."
while read -r image; do while read -r image; do
[ -z "$image" ] && continue [ -z "$image" ] && continue
if docker image inspect "$image" >/dev/null 2>&1; then if docker image inspect "$image" >/dev/null 2>&1; then
safe_name=$(echo "$image" | sed 's#[/:]#_#g') safe_name=$(echo "$image" | sed 's#[/:]#_#g')
log "Saving image: $image" log "Saving image: $image"
docker save "$image" \ docker save "$image" \
| gzip \ > "${STAGING_DIR}/images/${safe_name}.tar"
> "${STAGING_DIR}/images/${safe_name}.tar.gz"
fi fi
done < "${STAGING_DIR}/metadata/images.list" done < "${STAGING_DIR}/metadata/images.list"
@@ -229,7 +398,6 @@ for compose_file in "${COMPOSE_FILES[@]}"; do
log "Backing up volumes..." log "Backing up volumes..."
while read -r volume; do while read -r volume; do
[ -z "$volume" ] && continue [ -z "$volume" ] && continue
log "Saving volume: $volume" log "Saving volume: $volume"
@@ -238,7 +406,7 @@ for compose_file in "${COMPOSE_FILES[@]}"; do
-v "${volume}:/source:ro" \ -v "${volume}:/source:ro" \
-v "${STAGING_DIR}/volumes:/backup" \ -v "${STAGING_DIR}/volumes:/backup" \
alpine \ alpine \
tar -czf "/backup/${volume}.tar.gz" -C /source . sh -c "cd /source && tar cf - . | tar xf - -C /backup"
done < "${STAGING_DIR}/metadata/volumes.list" done < "${STAGING_DIR}/metadata/volumes.list"
@@ -270,22 +438,53 @@ for compose_file in "${COMPOSE_FILES[@]}"; do
log "Uploading project backup to Restic..." log "Uploading project backup to Restic..."
# Capture restic's JSON output (outside the staging dir so it is
# not part of the backup). Errors/warnings still go to stderr.
RESTIC_OUTPUT="$(mktemp /tmp/restic-backup-XXXXXX.json)"
restic backup \ restic backup \
"${STAGING_DIR}" \ "${STAGING_DIR}" \
--json \
--tag docker \ --tag docker \
--tag compose \ --tag compose \
--tag "${project_name}" \ --tag "${project_name}" \
--tag "${HOSTNAME_SHORT}" --tag "${HOSTNAME_SHORT}" \
> "${RESTIC_OUTPUT}"
###################################################
# Log snapshot details and get backup size
###################################################
summary=$(jq -cR 'fromjson? | select(.message_type == "summary")' \
"${RESTIC_OUTPUT}" | tail -n 1)
rm -f "${RESTIC_OUTPUT}"
RESTIC_OUTPUT=""
if [[ -n "$summary" ]]; then
log_snapshot_info "$summary"
snap_bytes=$(jq -r '.total_bytes_processed // 0' <<< "$summary")
added_bytes=$(jq -r '.data_added // 0' <<< "$summary")
else
log "WARNING: Restic returned no snapshot summary for ${project_name}"
snap_bytes=0
added_bytes=0
fi
snapnum=$(awk -v b="$snap_bytes" 'BEGIN{printf "%.2f", b / 1073741824}')
################################################### ###################################################
# Cleanup Local Backup # Cleanup Local Backup
################################################### ###################################################
rm -rf "${STAGING_DIR}" rm -rf "${STAGING_DIR}"
CURRENT_STAGING_DIR=""
log "Project complete: ${project_name}" log "Project complete: ${project_name} ${snapnum} GiB"
log "" log ""
total_size=$(awk -v a="$total_size" -v b="$snapnum" 'BEGIN{printf "%.2f", a + b}')
total_added=$(awk -v a="$total_added" -v b="$added_bytes" 'BEGIN{printf "%.0f", a + b}')
done done
############################################################################### ###############################################################################
@@ -301,3 +500,11 @@ restic forget \
--prune --prune
log "All compose projects backed up successfully." log "All compose projects backed up successfully."
log "Total Backup Size: $total_size GiB"
log "Total New Data Uploaded: $(human_bytes "$total_added")"
send_email_log \
"SUCCESS: Docker Backup (${HOSTNAME_SHORT})" || \
log "WARNING: Unable to send success email."
rm -f "${RUN_LOG}"
+322
View File
@@ -0,0 +1,322 @@
#!/bin/bash
#
# Script Name: openwrtupload.sh
# Description:
# Backs up OpenWrt configuration archives to a Backblaze B2 repository
# using Restic. Each .gz archive found in the source directory is
# extracted to a staging directory, backed up with Restic, and then
# removed along with the staging directory.
#
# Usage:
# openwrtupload.sh
#
# Author: Charlie Silverman
# Version: 20261031-01
#
# Requirements:
# restic, jq, awk, tar, numfmt (optional, for human-readable sizes).
# Restic environment file: /root/.restic-env
#
# Versioning:
# Versions use the format YYYYMMDD-xx, where YYYYMMDD is the release date
# and xx is the release number for that day, starting at 01.
#
# Revision History:
# 20261031-01 - Added script header and YYYYMMDD-xx versioning.
# Added strict error handling with error/interrupt logging;
# an archive is no longer deleted if its backup fails.
# Staging directory is cleaned up on failure or interrupt.
# Restic snapshot details (ID, parent, tags, file/dir counts,
# data added, snapshot size, duration) are now written to the
# log. restic backup now runs with --json and the details are
# read from its backup summary. Added totals at end of run.
# Added required-command check.
#
set -o errexit
set -o nounset
set -o pipefail
SCRIPT_VERSION="20261031-01"
#------------------------------------------------------------------------------
# Configuration
#------------------------------------------------------------------------------
LOG_FILE="/home/glibackup/openwrt-backup.log"
SOURCE_DIR="/home/glibackup/openwrt"
STAGING_DIR="/home/Backups/openwrt"
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
HOSTNAME_FQDN="$(hostname -f)"
RESTIC_TAGS=(openwrt backup)
# Restic repository configuration
export RESTIC_REPOSITORY="s3:https://s3.us-west-004.backblazeb2.com/ackitsbucket/openwrt"
export RESTIC_ENV_FILE="/root/.restic-env"
RESTIC_OUTPUT=""
STAGING_IN_USE=""
#------------------------------------------------------------------------------
# Logging Function
#------------------------------------------------------------------------------
log() {
local message
message="[$(date '+%F %T')] $*"
echo "$message"
echo "$message" >> "$LOG_FILE"
}
#------------------------------------------------------------------------------
# Convert a byte count to a human-readable size (e.g. 1.23GiB)
#------------------------------------------------------------------------------
human_bytes() {
local bytes="${1:-0}"
if command -v numfmt >/dev/null 2>&1; then
numfmt --to=iec-i --suffix=B --format="%.2f" "$bytes" 2>/dev/null \
|| echo "${bytes} B"
else
echo "${bytes} B"
fi
}
#------------------------------------------------------------------------------
# Write Restic snapshot details from a backup summary (JSON) to the log
#------------------------------------------------------------------------------
log_snapshot_info() {
local summary="$1"
local snapshot_id parent_id
local files_new files_changed files_unmodified
local dirs_new dirs_changed dirs_unmodified
local data_added data_added_packed
local total_files total_bytes total_duration
local tags
snapshot_id=$(jq -r '.snapshot_id // "none"' <<< "$summary")
parent_id=$(restic snapshots "$snapshot_id" --json 2>/dev/null \
| jq -r '.[0].parent // "none"' 2>/dev/null || echo "unknown")
files_new=$(jq -r '.files_new // 0' <<< "$summary")
files_changed=$(jq -r '.files_changed // 0' <<< "$summary")
files_unmodified=$(jq -r '.files_unmodified // 0' <<< "$summary")
dirs_new=$(jq -r '.dirs_new // 0' <<< "$summary")
dirs_changed=$(jq -r '.dirs_changed // 0' <<< "$summary")
dirs_unmodified=$(jq -r '.dirs_unmodified // 0' <<< "$summary")
data_added=$(jq -r '.data_added // 0' <<< "$summary")
data_added_packed=$(jq -r '.data_added_packed // empty' <<< "$summary")
total_files=$(jq -r '.total_files_processed // 0' <<< "$summary")
total_bytes=$(jq -r '.total_bytes_processed // 0' <<< "$summary")
total_duration=$(jq -r '.total_duration // 0' <<< "$summary")
tags=$(IFS=,; echo "${RESTIC_TAGS[*]}" | sed 's/,/, /g')
log "---------------- Restic Snapshot -----------------"
log "Snapshot ID: ${snapshot_id:0:8} (${snapshot_id})"
log "Parent snapshot: ${parent_id:0:8}"
log "Tags: ${tags}"
log "Files: ${files_new} new, ${files_changed} changed, ${files_unmodified} unmodified"
log "Directories: ${dirs_new} new, ${dirs_changed} changed, ${dirs_unmodified} unmodified"
if [[ -n "$data_added_packed" ]]; then
log "Data added: $(human_bytes "$data_added") ($(human_bytes "$data_added_packed") stored)"
else
log "Data added: $(human_bytes "$data_added")"
fi
log "Snapshot size: $(human_bytes "$total_bytes") in ${total_files} files"
log "Duration: $(awk -v d="$total_duration" 'BEGIN{printf "%.1f", d}') seconds"
log "--------------------------------------------------"
}
#------------------------------------------------------------------------------
# Cleanup Function
#------------------------------------------------------------------------------
cleanup() {
if [[ -n "${STAGING_IN_USE:-}" && -d "$STAGING_DIR" ]]; then
log "Cleaning up staging directory ${STAGING_DIR}"
rm -rf "$STAGING_DIR" || log "WARNING: Failed to remove ${STAGING_DIR}"
STAGING_IN_USE=""
fi
if [[ -n "${RESTIC_OUTPUT:-}" ]]; then
rm -f "$RESTIC_OUTPUT"
RESTIC_OUTPUT=""
fi
}
#------------------------------------------------------------------------------
# Error Handling
#------------------------------------------------------------------------------
error_handler() {
local exit_code=$?
local line_number=$1
trap - ERR
log "ERROR: Backup failed on line ${line_number} with exit code ${exit_code}"
cleanup
log "----------------------------------------------------"
exit "${exit_code}"
}
interrupt_handler() {
trap - SIGINT SIGTERM
log "ERROR: Backup interrupted by signal"
cleanup
log "----------------------------------------------------"
exit 1
}
trap 'error_handler $LINENO' ERR
trap interrupt_handler SIGINT SIGTERM
#------------------------------------------------------------------------------
# Startup Logging
#------------------------------------------------------------------------------
log "===================================================="
log "OpenWrt Backup Starting (openwrtupload.sh v${SCRIPT_VERSION})"
log "Host: ${HOSTNAME_FQDN}"
log "Source Directory: ${SOURCE_DIR}"
log "Log File: ${LOG_FILE}"
log "===================================================="
#------------------------------------------------------------------------------
# Verify Requirements
#------------------------------------------------------------------------------
for cmd in restic jq awk tar; do
if ! command -v "$cmd" >/dev/null 2>&1; then
log "ERROR: Missing required command: $cmd"
exit 1
fi
done
#------------------------------------------------------------------------------
# Load Restic Environment
#------------------------------------------------------------------------------
# shellcheck source=/dev/null
source "$RESTIC_ENV_FILE"
#------------------------------------------------------------------------------
# Locate Archives
#------------------------------------------------------------------------------
shopt -s nullglob
ARCHIVES=("$SOURCE_DIR"/*.gz)
shopt -u nullglob
if [[ ${#ARCHIVES[@]} -eq 0 ]]; then
log "No backup files found."
log "----------------------------------------------------"
exit 0
fi
log "Found ${#ARCHIVES[@]} archive(s) to back up"
#------------------------------------------------------------------------------
# Process Each Archive
#------------------------------------------------------------------------------
TAG_ARGS=()
for tag in "${RESTIC_TAGS[@]}"; do
TAG_ARGS+=(--tag "$tag")
done
total_bytes_all=0
total_added_all=0
archive_count=0
for FILE in "${ARCHIVES[@]}"; do
log "Backing up archive ${FILE}..."
# Start from an empty staging directory
rm -rf "$STAGING_DIR"
mkdir -p "$STAGING_DIR"
STAGING_IN_USE="yes"
log "Extracting archive ${FILE} to ${STAGING_DIR}"
tar xzf "$FILE" -C "$STAGING_DIR"
log "Backing up ${STAGING_DIR} with restic"
# Capture restic's JSON output in a temporary file. Errors and
# warnings still go to the log via stderr.
RESTIC_OUTPUT="$(mktemp /tmp/restic-backup-XXXXXX.json)"
restic backup \
"$STAGING_DIR" \
--json \
"${TAG_ARGS[@]}" \
> "$RESTIC_OUTPUT" 2>> "$LOG_FILE"
summary=$(jq -cR 'fromjson? | select(.message_type == "summary")' \
"$RESTIC_OUTPUT" | tail -n 1)
rm -f "$RESTIC_OUTPUT"
RESTIC_OUTPUT=""
if [[ -n "$summary" ]]; then
log_snapshot_info "$summary"
snap_bytes=$(jq -r '.total_bytes_processed // 0' <<< "$summary")
added_bytes=$(jq -r '.data_added // 0' <<< "$summary")
else
log "WARNING: Restic returned no snapshot summary for ${FILE}"
snap_bytes=0
added_bytes=0
fi
total_bytes_all=$(awk -v a="$total_bytes_all" -v b="$snap_bytes" 'BEGIN{printf "%.0f", a + b}')
total_added_all=$(awk -v a="$total_added_all" -v b="$added_bytes" 'BEGIN{printf "%.0f", a + b}')
archive_count=$((archive_count + 1))
log "Cleaning up staging directory ${STAGING_DIR}"
rm -rf "$STAGING_DIR"
STAGING_IN_USE=""
log "Backup Complete."
log "Removing archive file ${FILE}"
rm -f "$FILE"
log "----------------------------------------------------"
done
#------------------------------------------------------------------------------
# Completion
#------------------------------------------------------------------------------
log "All archives backed up successfully."
log "Archives Processed: ${archive_count}"
log "Total Backup Size: $(human_bytes "$total_bytes_all")"
log "Total New Data Uploaded: $(human_bytes "$total_added_all")"
log "===================================================="
exit 0
+326 -173
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"
while [[ $# -gt 0 ]]; do ###############################################################################
echo $1 # Logging Functions
IFS="=" read -r command value <<< "$1" ###############################################################################
echo "Command: $command"
echo "Value: $value" log_info() {
echo "Value length: ${#value}" local MESSAGE="$1"
case "$command" in echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO ] $MESSAGE" | tee -a "$LOGFILE"
--source) }
if [ "${#value})" -gt 0 ]; then
SRCFOLDER="$value" log_warn() {
shift 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 else
SRCFOLDER="$2" MOUNT=$(df "$DIR" | awk 'NR==2 {print $6}')
shift 2
fi fi
;;
--destination) if [[ "$MOUNT" != "$DIR" ]]; then
if [ "${#value})" -gt 0 ]; then du -sm "$DIR" | awk '{print $1}'
DSTFOLDER="$value"
shift
else else
DSTFOLDER="$2" local USED_KB
shift 2 USED_KB=$(df "$DIR" | awk 'NR==2 {print $3}')
echo $(( USED_KB / 1024 ))
fi fi
;; }
--sleep)
if [ "${#value})" -gt 0 ]; then format_size() {
SLEEP="$value"
shift 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 else
SLEEP="$2" echo "${SIZE_MB}MB"
shift 2
fi fi
}
###############################################################################
# Option Processing
###############################################################################
while getopts ":s:d:t:l:h" opt; do
case "${opt}" in
s)
SRCFOLDER="$OPTARG"
;; ;;
--help)
HELP=true d)
shift DSTFOLDER="$OPTARG"
;; ;;
*)
echo "ERROR: Unknown argument: $1" t)
HELP=true SLEEP="$OPTARG"
break
;; ;;
l)
LOGFILE="$OPTARG"
;;
h)
show_help
exit 0
;;
:)
echo "Missing argument for -${OPTARG}"
exit 1
;;
\?)
echo "Invalid option: -${OPTARG}"
exit 1
;;
esac 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
echo -e "\n\033[0;31mE\e[4mRROR: Source Folder $SRCFOLDER does not exist.\e[0m"
ERROR=1 ERROR=1
fi fi
# Check if destination folder exists
if [[ ! -d "${DSTFOLDER}" ]]; then if [[ -z "$DSTFOLDER" ]]; then
echo -e "\n\033[0;31mE\e[4mRROR: Destination Folder $DSTFOLDER does not exist.\e[0m" log_error "Destination folder not specified."
ERROR=1 ERROR=1
fi fi
# Verify that a sleep value was supplied
if [ -z "${SLEEP}" ]; then if [[ ! -d "$SRCFOLDER" ]]; then
echo -e "\n\033[0;31mE\e[4mRROR: Sleep delay not specified.\e[0m" log_error "Source folder does not exist: $SRCFOLDER"
ERROR=1 ERROR=1
else fi
# Verify that the supplied sleep value is actually a Number
re='^[0-9]+$' if [[ ! -d "$DSTFOLDER" ]]; then
if ! [[ $SLEEP =~ $re ]]; then log_error "Destination folder does not exist: $DSTFOLDER"
echo -e "\n\033[0;31m\e[4mERROR: Sleep delay invalid.\e[0m"
ERROR=1 ERROR=1
fi
fi
fi fi
# Help display if ! [[ "$SLEEP" =~ ^[0-9]+$ ]]; then
if $HELP; then log_error "Sleep value must be numeric."
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 (( ERROR > 0 )); then
exit 1
# 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}')
fi fi
# Determining the size of the source data. If the source path ###############################################################################
# matches a mount point from df, assuming that mount point data # Calculate Source Size
# 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"
fi
echo "Monitoring $DSTFOLDER..." log_info "Calculating source size."
# Getting the mount point that relates to the destination path, stripping off MEGS=$(get_directory_size_mb "$SRCFOLDER")
# the trailing slash if found MEGSGB=$(echo "scale=3;$MEGS/1024" | bc)
if [[ "${$DSTFOLDER: -1}" = "/" ]]; then MEGSDISPLAY=$(format_size "$MEGS")
DSTMOUNT=$(df ${DSTFOLDER%?} | sed -n '2p' | awk '{print $6}')
else log_info "Source size: $MEGSDISPLAY"
DSTMOUNT=$(df $DSTFOLDER | sed -n '2p' | awk '{print $6}')
fi ###############################################################################
# Initialize Destination
###############################################################################
FIRST=$(get_directory_size_mb "$DSTFOLDER")
FIRSTGB=$(echo "scale=3;$FIRST/1024" | bc)
FIRSTDISPLAY=$(format_size "$FIRST")
SECONDSLEFT=1
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 # Monitor Loop
END=$(du -sm $DSTFOLDER |awk '{print $1}') ###############################################################################
else
END=$(df $DSTFOLDER | sed -n '2p' | awk '{print $3}') while true; do
END=$(($END / 1024))
fi sleep "$SLEEP"
END=$(get_directory_size_mb "$DSTFOLDER")
ENDDISPLAY=$(format_size "$END")
ENDGB=$(echo "scale=3;$END/1024" | bc)
TIMENOW=$(date +%s) TIMENOW=$(date +%s)
TIMEDIFF=$(($TIMENOW-$STARTTIME)) TIMEDIFF=$(( TIMENOW - STARTTIME ))
DOWNLOADED=$(( $END-$START ))
TOTALDOWNLOADED=$(( $END-$FIRST ))
if [[ $DOWNLOADED -lt 0 ]]; then
DOWNLOADED=0
fi
ENDDISPLAY="${END}MB" DOWNLOADED=$(( END - START ))
ENDGB=$(echo "scale=2;$END/1024" | bc) TOTALDOWNLOADED=$(( END - FIRST ))
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')/ CURRENTTIME=$(date '+%Y-%m-%d %H:%M:%S')
CURRENTPCT=$(echo "scale=2;($ENDGB/$MEGSGB)*100" | bc | cut -d . -f 1)
if [[ "$DOWNLOADED" == "0" ]]; then 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..." echo "[${CURRENTTIME}] ${ENDDISPLAY} of ${MEGSDISPLAY} (${CURRENTPCT}%) No change detected | Sleeping ${SLEEP}s..."
continue continue
fi fi
OVERALLMEGSPERSECOND=$(( $TOTALDOWNLOADED/$TIMEDIFF )) if (( TIMEDIFF <= 0 || TOTALDOWNLOADED <= 0 )); then
OVERALLSECONDSLEFT=$(( ($MEGS-$END)/$TOTALDOWNLOADED*$TIMEDIFF )) START="$END"
OVERALLDAYS=$(( $OVERALLSECONDSLEFT/60/60/24 )) continue
OVERALLHOURS=$(( ($OVERALLSECONDSLEFT-$OVERALLDAYS*60*60*24)/60/60 )) fi
OVERALLMINUTES=$(( ($OVERALLSECONDSLEFT-$OVERALLDAYS*60*60*24-$OVERALLHOURS*60*60)/60 ))
OVERALLSECONDS=$(( $OVERALLSECONDSLEFT-$OVERALLDAYS*60*60*24-$OVERALLHOURS*60*60-$OVERALLMINUTES*60 )) OVERALLMEGSPERSECOND=$(( TOTALDOWNLOADED / TIMEDIFF ))
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..."
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"
START=$END
done done