#!/bin/bash # Script Metadata #name=Mover Status Script #description=This script monitors the progress of the "Mover" process and posts updates to a Discord/Telegram webhook. #backgroundOnly=true #arrayStarted=true # --------------------------------------------------------- # Mover Status Script # --------------------------------------------------------- # Monitors Unraid's mover process and posts progress updates # to Discord and/or Telegram webhooks. # # Dependencies: bash, curl, jq, du, pgrep, date # Runs as a backgroundOnly Unraid user script. # --------------------------------------------------------- # Simple timestamp for logs log() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" } # Log the starting message log "Starting Mover Status Monitor..." # ------------------------------------------- # Script Settings: Edit these! # ------------------------------------------- # Configure basic settings and webhook details USE_TELEGRAM=false # Enable notifications to Telegram USE_DISCORD=false # Enable notifications to Discord TELEGRAM_BOT_TOKEN="xxxx" # Telegram bot token TELEGRAM_CHAT_ID="xxxx" # Telegram chat ID DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/xxxx/xxxx" # Discord webhook URL DISCORD_NAME_OVERRIDE="Mover Bot" # Display name for Discord notifications NOTIFICATION_INCREMENT=25 # Notification frequency in percentage increments DRY_RUN=false # Enable this to test the notifications without actual monitoring ENABLE_DEBUG=false # Set to true to enable debug logging DU_POLL_INTERVAL=30 # Seconds between disk usage recalculations (higher = less I/O load) CACHE_PATH="/mnt/cache" # Path to cache directory to monitor ENABLE_FILE_INFO=false # Show file count and current file in notifications (requires Mover Tuning plugin) # ------------------------------------------- # Webhook Messages: Edit these if you want # ------------------------------------------- # Custom messages for each notification point TELEGRAM_MOVING_MESSAGE="Moving data from SSD Cache to HDD Array. Progress: {percent}% complete. Remaining data: {remaining_data}. Estimated completion time: {etc}. Note: Services like Plex may run slow or be unavailable during the move." DISCORD_MOVING_MESSAGE="Moving data from SSD Cache to HDD Array.\nProgress: **{percent}%** complete.\nRemaining data: {remaining_data}.\nEstimated completion time: {etc}.\n\nNote: Services like Plex may run slow or be unavailable during the move." COMPLETION_MESSAGE="Moving has been completed!" DISCORD_COMPLETION_MESSAGE="" # If empty, falls back to COMPLETION_MESSAGE TELEGRAM_COMPLETION_MESSAGE="" # If empty, falls back to COMPLETION_MESSAGE # --------------------------------------- # Exclusion Folders: Define paths to exclude # --------------------------------------- # Set EXCLUDE_PATH_XX to directories you want to exclude from being monitored. # Leave EXCLUDE_PATH_XX variables empty if no exclusions are needed. # This will result in monitoring the entire directory specified in the script (/mnt/cache). # # Example usage: # EXCLUDE_PATH_01="/mnt/cache/your/excluded/folder" # EXCLUDE_PATH_02="/mnt/cache/another/excluded/folder" # EXCLUDE_PATH_03="/mnt/cache/maybe/a/.hidden/folder" # Add more EXCLUDE_PATH_XX as needed. # shellcheck disable=SC2034 EXCLUDE_PATH_01="" # shellcheck disable=SC2034 EXCLUDE_PATH_02="" # --------------------------------- # Do Not Modify: Script essentials # --------------------------------- # Script versioning - check for updates CURRENT_VERSION="0.0.10" # Function to check the latest version check_latest_version() { LATEST_VERSION=$(curl -fsSL --connect-timeout 5 --max-time 10 "https://api.github.com/repos/engels74/mover-status/releases" | jq -r .[0].tag_name) || LATEST_VERSION="" } # Initialize to -1 to ensure 0% notification LAST_NOTIFIED=-1 # --------------------------------------------------------- # Do Not Modify: Variable checking! # --------------------------------------------------------- # Check if at least one notification method is enabled if ! $USE_TELEGRAM && ! $USE_DISCORD; then log "Error: Both USE_TELEGRAM and USE_DISCORD are set to false. At least one must be true." exit 1 fi # Check webhook configurations conditionally if [[ $USE_TELEGRAM == true ]]; then if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then log "Error: Telegram settings not configured correctly." exit 1 fi fi if [[ $USE_DISCORD == true ]]; then if ! [[ $DISCORD_WEBHOOK_URL =~ ^https://(discord\.com|discordapp\.com)/api/webhooks/ ]]; then log "Error: Invalid Discord webhook URL." exit 1 fi fi # Validate DU_POLL_INTERVAL is a positive integer if ! [[ "$DU_POLL_INTERVAL" =~ ^[0-9]+$ ]] || [ "$DU_POLL_INTERVAL" -eq 0 ]; then log "Error: DU_POLL_INTERVAL must be a positive integer. Got: '$DU_POLL_INTERVAL'" exit 1 fi # Validate CACHE_PATH exists if [ ! -d "$CACHE_PATH" ]; then log "Error: CACHE_PATH directory does not exist: '$CACHE_PATH'" exit 1 fi # Check latest version once at startup (after validation so we don't hit the API on misconfiguration) LATEST_VERSION="" check_latest_version # --------------------------------------------------------- # Do Not Modify: Dry-run check # --------------------------------------------------------- if $DRY_RUN; then log "Running in dry-run mode. No real monitoring will be performed." # Detect data source for informational purposes if [ -f "/usr/local/emhttp/state/mover.ini" ] && grep -q "TotalToSecondary" "/usr/local/emhttp/state/mover.ini" 2>/dev/null; then log "Dry-run: mover.ini detected (Mover Tuning plugin available)" dry_run_data_source="mover_ini" else log "Dry-run: Using du polling mode (no mover.ini found)" dry_run_data_source="du_polling" fi # Simulate data for notification dry_run_percent=50 # Arbitrary progress percentage for testing dry_run_remaining_data="500 GB" # Arbitrary remaining data amount for testing dry_run_datetime=$(date +"%B %d (%Y) - %H:%M:%S") dry_run_etc_discord="" dry_run_etc_telegram="01/01/2099, 12pm" # Simulate file info if available dry_run_file_count="" dry_run_current_file="" if $ENABLE_FILE_INFO && [ "$dry_run_data_source" = "mover_ini" ]; then dry_run_file_count="898/1796 files" dry_run_current_file="/mnt/cache/share/example_file.txt" fi # Determine color based on percentage if [ "$dry_run_percent" -le 34 ]; then dry_run_color=16744576 # Light Red elif [ "$dry_run_percent" -le 65 ]; then dry_run_color=16753920 # Light Orange else dry_run_color=9498256 # Light Green fi # Footer text with version checking footer_text="Version: v${CURRENT_VERSION}" if [[ -n "${LATEST_VERSION}" && "${LATEST_VERSION}" != "${CURRENT_VERSION}" ]]; then footer_text+=" (update available)" fi # Prepare messages with placeholders dry_run_value_message_discord="${DISCORD_MOVING_MESSAGE//\{percent\}/$dry_run_percent}" dry_run_value_message_discord="${dry_run_value_message_discord//\{remaining_data\}/$dry_run_remaining_data}" dry_run_value_message_discord="${dry_run_value_message_discord//\{etc\}/$dry_run_etc_discord}" dry_run_value_message_discord="${dry_run_value_message_discord//\{file_count\}/$dry_run_file_count}" dry_run_value_message_discord="${dry_run_value_message_discord//\{current_file\}/$dry_run_current_file}" dry_run_value_message_telegram="${TELEGRAM_MOVING_MESSAGE//\{percent\}/$dry_run_percent}" dry_run_value_message_telegram="${dry_run_value_message_telegram//\{remaining_data\}/$dry_run_remaining_data}" dry_run_value_message_telegram="${dry_run_value_message_telegram//\{etc\}/$dry_run_etc_telegram}" dry_run_value_message_telegram="${dry_run_value_message_telegram//\{file_count\}/$dry_run_file_count}" dry_run_value_message_telegram="${dry_run_value_message_telegram//\{current_file\}/$dry_run_current_file}" dry_run_value_message_telegram+=" ${footer_text}" # Send test notifications if $USE_TELEGRAM; then log "Sending test notification to Telegram..." dry_run_json_payload=$(jq -n \ --arg chat_id "$TELEGRAM_CHAT_ID" \ --arg text "$dry_run_value_message_telegram" \ '{chat_id: $chat_id, text: $text, disable_notification: "false", parse_mode: "HTML"}') /usr/bin/curl -s -o /dev/null -H "Content-Type: application/json" -X POST -d "$dry_run_json_payload" "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage" fi if $USE_DISCORD; then log "Sending test notification to Discord..." dry_run_notification_data='{ "username": "'"$DISCORD_NAME_OVERRIDE"'", "content": null, "embeds": [ { "title": "Mover: Moving Data", "description": "This is a test message from dry-run mode (data source: '"$dry_run_data_source"').", "color": '"$dry_run_color"', "fields": [ { "name": "'"$dry_run_datetime"'", "value": "'"${dry_run_value_message_discord}"'" } ], "footer": { "text": "'"$footer_text"'" } } ] }' /usr/bin/curl -s -o /dev/null -H "Content-Type: application/json" -X POST -d "$dry_run_notification_data" "$DISCORD_WEBHOOK_URL" fi log "Dry-run complete. Exiting script." exit 0 fi # --------------------------------------------------------- # Mover Status Script - Do Not Edit! # --------------------------------------------------------- # Prepare exclusion paths declare -a exclude_paths for var_name in "${!EXCLUDE_PATH_@}"; do if [ -n "${!var_name}" ]; then if [ ! -d "${!var_name}" ]; then log "Error: Exclusion path '${!var_name}' (${var_name}) does not exist." exit 1 fi exclude_paths+=("${!var_name}") fi done # Calculate total size of all excluded directories get_excluded_size() { local total=0 local path size for path in "${exclude_paths[@]}"; do if [ -d "$path" ]; then size=$(du -sb "$path" 2>/dev/null | cut -f1) total=$((total + ${size:-0})) fi done echo "$total" } # Check if any mover-related process is running (supports Unraid v7+ and Mover Tuning plugin) is_mover_running() { pgrep -x "mover" > /dev/null 2>&1 && return 0 pgrep -x "age_mover" > /dev/null 2>&1 && return 0 pgrep -f "^/usr/libexec/unraid/move" > /dev/null 2>&1 && return 0 return 1 } # Mover.ini path (written by Mover Tuning plugin's age_mover) MOVER_INI_PATH="/usr/local/emhttp/state/mover.ini" # State persistence paths STATE_DIR="/tmp/mover-status" STATE_FILE="${STATE_DIR}/state" LAST_RUN_FILE="${STATE_DIR}/last-run" # Global data source identifier: "mover_ini" or "du_polling" DATA_SOURCE="" # Progress globals (set by get_progress) PROGRESS_PERCENT=0 PROGRESS_REMAINING_BYTES=0 PROGRESS_MOVED_BYTES=0 PROGRESS_TOTAL_BYTES=0 PROGRESS_FILE_COUNT="" PROGRESS_REMAIN_FILES="" PROGRESS_CURRENT_FILE="" # Tracks bytes already moved when script started monitoring (for accurate late-join ETA) monitoring_start_bytes=0 # INI globals (set by read_mover_ini) INI_TOTAL_TO_SECONDARY=0 INI_REMAIN_TO_SECONDARY=0 INI_TOTAL_FILES=0 INI_REMAIN_FILES=0 INI_CURRENT_FILE="" INI_ACTION="" # Detect whether mover.ini is available and usable detect_data_source() { if [ -f "$MOVER_INI_PATH" ] && grep -q "TotalToSecondary" "$MOVER_INI_PATH" 2>/dev/null; then DATA_SOURCE="mover_ini" log "Data source: mover.ini (Mover Tuning plugin detected)" else DATA_SOURCE="du_polling" log "Data source: du polling (standard mover)" fi } # Parse mover.ini into INI_* globals read_mover_ini() { if [ ! -f "$MOVER_INI_PATH" ]; then log "Warning: mover.ini not found at $MOVER_INI_PATH" return 1 fi local key value # shellcheck disable=SC2034 while IFS='=' read -r key value; do # Remove surrounding quotes from value value="${value%\"}" value="${value#\"}" case "$key" in TotalToSecondary) INI_TOTAL_TO_SECONDARY="$value" ;; RemainToSecondary) INI_REMAIN_TO_SECONDARY="$value" ;; TotalFilesToSecondary) INI_TOTAL_FILES="$value" ;; RemainFilesToSecondary) INI_REMAIN_FILES="$value" ;; File) INI_CURRENT_FILE="$value" ;; Action) INI_ACTION="$value" ;; esac done < "$MOVER_INI_PATH" # Staleness check: warn if file hasn't been modified in >3x DU_POLL_INTERVAL local mod_time current_time age stale_threshold mod_time=$(stat -c %Y "$MOVER_INI_PATH" 2>/dev/null) || return 0 current_time=$(date +%s) age=$((current_time - mod_time)) stale_threshold=$((DU_POLL_INTERVAL * 3)) if [ "$age" -gt "$stale_threshold" ]; then if is_mover_running; then # Mover is alive — large file transfer likely; only log in debug mode if $ENABLE_DEBUG; then log "mover.ini unchanged for ${age}s — mover still running (likely processing a large file: ${INI_CURRENT_FILE##*/})" fi else log "Warning: mover.ini hasn't been updated in ${age}s (threshold: ${stale_threshold}s) and mover process not found — plugin may have stalled" fi fi return 0 } # Unified progress reader — sets PROGRESS_* globals from either data source get_progress() { if [ "$DATA_SOURCE" = "mover_ini" ]; then read_mover_ini || return 1 PROGRESS_TOTAL_BYTES="$INI_TOTAL_TO_SECONDARY" PROGRESS_REMAINING_BYTES="$INI_REMAIN_TO_SECONDARY" PROGRESS_MOVED_BYTES=$((INI_TOTAL_TO_SECONDARY - INI_REMAIN_TO_SECONDARY)) if [ "$PROGRESS_MOVED_BYTES" -lt 0 ]; then PROGRESS_MOVED_BYTES=0 fi if [ "$PROGRESS_TOTAL_BYTES" -gt 0 ]; then PROGRESS_PERCENT=$((PROGRESS_MOVED_BYTES * 100 / PROGRESS_TOTAL_BYTES)) if [ "$PROGRESS_PERCENT" -gt 99 ]; then PROGRESS_PERCENT=99 fi else PROGRESS_PERCENT=0 fi # Apply exclusion adjustment (uses initial snapshot — excluded dirs are static during mover run) if [ ${#exclude_paths[@]} -gt 0 ]; then PROGRESS_REMAINING_BYTES=$((PROGRESS_REMAINING_BYTES - initial_excluded_size)) if [ "$PROGRESS_REMAINING_BYTES" -lt 0 ]; then PROGRESS_REMAINING_BYTES=0 fi PROGRESS_TOTAL_BYTES=$((PROGRESS_TOTAL_BYTES - initial_excluded_size)) if [ "$PROGRESS_TOTAL_BYTES" -lt 0 ]; then PROGRESS_TOTAL_BYTES=0 fi PROGRESS_MOVED_BYTES=$((PROGRESS_TOTAL_BYTES - PROGRESS_REMAINING_BYTES)) if [ "$PROGRESS_MOVED_BYTES" -lt 0 ]; then PROGRESS_MOVED_BYTES=0 fi # Recalculate percent if [ "$PROGRESS_TOTAL_BYTES" -gt 0 ]; then PROGRESS_PERCENT=$((PROGRESS_MOVED_BYTES * 100 / PROGRESS_TOTAL_BYTES)) if [ "$PROGRESS_PERCENT" -gt 99 ]; then PROGRESS_PERCENT=99 fi else PROGRESS_PERCENT=0 fi fi PROGRESS_FILE_COUNT="$INI_TOTAL_FILES" PROGRESS_REMAIN_FILES="$INI_REMAIN_FILES" PROGRESS_CURRENT_FILE="$INI_CURRENT_FILE" else # du_polling mode — uses current_size and initial_size (set in main loop) local current_du current_du=$(du -sb "$CACHE_PATH" | cut -f1) if [ ${#exclude_paths[@]} -gt 0 ]; then current_du=$((current_du - initial_excluded_size)) if [ "$current_du" -lt 0 ]; then current_du=0 fi fi PROGRESS_REMAINING_BYTES="$current_du" PROGRESS_TOTAL_BYTES="$initial_size" PROGRESS_MOVED_BYTES=$((initial_size - current_du)) if [ "$PROGRESS_MOVED_BYTES" -lt 0 ]; then PROGRESS_MOVED_BYTES=0 fi if [ "$initial_size" -gt 0 ]; then PROGRESS_PERCENT=$((PROGRESS_MOVED_BYTES * 100 / initial_size)) if [ "$PROGRESS_PERCENT" -lt 0 ]; then PROGRESS_PERCENT=0 elif [ "$PROGRESS_PERCENT" -gt 99 ]; then PROGRESS_PERCENT=99 fi else PROGRESS_PERCENT=0 fi PROGRESS_FILE_COUNT="" PROGRESS_REMAIN_FILES="" PROGRESS_CURRENT_FILE="" fi return 0 } # Get mover PID from pid file or pgrep get_mover_pid() { local pid if [ -f "/var/run/mover.pid" ]; then pid=$(cat /var/run/mover.pid 2>/dev/null) if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then echo "$pid" return 0 fi fi # Fallback to pgrep pid=$(pgrep -x "mover" 2>/dev/null || pgrep -x "age_mover" 2>/dev/null || pgrep -f "^/usr/libexec/unraid/move" 2>/dev/null) if [ -n "$pid" ]; then echo "$pid" return 0 fi return 1 } # Get mover process start time as epoch seconds get_mover_start_time() { local pid=$1 if [ -z "$pid" ]; then return 1 fi # Use stat on /proc/ to get process start time (Linux-specific) local start_time start_time=$(stat -c %Y "/proc/${pid}" 2>/dev/null) if [ -n "$start_time" ]; then echo "$start_time" return 0 fi return 1 } # Create state directory if missing init_state_dir() { if [ ! -d "$STATE_DIR" ]; then mkdir -p "$STATE_DIR" if $ENABLE_DEBUG; then log "Created state directory: $STATE_DIR" fi fi } # Atomically write tracking state to state file save_state() { local tmp_file="${STATE_FILE}.tmp" cat > "$tmp_file" < "$LAST_RUN_FILE" <" elif [[ $platform == "telegram" ]]; then date -d "@${completion_time_estimate}" +"%H:%M on %b %d (%Z)" fi else echo "Calculating..." fi } send_notification() { local percent=$1 local remaining_data=$2 local datetime datetime=$(date +"%B %d (%Y) - %H:%M:%S") local etc_discord etc_discord=$(calculate_etc "$percent" "discord") local etc_telegram etc_telegram=$(calculate_etc "$percent" "telegram") # Prepare file info placeholders local file_count_str="" local current_file_str="" if $ENABLE_FILE_INFO && [ -n "$PROGRESS_FILE_COUNT" ] && [ "$PROGRESS_FILE_COUNT" != "0" ]; then local files_moved=$((PROGRESS_FILE_COUNT - ${PROGRESS_REMAIN_FILES:-0})) file_count_str="${files_moved}/${PROGRESS_FILE_COUNT} files" if [ -n "$PROGRESS_CURRENT_FILE" ]; then current_file_str="$PROGRESS_CURRENT_FILE" fi fi # Prepare the messages using the predefined templates local value_message_discord="${DISCORD_MOVING_MESSAGE//\{percent\}/$percent}" value_message_discord="${value_message_discord//\{remaining_data\}/$remaining_data}" value_message_discord="${value_message_discord//\{etc\}/$etc_discord}" value_message_discord="${value_message_discord//\{file_count\}/$file_count_str}" value_message_discord="${value_message_discord//\{current_file\}/$current_file_str}" local value_message_telegram="${TELEGRAM_MOVING_MESSAGE//\{percent\}/$percent}" value_message_telegram="${value_message_telegram//\{remaining_data\}/$remaining_data}" value_message_telegram="${value_message_telegram//\{etc\}/$etc_telegram}" value_message_telegram="${value_message_telegram//\{file_count\}/$file_count_str}" value_message_telegram="${value_message_telegram//\{current_file\}/$current_file_str}" local footer_text="Version: v${CURRENT_VERSION}" if [[ -n "${LATEST_VERSION}" && "${LATEST_VERSION}" != "${CURRENT_VERSION}" ]]; then footer_text+=" (update available)" fi value_message_telegram+=" ${footer_text}" # Determine the color based on completion and percentage local color if [ "$percent" -ge 100 ] || ! is_mover_running; then build_completion_summary value_message_discord="$COMPLETION_SUMMARY_DISCORD" value_message_telegram="$COMPLETION_SUMMARY_TELEGRAM" color=65280 # Green for completion else if [ "$percent" -le 34 ]; then color=16744576 # Light Red elif [ "$percent" -le 65 ]; then color=16753920 # Light Orange else color=9498256 # Light Green fi fi # Send the notifications log "Sending notification..." if $USE_TELEGRAM; then local json_payload json_payload=$(jq -n \ --arg chat_id "$TELEGRAM_CHAT_ID" \ --arg text "$value_message_telegram" \ '{chat_id: $chat_id, text: $text, disable_notification: "false", parse_mode: "HTML"}') if $ENABLE_DEBUG; then log "Preparing to send to Telegram: $json_payload" fi local response response=$(curl -s -H "Content-Type: application/json" -X POST -d "$json_payload" "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage") if $ENABLE_DEBUG; then log "Telegram response: $response" fi fi if $USE_DISCORD; then local notification_data='{ "username": "'"$DISCORD_NAME_OVERRIDE"'", "content": null, "embeds": [ { "title": "Mover: Moving Data", "color": '"$color"', "fields": [ { "name": "'"$datetime"'", "value": "'"${value_message_discord}"'" } ], "footer": { "text": "'"$footer_text"'" } } ] }' if $ENABLE_DEBUG; then log "Preparing to send to Discord: $notification_data" fi local response response=$(curl -s -H "Content-Type: application/json" -X POST -d "$notification_data" "$DISCORD_WEBHOOK_URL" -w "\nHTTP status: %{http_code}\nCurl Error: %{errormsg}") if $ENABLE_DEBUG; then log "Discord response: $response" fi fi } # Initialize state directory for crash recovery init_state_dir # Main Script Execution Loop while true; do log "Monitoring new mover process..." # Wait for the mover process to start log "Mover process not found, waiting to start monitoring..." while ! is_mover_running; do sleep 10 done log "Mover process found, starting monitoring..." # Detect data source (mover.ini vs du polling) detect_data_source # Get mover PID for state tracking mover_pid=$(get_mover_pid) || mover_pid="" mover_start_time=$(get_mover_start_time "$mover_pid") || mover_start_time="" # Capture initial excluded size (used for consistent total adjustment) initial_excluded_size=0 if [ ${#exclude_paths[@]} -gt 0 ]; then initial_excluded_size=$(get_excluded_size) if [ "$initial_excluded_size" -gt 0 ]; then log "Excluding $(human_readable "$initial_excluded_size") from ${#exclude_paths[@]} path(s)" fi fi # Try to resume from saved state (crash recovery) if load_state; then log "Resuming monitoring from saved state — skipping 0% notification" # Get fresh progress data get_progress remaining_readable=$(human_readable "$PROGRESS_REMAINING_BYTES") percent="$PROGRESS_PERCENT" else # Fresh start — determine initial size if [ "$DATA_SOURCE" = "mover_ini" ]; then get_progress initial_size="$PROGRESS_TOTAL_BYTES" else initial_size=$(du -sb "$CACHE_PATH" | cut -f1) initial_size=$((initial_size - initial_excluded_size)) if [ "$initial_size" -lt 0 ]; then initial_size=0 fi fi initial_readable=$(human_readable "$initial_size") log "Initial total size of data: $initial_readable" start_time=$(date +%s) log "Monitoring started at: $(date -d "@$start_time" '+%Y-%m-%d %H:%M:%S')" # Check for late-join (mover already running before script started) if [ "$DATA_SOURCE" = "mover_ini" ] && [ "$PROGRESS_MOVED_BYTES" -gt 0 ]; then monitoring_start_bytes="$PROGRESS_MOVED_BYTES" log "Late join detected — mover already $(( PROGRESS_PERCENT ))% complete (using mover.ini data, baseline: $(human_readable "$monitoring_start_bytes"))" percent="$PROGRESS_PERCENT" remaining_readable=$(human_readable "$PROGRESS_REMAINING_BYTES") elif [ "$DATA_SOURCE" = "du_polling" ] && [ -n "$mover_start_time" ]; then monitoring_start_bytes=0 script_time=$(date +%s) if [ $((script_time - mover_start_time)) -gt 60 ]; then log "Late join detected — progress relative to cache size at script start" fi percent=0 remaining_readable="$initial_readable" else monitoring_start_bytes=0 percent=0 remaining_readable="$initial_readable" fi LAST_NOTIFIED=-1 send_notification "$percent" "$remaining_readable" LAST_NOTIFIED=$((percent / NOTIFICATION_INCREMENT * NOTIFICATION_INCREMENT)) log "Initial notification sent with ${percent}% completion." fi # Monitor the progress last_du_time=0 while true; do current_time=$(date +%s) # Only recalculate progress when DU_POLL_INTERVAL has passed if [ $((current_time - last_du_time)) -ge "$DU_POLL_INTERVAL" ]; then get_progress remaining_readable=$(human_readable "$PROGRESS_REMAINING_BYTES") percent="$PROGRESS_PERCENT" last_du_time=$current_time # Save state for crash recovery save_state if $ENABLE_DEBUG; then log "Progress poll [${DATA_SOURCE}]: percent=$percent, moved=${PROGRESS_MOVED_BYTES}, remaining=${PROGRESS_REMAINING_BYTES}, total=${PROGRESS_TOTAL_BYTES}" if [ -n "$PROGRESS_FILE_COUNT" ] && [ "$PROGRESS_FILE_COUNT" != "0" ]; then log " Files: ${PROGRESS_REMAIN_FILES}/${PROGRESS_FILE_COUNT} remaining" fi fi fi # Check if the mover process is still running if ! is_mover_running; then log "Mover process is no longer running." # Final progress read — captures mover.ini final state before it goes stale get_progress remaining_readable=$(human_readable "$PROGRESS_REMAINING_BYTES") log "Total data moved: ${PROGRESS_MOVED_BYTES} bytes, Total: ${PROGRESS_TOTAL_BYTES} bytes." send_notification 100 "$remaining_readable" save_last_run LAST_NOTIFIED=-1 log "Final notification sent and monitoring loop exiting." break fi # Send notifications based on increment if [ "$((percent / NOTIFICATION_INCREMENT * NOTIFICATION_INCREMENT))" -ge $((LAST_NOTIFIED + NOTIFICATION_INCREMENT)) ]; then log "Condition met for sending update: Current percent $percent (rounded down to nearest increment: $((percent / NOTIFICATION_INCREMENT * NOTIFICATION_INCREMENT))) >= Last notified $LAST_NOTIFIED + Increment $NOTIFICATION_INCREMENT" send_notification "$percent" "$remaining_readable" LAST_NOTIFIED="$((percent / NOTIFICATION_INCREMENT * NOTIFICATION_INCREMENT))" log "Notification sent for $percent% completion." fi sleep 5 # Check every 5 seconds; progress only recalculated per DU_POLL_INTERVAL done # Delay before restarting monitoring log "Restarting monitoring after completion..." sleep 10 done # Mover Status Script # # This script monitors the progress of the "Mover" process and posts updates to a Discord/Telegram webhook. # Copyright (C) 2024 - engels74 # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # # Contact: engels74@tuta.io