May 30May 30 I added my scripts to this FR (works on 7.3):https://product.unraid.net/p/optimize-boot-backup-by-disabling-compression-or-adding-a-toggleQuite easy to add notifications if you'd wan't that. Edited May 30May 30 by Niklas
June 26Jun 26 The expanded and corrected versions for V7.3.x and the changes to ‘boot’, ‘Internal Boot’, … instead of ‘flash’. Older operating system versions should also work, but have not really been tested as there are currently no older versions, such as V6.x available.The enhanced, extended and improved Script with more Documentation:New Script#!/bin/bash ############################################################################# # Title: Flash Boot Backup License Boot Analysis # Description: Automates Flash or Boot Backup Creation, Retention, License and Boot Analysis # # Variable Naming Convention: # -------------------------------------- # - GLOBAL VARIABLES: UPPER_SNAKE_CASE (e.g., HOST_NAME, BACKUP_RETENTION_DAYS) # - LOCAL VARIABLES (within modules): UPPER_SNAKE_CASE (e.g., DISK_COUNT, CURRENT_TIME) # - FUNCTIONS: UPPER_SNAKE_CASE (e.g., PRINT_MSG(), LOG_TO_FILE()) # - UI DISPLAY TEXTS: Title Case (e.g., "USB Stick", "Backup File", "TPM Mode") # # The readonly Keyword Explained: # -------------------------------- # "readonly" makes a variable truly immutable (cannot be changed after assignment). # This protects critical system paths from accidental modification during script execution. # Example: # NOTIFICATION_SCRIPT="/usr/local/emhttp/webGui/scripts/notify" # readonly NOTIFICATION_SCRIPT # # Cannot later execute: NOTIFICATION_SCRIPT="/wrong/path" (will fail with error) # # Benefits of readonly: # - Prevents bugs from accidental variable reassignment # - Documents intent clearly for other developers # - Helps with security (sensitive paths cannot be hijacked) # ############################################################################# # ============================================================================= # 1. CONFIGURATION & IDENTIFIERS # ============================================================================= # ----------------------------------------------------------------------------- # GENERAL SETTINGS (USER-CUSTOMIZABLE) # Configurable Values for Script Operation. User adjustments permitted. # readonly keyword omitted to allow user overrides via Environment or File edit. # ----------------------------------------------------------------------------- SCRIPT_TITLE="Flash Boot Backup License Boot Analysis" HOST_NAME=$(hostname) MIN_DISKS=3 # Minimum Disks required in Array # ----------------------------------------------------------------------------- # RETENTION POLICIES (USER-CUSTOMIZABLE) # Controls Backup and Log File Retention Duration and Quantity Limits. # Age-based deletion executes first, followed by count-based Enforcement. # readonly keyword omitted to allow user Adjustments. # ----------------------------------------------------------------------------- BACKUP_RETENTION_DAYS=150 # Backup Files kept for specified Days MAX_BACKUPS_TO_KEEP=15 # Maximum Backup Files retained LOG_RETENTION_DAYS=90 # Log Files kept for specified Days MAX_LOG_FILES_TO_KEEP=30 # Maximum Log Files retained # ----------------------------------------------------------------------------- # FEATURE FLAGS (USER-CUSTOMIZABLE - ENABLE/DISABLE) # Controls execution of Individual Modules. Accepts TRUE or FALSE. # readonly keyword omitted to allow user overrides. # ----------------------------------------------------------------------------- ENABLE_LICENSE_CHECK=TRUE # Check License Status (TPM/USB) ENABLE_BOOT_CHECK=TRUE # Analyze Boot Medium Type ENABLE_HARDWARE_CHECK=TRUE # Verify TPM/USB Hardware availability ENABLE_SYSTEM_CHECK=TRUE # Check Array Disks and Stale Files ENABLE_BACKUP_CREATE=TRUE # Create New Backup File ENABLE_BACKUP_RETENTION=TRUE # Apply Backup Retention Policy ENABLE_LOG_RETENTION=TRUE # Apply Log Retention Policy ENABLE_NOTIFICATION=TRUE # Send System Notification SHOW_CLI_OUTPUT=TRUE # Display progress in Console/Terminal # ----------------------------------------------------------------------------- # DIRTY CHECK OVERRIDE (USER-CUSTOMIZABLE) # Controls behavior when Stale Temporary Files are detected. # TRUE permits Continuation with Warning, FALSE triggers Abort. # Default: FALSE (abort on Stale Files). # ----------------------------------------------------------------------------- SKIP_DIRTY_CHECK=FALSE # ----------------------------------------------------------------------------- # AUTO CLEANUP SETTINGS (USER-CUSTOMIZABLE) # Controls automatic deletion of Stale Temporary Files at Script end. # When FALSE (default), Stale Files trigger Abort or Warning (see SKIP_DIRTY_CHECK). # When TRUE, Stale Files are automatically deleted AFTER all other Modules complete. # ----------------------------------------------------------------------------- ENABLE_AUTO_CLEANUP=FALSE # ----------------------------------------------------------------------------- # FILE NAMING CONVENTIONS (USER-CUSTOMIZABLE) # Defines Log File Base Name and glob Pattern for Retention Matching. # ----------------------------------------------------------------------------- LOG_BASENAME="Boot_Backup_Analysis" LOG_FILE_PATTERN="${LOG_BASENAME}_*.log" # ----------------------------------------------------------------------------- # NOTIFICATION SETTINGS (USER-CUSTOMIZABLE) # Defines Title and Subject for Error Notifications via Unraid Notify Script. # ----------------------------------------------------------------------------- NOTIFY_ERROR_TITLE="Flash Backup Aborted" NOTIFY_ERROR_SUBJECT="Flash or Boot Backup - Error" # ----------------------------------------------------------------------------- # USB BRAND PATTERNS (USER-CUSTOMIZABLE) # Pipe-separated list of USB Brand Names for License Device Detection. # Extended Lists accommodate additional USB Manufacturers. # ----------------------------------------------------------------------------- USB_BRAND_PATTERN='ADATA|Corsair|Crucial|G.Skill|HP|Imation|Integral|Kingston|Lexar|Netac|PNY|Samsung|SanDisk|Silicon Power|Team Group|Toshiba|Transcend|Verbatim' # ----------------------------------------------------------------------------- # MODULE TITLES (STATIC - NEVER MODIFIED AT RUNTIME) # Fixed Labels for CLI Section Headers and Log Entries. # readonly applied: Assigned once during initialization, No runtime modification. # ----------------------------------------------------------------------------- readonly MOD_TITLE_1="LICENSE STATUS" readonly MOD_TITLE_2="BOOT MEDIUM ANALYSIS" readonly MOD_TITLE_3="HARDWARE CHECK" readonly MOD_TITLE_4="SYSTEM INTEGRITY CHECK" readonly MOD_TITLE_5="CREATE BACKUP" readonly MOD_TITLE_6="APPLY BACKUP RETENTION" readonly MOD_TITLE_7="APPLY LOG RETENTION" readonly MOD_TITLE_8="AUTO CLEANUP" readonly MOD_TITLE_9="SEND NOTIFICATION" # ============================================================================= # 2. SYSTEM CONSTANTS # ============================================================================= # ----------------------------------------------------------------------------- # SYSTEM PATHS (UNRAID-SPECIFIC - FIXED) # Operating System-defined Paths. Immutable during Script execution. # readonly applied: Prevents accidental or malicious Path Modification. # ----------------------------------------------------------------------------- readonly NOTIFICATION_SCRIPT="/usr/local/emhttp/webGui/scripts/notify" readonly VAR_INI_PATH="/var/local/emhttp/var.ini" # ----------------------------------------------------------------------------- # DIRECTORY PATHS (DERIVED - AUTO-COMPUTED) # Computed at Script Startup from HOST_NAME and configurable Values above. # Represents Physical Directory Structure on Target System. # readonly applied: Calculated once, Never modified Subsequently. # Dependency order Preserved: Derived Paths Reference earlier Variables. # ----------------------------------------------------------------------------- readonly BACKUP_BASE_DIR="/mnt/user/Backup_${HOST_NAME}/boot" readonly ARCHIVE_SUBDIR="Backup_Reports" readonly LOG_LOCAL_SUBDIR="Backup_Logs_on_Boot" readonly ARCHIVE_LOG_DIR="${BACKUP_BASE_DIR}/${ARCHIVE_SUBDIR}" readonly LOCAL_LOG_DIR="/boot/${LOG_LOCAL_SUBDIR}" # ----------------------------------------------------------------------------- # LOG FILE PATHS (DERIVED - AUTO-COMPUTED PER EXECUTION) # Unique per run via embedded Timestamp in Filename. Prevents File Collisions. # LOG_FILE_PATH: Active Session Log (Write Target) # ARCHIVE_LOG_PATH: Permanent Archive Copy (Final Destination) # readonly applied: Calculated once, Never modified Subsequently. # ----------------------------------------------------------------------------- readonly LOG_FILE_NAME="${LOG_BASENAME}_${HOST_NAME}_$(date '+%Y-%m-%d_%H-%M-%S').log" readonly LOCAL_LOG_PATH="${LOCAL_LOG_DIR}/${LOG_FILE_NAME}" readonly ARCHIVE_LOG_PATH="${ARCHIVE_LOG_DIR}/${LOG_FILE_NAME}" readonly LAST_RUN_LOG="${ARCHIVE_LOG_DIR}/Last_Run.log" readonly LOG_FILE_PATH="$LOCAL_LOG_PATH" # ============================================================================= # 3. GLOBAL VARIABLES (RUNTIME STATE) # ============================================================================= # NOTE: Variables below remain mutable (readonly NOT Applied). # Purpose: Carry Initial/Default Values that are overwritten during Module execution. # Applying readonly would cause Runtime Failure (Variable Assignment Rejection). # Pattern: Initialize → Module Updates → Final State persisted # ----------------------------------------------------------------------------- # LICENSE & BOOT STATE (INITIAL VALUES - MODULES 1-3 POPULATE) # Neutral Default Values updated upon detection of actual System Configuration. # Transition: Unknown/Empty → Detected Value (USB Flash, TPM, Migration, Invalid). # ----------------------------------------------------------------------------- LICENSE_TYPE="" # Initial: empty → Set by Module 1 LICENSE_STATUS="Unknown" # Initial: Unknown → Set by Module 1 DEVICE_BINDING="None" # Initial: None → Set by Module 1 BOOT_MEDIUM_TYPE="Unknown" # Initial: Unknown → Set by Module 2 # ----------------------------------------------------------------------------- # SYSTEM STATE (INITIAL VALUES - MODULE 4 POPULATES) # Stale File detection initialized clean. Updated during Module 4 execution. # HAS_STALE_FILES: FALSE → TRUE if Stale Files discovered # SKIP_DIRTY_CHECK_ACTIVE: FALSE → TRUE if Bypass activated # ----------------------------------------------------------------------------- HAS_STALE_FILES=FALSE # Initial: FALSE → TRUE by Module 4 if Stale Files found SKIP_DIRTY_CHECK_ACTIVE=FALSE # Initial: FALSE → TRUE by Module 4 if Bypass activated # ----------------------------------------------------------------------------- # BACKUP STATE (INITIAL VALUES - MODULE 5 POPULATES) # Empty until Module 5 Creates Backup and populates with generated Values. # ----------------------------------------------------------------------------- BACKUP_FILE="" # Initial: empty → Set by Module 5 DEST_FILE="" # Initial: empty → Set by Module 5 # ----------------------------------------------------------------------------- # CLEANUP STATE (INITIAL VALUES - MODULE 8 POPULATES) # Tracks automatic cleanup execution and outcomes. # Auto-computed by Module 8 based on actual file operations performed. # ----------------------------------------------------------------------------- CLEANUP_EXECUTED=FALSE # Initial: FALSE → TRUE by Module 8 if cleanup runs CLEANUP_SUCCESS=0 # Initial: 0 → Files by Module 8 successfully removed CLEANUP_FAILED=0 # Initial: 0 → Files by Module 8 that failed removal CLEANUP_TOTAL=0 # Initial: 0 → Total files by Module 8 processed # ----------------------------------------------------------------------------- # OVERALL STATUS TRACKING (INITIAL VALUE - ALL MODULES UPDATE) # Cumulative Result Tracker across all executed Modules. # Escalation Priority: ERROR > WARNING > SUCCESS (Monotonic Escalation only). # Modules may increase Severity; Deescalation not supported. # Final Value determines completion Message and Notification Priority. # ----------------------------------------------------------------------------- OVERALL_STATUS="SUCCESS" # Initial: SUCCESS → WARNING or ERROR by any Module # ============================================================================= # 4. MESSAGE TEXTS (REUSABLE CLI MESSAGES) # ============================================================================= # ----------------------------------------------------------------------------- # LEVEL LABELS (STATIC - FIXED STRINGS) # Visual indicators for CLI Output and Log Entries. # readonly applied: Never modified after initialization. # ----------------------------------------------------------------------------- readonly MSG_SUCCESS="[SUCCESS]" readonly MSG_ERROR="[ERROR]" readonly MSG_WARNING="[WARNING]" readonly MSG_INFO="[INFO]" # ----------------------------------------------------------------------------- # REUSABLE ERROR/WARNING MESSAGES (STATIC - FIXED STRINGS) # Predefined Messages used at Multiple Points throughout Script execution. # readonly applied: Never modified after initialization. # Centralization ensures consistent Wording across all Modules. # Single-Point modification enables future text adjustments. # ----------------------------------------------------------------------------- readonly MSG_STALE_FILES_FOUND="Stale Temporary Files found." readonly MSG_NO_STALE_FILES="No Stale Temporary Files found." readonly MSG_ARRAY_INTEGRITY_CONFIRMED="Array Integrity confirmed." readonly MSG_NO_USB_STICK="No USB Stick detected, but Flash License active." readonly MSG_USB_STICK_DETECTED="USB Stick detected for License." readonly MSG_TPM_MODE="TPM Mode: No USB Stick required for License." readonly MSG_CONFIG_FILE_NOT_FOUND="Configuration File not found." readonly MSG_VARINI_NOT_FOUND="var.ini not found." readonly MSG_NO_TPM_DEVICE="No TPM Device available." readonly MSG_UNZIP_NOT_FOUND="'unzip' Command not found. Integrity Check impossible." readonly MSG_FLASH_BACKUP_FAILED="Flash Backup Tool failed." readonly MSG_INVALID_FILENAME="Invalid Filename generated." readonly MSG_SOURCE_FILE_MISSING="Source File does not exist." readonly MSG_COPY_FAILED="Copying Backup File failed." readonly MSG_BACKUP_CORRUPTED="Backup File corrupted! Removing defective File." readonly MSG_CHANGE_DIR_FAILED="Change to Directory failed." readonly MSG_PROCESS_ABORTED="Process aborted: Manual cleanup required." readonly MSG_WARNING_IGNORED="Warning ignored." readonly MSG_CLEANUP_STARTED="Auto cleanup initiated." readonly MSG_CLEANUP_FAILED="Cleanup failed. No Files removed. Manual intervention required." readonly MSG_CLEANUP_SKIPPED="Auto cleanup skipped (Disabled or no Stale Files)." # ============================================================================= # 5. HELPER FUNCTIONS # ============================================================================= # ----------------------------------------------------------------------------- # PRINT HEADER BANNER (CLI OUTPUT - STARTUP) # Displays Script Title, Host Name, and Timestamp at Initialization. # Single Invocation before any Module Execution. # Visibility Controlled by SHOW_CLI_OUTPUT Feature Flag. # ----------------------------------------------------------------------------- PRINT_HEADER() { echo "==============================================================" echo " ${SCRIPT_TITLE}" echo "==============================================================" echo "HOST: $(hostname)" echo "TIMESTAMP: $(date '+%d.%m.%Y %H:%M')" echo "----------------------------------------" } # ----------------------------------------------------------------------------- # PRINT SECTION HEADER (CLI OUTPUT - BETWEEN MODULES) # Displays formatted Section Title between Module Blocks. # Parameter: $1 = Section Title (from MOD_TITLE_* Variables) # Visibility Controlled by SHOW_CLI_OUTPUT Feature Flag. # ----------------------------------------------------------------------------- PRINT_SECTION() { echo "" echo "[${1}]" echo "--------------------------------------------------------------" } # ----------------------------------------------------------------------------- # PRINT MESSAGE WITH LEVEL PREFIX (CLI OUTPUT - UNIFIED) # Centralized Message formatting for consistent CLI presentation. # Parameters: $1 = Level (SUCCESS/ERROR/WARNING/INFO), $2 = Message Text # Constructs Formatted Output String in Local Variable OUTPUT. # stdbuf Applies Line-buffered Terminal Display when SHOW_CLI_OUTPUT=TRUE. # ----------------------------------------------------------------------------- PRINT_MSG() { local LEVEL="$1" local MSG="$2" local OUTPUT="" case "$LEVEL" in SUCCESS) OUTPUT="${MSG_SUCCESS} ${MSG}" ;; ERROR) OUTPUT="${MSG_ERROR} ${MSG}" ;; WARNING) OUTPUT="${MSG_WARNING} ${MSG}" ;; INFO) OUTPUT="${MSG_INFO} ${MSG}" ;; *) OUTPUT="$MSG" ;; esac # Line-buffered Output for immediate Display stdbuf -oL echo "$OUTPUT" } # ----------------------------------------------------------------------------- # WRITE TIMESTAMPED ENTRY TO LOG FILE (LOGGING - UNIFIED) # Appends Single Log Entry with Timestamp and Host Name. # Parameter: $1 = Message Text # Destination: LOG_FILE_PATH (Current Session Log) # Invocation: All Modules (Independent of SHOW_CLI_OUTPUT Setting). # ----------------------------------------------------------------------------- LOG_TO_FILE() { local MSG="$1" local TIMESTAMPED_MSG="[$(date '+%Y-%m-%d %H:%M:%S')] [$(hostname)] $MSG" echo "$TIMESTAMPED_MSG" >> "$LOG_FILE_PATH" } # ----------------------------------------------------------------------------- # MASK GUID FORMAT (SENSITIVE DATA OBSCURATION) # Preserves GUID Structure while obscuring sensitive Content. # # UNRAID GUID STRUCTURE (Uppercase Alphanumeric: 0-9, A-Z only): # # - TPM GUIDs: Starts with "01-", 2 Segments (1 Dash) # Example: "01-ABC23D123456GHIJ7890KLMN" # → Masked: "01-ABC23*******************" # Recognition: "01-" Prefix, first 5 Chars of Segment 2 visible. # # - Flash GUIDs: 4 Segments (3 Dashes) # Example: "0789-1012-3456-789AABE12345" # → Masked: "0789-10**-34**-789AB*******" # Recognition: All 4 Segments visible, specific show/mask per Segment. # # MASKING LOGIC: # - TPM: Show "01-" + first 5 Chars of Segment 2 + rest as **** # - Flash Seg 1: Show all if ≤4 Chars, else first 4 + **** # - Flash Seg 2: Show first 2 Chars + **** (Pad to 4 total) # - Flash Seg 3: Show first 2 Chars + **** (Pad to 4 total) # - Flash Seg 4: Show first 5 Chars + rest as **** # # PARAMETER: $1 = Original GUID String (expected Uppercase Alphanumeric) # RETURN: Masked GUID String preserving original Format Structure # ----------------------------------------------------------------------------- MASK_GUID_FORMAT() { local GUID="$1" if [[ -z "$GUID" ]]; then echo "---"; return; fi # TPM GUID if [[ "$GUID" == 01-* ]]; then local TPM_PART="${GUID#01-}" local PART_LEN=${#TPM_PART} if [[ ${PART_LEN} -gt 5 ]]; then local SHOW_CHARS="${TPM_PART:0:5}" local MASK_LEN=$((PART_LEN - 5)) echo "01-${SHOW_CHARS}$(printf '*%.0s' $(seq 1 ${MASK_LEN}))" elif [[ ${PART_LEN} -gt 0 ]]; then echo "01-${TPM_PART}****" else echo "01-****" fi return fi # Flash GUID local -a SEGMENTS IFS='-' read -ra SEGMENTS <<< "$GUID" if [[ ${#SEGMENTS[@]} -ge 4 ]]; then local S1="${SEGMENTS[0]}" local S2="${SEGMENTS[1]}" local S3="${SEGMENTS[2]}" local S4="${SEGMENTS[3]}" # Segment 1: Show all if ≤4 Chars, else first 4 + **** local S1_LEN=${#S1} local MASKED_S1 if [[ ${S1_LEN} -le 4 ]]; then MASKED_S1="${S1}" else MASKED_S1="${S1:0:4}$(printf '*%.0s' $(seq 1 $((S1_LEN - 4))))" fi # Segment 2: Show first 2 Chars + ** padding local S2_LEN=${#S2} local MASKED_S2 if [[ ${S2_LEN} -ge 2 ]]; then MASKED_S2="${S2:0:2}**" elif [[ ${S2_LEN} -eq 1 ]]; then MASKED_S2="${S2}***" else MASKED_S2="****" fi # Segment 3: Show first 2 Chars + ** padding local S3_LEN=${#S3} local MASKED_S3 if [[ ${S3_LEN} -ge 2 ]]; then MASKED_S3="${S3:0:2}**" elif [[ ${S3_LEN} -eq 1 ]]; then MASKED_S3="${S3}***" else MASKED_S3="****" fi # Segment 4: Show first 5 Chars + rest as **** local S4_LEN=${#S4} local MASKED_S4 if [[ ${S4_LEN} -gt 5 ]]; then MASKED_S4="${S4:0:5}$(printf '*%.0s' $(seq 1 $((S4_LEN - 5))))" elif [[ ${S4_LEN} -gt 0 ]]; then MASKED_S4="${S4}****" else MASKED_S4="****" fi echo "${MASKED_S1}-${MASKED_S2}-${MASKED_S3}-${MASKED_S4}" elif [[ ${#SEGMENTS[@]} -ge 2 ]]; then # 2+ Segments but not 4 — generic fallback local FIRST="${SEGMENTS[0]}" local SECOND="${SEGMENTS[1]}" if [[ ${#SECOND} -gt 5 ]]; then echo "${FIRST}-${SECOND:0:5}$(printf '*%.0s' $(seq 1 $(${#SECOND} - 5)))" elif [[ ${#SECOND} -gt 0 ]]; then echo "${FIRST}-${SECOND}****" else echo "${FIRST}-****" fi else # Single Segment if [[ ${#GUID} -gt 5 ]]; then echo "${GUID:0:5}$(printf '*%.0s' $(seq 1 $(${#GUID} - 5)))" elif [[ ${#GUID} -gt 0 ]]; then echo "${GUID}****" else echo "****" fi fi } # ----------------------------------------------------------------------------- # CLEAN VENDOR NAME (DEVICE STRING NORMALIZATION) # Extracts Primary Vendor Name from USB Device String. # Removes Common Model Suffixes (Cruzer, DataTraveler, Flash). # Input Example: "SanDisk Cruzer Blade" → "SanDisk" # Parameter: $1 = Raw Vendor String from lsusb Output # Return: Cleaned Vendor Name Only # ----------------------------------------------------------------------------- CLEAN_VENDOR_NAME() { local RAW_VENDOR="$1" local VENDOR=$(echo "$RAW_VENDOR" | awk '{print $1}') if [[ "$VENDOR" == *"Cruzer"* ]]; then VENDOR=$(echo "$VENDOR" | sed 's/Cruzer$//g') elif [[ "$VENDOR" == *"DataTraveler"* ]]; then VENDOR=$(echo "$VENDOR" | sed 's/DataTraveler$//g') elif [[ "$VENDOR" == *"Flash"* ]]; then VENDOR=$(echo "$VENDOR" | sed 's/Flash$//g') fi echo "$VENDOR" | xargs } # ----------------------------------------------------------------------------- # CLEAN MODEL NAME (DEVICE STRING NORMALIZATION) # Strips Storage Capacity Suffixes from Device Model Strings. # Matches and Removes: GB, TB, MB, GiB, MiB with Numeric Prefixes. # Input Example: "WD Red SN700 1TB" → "WD Red SN700" # Parameter: $1 = Raw Model String # Return: Cleaned Model Name without Capacity Information # ----------------------------------------------------------------------------- CLEAN_MODEL_NAME() { local RAW_MODEL="$1" local CLEANED=$(echo "$RAW_MODEL" | sed -E 's/[[:space:]]*[0-9]+\.?[0-9]*(GB|TB|MB|GiB|MiB)[[:space:]]*$//gi') if [[ -z "$CLEANED" ]]; then echo "$RAW_MODEL" else echo "$CLEANED" fi } # ----------------------------------------------------------------------------- # SEND ERROR NOTIFICATION (CRITICAL ERROR HANDLER) # Triggers Unraid System Notification for Critical Errors. # Parameters: $1 = Title (Optional, Defaults to NOTIFY_ERROR_TITLE) # $2 = Subject (Optional, Defaults to NOTIFY_ERROR_SUBJECT) # $3 = Message (Required) # Hardcoded "-i error" Icon for Unraid Notify Script Compatibility. # ----------------------------------------------------------------------------- SEND_ERROR_NOTIFICATION() { local TITLE="${1:-${NOTIFY_ERROR_TITLE}}" local SUBJECT="${2:-${NOTIFY_ERROR_SUBJECT}}" local MESSAGE="$3" "$NOTIFICATION_SCRIPT" \ -e "$TITLE" \ -s "$SUBJECT" \ -d "$MESSAGE" \ -i "error" } # ----------------------------------------------------------------------------- # HANDLE CRITICAL ERROR AND EXIT (FATAL CONDITION HANDLER) # Centralized Error Handler for Fatal Conditions in Module 5. # Parameters: $1 = Log Message (Written to Log File) # $2 = Notification Message (Sent via System Alert) # Sequential Actions: Log Entry → Notification Send → Exit with Code 1. # Exclusive Use by Module 5 for Backup Creation Failures. # ----------------------------------------------------------------------------- HANDLE_ERROR_EXIT() { local LOG_MSG="$1" local NOTIFY_MSG="$2" local LOG_PATH="$LOG_FILE_PATH" LOG_TO_FILE "${MSG_ERROR} ${LOG_MSG}" SEND_ERROR_NOTIFICATION "" "" "${NOTIFY_MSG}" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "ERROR" "${MSG_ERROR} ${LOG_MSG}" echo "────────────────────────────────────────────────────────────" echo "→ For detailed Error Information, check Log File:" echo " $LOG_PATH" echo "────────────────────────────────────────────────────────────" fi exit 1 } # ============================================================================= # 6. INITIALIZATION # ============================================================================= # Create Required Directories # Precedence: Created before any Module execution. # Failure Condition: Script Termination with Error Notification. # mkdir -p Behavior: Parent Directories Created Automatically as needed. mkdir -p "$BACKUP_BASE_DIR" || { SEND_ERROR_NOTIFICATION "" "" "Cannot create Backup Directory." exit 1 } mkdir -p "${ARCHIVE_LOG_DIR}" || { SEND_ERROR_NOTIFICATION "" "" "Cannot create Archive Log Directory." exit 1 } mkdir -p "${LOCAL_LOG_DIR}" || { SEND_ERROR_NOTIFICATION "" "" "Cannot create Local Log Directory." exit 1 } # Create Log Files # Command: touch Creates Empty Files if Non-existent. # Failure Condition: Script Termination with Error Notification. touch "$LOCAL_LOG_PATH" 2>/dev/null || { SEND_ERROR_NOTIFICATION "" "" "Cannot create Local Log File." exit 1 } touch "$ARCHIVE_LOG_PATH" 2>/dev/null || { SEND_ERROR_NOTIFICATION "" "" "Cannot create Archive Log File." exit 1 } # Write Initial Log Entry # Mechanism: Block-Redirect Overwrites Log Files with Startup Metadata. # Purpose: Establishes Baseline for Session Tracking. { echo "==========================================" echo "START ${SCRIPT_TITLE}" echo "TIMESTAMP: $(date '+%d.%m.%Y %H:%M')" echo "HOSTNAME: ${HOST_NAME}" echo "BACKUP TARGET: ${BACKUP_BASE_DIR}" echo "LOCAL LOG: ${LOCAL_LOG_PATH}" echo "ARCHIVE LOG: ${ARCHIVE_LOG_PATH}" echo "LAST RUN LOG: ${LAST_RUN_LOG}" echo "==========================================" } > "$LOG_FILE_PATH" # Print CLI Header (Conditional) # Visibility Controlled by SHOW_CLI_OUTPUT Feature Flag. if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_HEADER fi # ============================================================================= # 7. MODULES # ============================================================================= # Execution Model: 9 Modular Blocks Process Distinct Tasks Sequentially. # Conditional Execution: Each Module Respects Corresponding ENABLE_* Feature Flag. # Status Accumulation: OVERALL_STATUS Collects Warnings and Errors Across Modules. # Final Determination: Overall Status Sets Completion Message and Notification Priority. # Functional Categorization: # - Modules 1-4: Validation and Analysis (Pre-Backup Checks) # - Modules 5-7: Core Operations (Backup + Retention) # - Module 8: Auto Cleanup (Deferred Stale File removal) # - Module 9: Send Notification (Final Report with complete Data) # Execution Characteristics: # - Conditional Execution Based on ENABLE_* Flags # - Sequential Processing (Order Preserved as Listed) # - State Propagation via Global Variables # - Error Handling via Centralized Handlers # - Log Recording Independent of CLI Output Setting # - Status Escalation Preserves Highest Severity (ERROR > WARNING > SUCCESS) # --- MODULE 1: LICENSE STATUS ----------------------------------------------- if [ "$ENABLE_LICENSE_CHECK" = "TRUE" ]; then LOG_TO_FILE "---[MODULE 1: ${MOD_TITLE_1}]---" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_SECTION "${MOD_TITLE_1}" fi # Local Module Variables LOCAL_SERVER_NAME="" LOCAL_OS_VERSION="" LOCAL_LICENSE_TYPE="" LOCAL_FLASH_GUID="" LOCAL_TPM_GUID="" LOCAL_REG_GUID="" # Parse var.ini Configuration File if [ -f "$VAR_INI_PATH" ]; then LOCAL_SERVER_NAME=$(awk -F'=' '/^NAME=/ {gsub(/"/, "", $2); gsub(/ /, "_", $2); print $2}' "$VAR_INI_PATH") LOCAL_OS_VERSION=$(awk -F'=' '/^version=/ {gsub(/"/, "", $2); print $2}' "$VAR_INI_PATH") LOCAL_LICENSE_TYPE=$(awk -F'=' '/^regTy=/ {gsub(/"/, "", $2); print $2}' "$VAR_INI_PATH" 2>&1) || true if [[ "$LOCAL_LICENSE_TYPE" == *"error"* ]] || [[ "$LOCAL_LICENSE_TYPE" == *"cannot"* ]]; then LOG_TO_FILE "${MSG_WARNING} awk for regTy reported issues: $LOCAL_LICENSE_TYPE" LOCAL_LICENSE_TYPE="" fi LOCAL_FLASH_GUID=$(grep '^flashGUID=' "$VAR_INI_PATH" 2>&1 | cut -d'"' -f2 | tr -d '[:space:]') || true if [[ "$LOCAL_FLASH_GUID" == *"error"* ]] || [[ "$LOCAL_FLASH_GUID" == *"No such"* ]]; then LOG_TO_FILE "${MSG_WARNING} grep for flashGUID reported issues: $LOCAL_FLASH_GUID" LOCAL_FLASH_GUID="" fi LOCAL_TPM_GUID=$(grep '^tpmGUID=' "$VAR_INI_PATH" 2>&1 | cut -d'"' -f2 | tr -d '[:space:]') || true if [[ "$LOCAL_TPM_GUID" == *"error"* ]] || [[ "$LOCAL_TPM_GUID" == *"No such"* ]]; then LOG_TO_FILE "${MSG_WARNING} grep for tpmGUID reported issues: $LOCAL_TPM_GUID" LOCAL_TPM_GUID="" fi LOCAL_REG_GUID=$(grep '^regGUID=' "$VAR_INI_PATH" 2>&1 | cut -d'"' -f2 | tr -d '[:space:]') || true if [[ "$LOCAL_REG_GUID" == *"error"* ]] || [[ "$LOCAL_REG_GUID" == *"No such"* ]]; then LOG_TO_FILE "${MSG_WARNING} grep for regGUID reported issues: $LOCAL_REG_GUID" LOCAL_REG_GUID="" fi LICENSE_TYPE="$LOCAL_LICENSE_TYPE" else PRINT_MSG "ERROR" "${MSG_ERROR} ${MSG_CONFIG_FILE_NOT_FOUND}" LOG_TO_FILE "${MSG_WARNING} ${MSG_VARINI_NOT_FOUND}" fi # Determine License Binding Type LICENSE_STATUS="Unknown" DEVICE_BINDING="None" if [[ "$LOCAL_FLASH_GUID" == "$LOCAL_REG_GUID" ]] && [[ "$LOCAL_FLASH_GUID" != "$LOCAL_TPM_GUID" || -z "$LOCAL_TPM_GUID" ]]; then LICENSE_STATUS="USB_FLASH" DEVICE_BINDING="USB Stick" if [[ -n "$LOCAL_TPM_GUID" ]] && [[ "$LOCAL_FLASH_GUID" != "$LOCAL_TPM_GUID" ]]; then if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "INFO" "TPM Key present, currently not used." fi LOG_TO_FILE "${MSG_INFO} TPM Key present, currently not used." fi elif [[ "$LOCAL_FLASH_GUID" == "$LOCAL_TPM_GUID" ]] && [[ "$LOCAL_FLASH_GUID" == "$LOCAL_REG_GUID" ]]; then LICENSE_STATUS="FULL_TPM" DEVICE_BINDING="TPM" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "SUCCESS" "TPM Key used for Device Binding and TPM Licensing is active." fi LOG_TO_FILE "${MSG_SUCCESS} TPM Key used for Device Binding and TPM Licensing is active." elif [[ "$LOCAL_REG_GUID" == "$LOCAL_TPM_GUID" ]]; then LICENSE_STATUS="TPM_MIGRATION" DEVICE_BINDING="TPM" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "SUCCESS" "TPM Key used for Device Binding and TPM Licensing is available." fi LOG_TO_FILE "${MSG_INFO} TPM Key used for Device Binding and TPM Licensing is available." else LICENSE_STATUS="INVALID_CONFIG" DEVICE_BINDING="Unknown" LOG_TO_FILE "${MSG_WARNING} GUID Configuration could not be uniquely identified." fi # Mask GUIDs for Secure Display LOCAL_FLASH_MASKED=$(MASK_GUID_FORMAT "$LOCAL_FLASH_GUID") LOCAL_TPM_MASKED=$(MASK_GUID_FORMAT "$LOCAL_TPM_GUID") LOCAL_REG_MASKED=$(MASK_GUID_FORMAT "$LOCAL_REG_GUID") # CLI Output (Consistent Format - Title Case for UI Terms) if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then echo "USB LICENSE ID: ${LOCAL_FLASH_MASKED:-N/A}" echo "TPM KEY: ${LOCAL_TPM_MASKED:-N/A}" echo "ACTIVE REGISTRATION: ${LOCAL_REG_MASKED:-N/A}" echo "----------------------------------------" echo "SERVER NAME: ${LOCAL_SERVER_NAME:-Unavailable}" echo "OS VERSION: ${LOCAL_OS_VERSION:-Unavailable}" echo "LICENSE TYPE: ${LOCAL_LICENSE_TYPE:-Not detected or invalid}" echo "DETECTED BINDING: ${DEVICE_BINDING} (${LICENSE_STATUS})" echo "----------------------------------------" echo "DETECTED MODE: ${LICENSE_STATUS}" echo "LICENSE DEVICE TYPE: ${DEVICE_BINDING}" fi # Check License Status and Update Overall Status if [[ "$LICENSE_STATUS" == "INVALID_CONFIG" ]]; then LOG_TO_FILE "${MSG_WARNING} License Configuration invalid" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "WARNING" "License Configuration invalid" fi OVERALL_STATUS="WARNING" fi # Log Module Results LOG_TO_FILE "USB LICENSE ID: ${LOCAL_FLASH_MASKED:-N/A}" LOG_TO_FILE "TPM KEY: ${LOCAL_TPM_MASKED:-N/A}" LOG_TO_FILE "ACTIVE REGISTRATION: ${LOCAL_REG_MASKED:-N/A}" LOG_TO_FILE "SERVER NAME: ${LOCAL_SERVER_NAME:-Unavailable}" LOG_TO_FILE "OS VERSION: ${LOCAL_OS_VERSION:-Unavailable}" LOG_TO_FILE "LICENSE TYPE: ${LOCAL_LICENSE_TYPE:-Undetermined}" LOG_TO_FILE "DETECTED BINDING: ${DEVICE_BINDING} (${LICENSE_STATUS})" LOG_TO_FILE "---[${MOD_TITLE_1} COMPLETED]---" LOG_TO_FILE "" else LOG_TO_FILE "---[MODULE 1: ${MOD_TITLE_1}]---" LOG_TO_FILE "MODULE [DISABLED]" fi # --- MODULE 2: BOOT MEDIUM ANALYSIS ----------------------------------------- if [ "$ENABLE_BOOT_CHECK" = "TRUE" ]; then LOG_TO_FILE "---[MODULE 2: ${MOD_TITLE_2}]---" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_SECTION "${MOD_TITLE_2}" fi BOOT_MEDIUM_TYPE="Unknown" # Local Variables for Boot Analysis BOOT_DEVICE_NAME="" PARENT_DEVICE="" TRANSPORT="" SYSTEM_DEV_FINAL="" SYS_BOOT_TYPE="Unknown" SYS_VENDOR="" SYS_MODEL_CLEAN="" # Detect Storage Types (NVMe, SATA, MMC) LSBLK_OUTPUT=$(lsblk -d -no NAME,TYPE 2>&1) || true if echo "$LSBLK_OUTPUT" | grep -qiE "error|cannot"; then LOG_TO_FILE "${MSG_WARNING} lsblk reported issues: $LSBLK_OUTPUT" fi NVME_DEV_GLOBAL=$(echo "$LSBLK_OUTPUT" | grep 'nvme' | awk '{print $1}' | head -1) SDA_DEV_GLOBAL=$(echo "$LSBLK_OUTPUT" | grep -E '^sd[a-z]$' | awk '{print $1}' | head -1) MMC_DEV_GLOBAL=$(echo "$LSBLK_OUTPUT" | grep 'mmc' | awk '{print $1}' | head -1) # Analyze Flash Label Link if [ -e "/dev/disk/by-label/flash" ]; then REAL_LINK=$(readlink -f "/dev/disk/by-label/flash") BOOT_DEVICE_NAME=$(basename "$REAL_LINK") # Remove Partition Number Suffix if [[ "$BOOT_DEVICE_NAME" =~ [0-9]+$ ]]; then PARENT_DEVICE="${BOOT_DEVICE_NAME%[0-9]*}" else PARENT_DEVICE="$BOOT_DEVICE_NAME" fi # Determine Transport Protocol (USB, SAS, etc.) TRANSPORT=$(lsblk -no TRAN "$PARENT_DEVICE" 2>&1 | tr -d '[:space:]') || true if [[ "$TRANSPORT" == *"error"* ]] || [[ "$TRANSPORT" == *"cannot"* ]]; then LOG_TO_FILE "${MSG_WARNING} lsblk for transport reported issues: $TRANSPORT" TRANSPORT="" fi case "$TRANSPORT" in usb) BOOT_MEDIUM_TYPE="USB Stick Boot" ;; *) SYS_PATH="/sys/block/$PARENT_DEVICE/device" if [ -L "$SYS_PATH" ]; then FULL_PATH=$(readlink -f "$SYS_PATH") if [[ "$FULL_PATH" =~ usb ]]; then BOOT_MEDIUM_TYPE="USB Stick Boot" else BOOT_MEDIUM_TYPE="Internal Boot" fi else BOOT_MEDIUM_TYPE="Internal Boot" fi ;; esac # Get Device Details for Internal Boot if [[ "$BOOT_MEDIUM_TYPE" == "Internal Boot" ]]; then if [[ -n "$NVME_DEV_GLOBAL" ]]; then SYSTEM_DEV_FINAL="$NVME_DEV_GLOBAL" elif [[ -n "$SDA_DEV_GLOBAL" ]]; then SYSTEM_DEV_FINAL="$SDA_DEV_GLOBAL" elif [[ -n "$MMC_DEV_GLOBAL" ]]; then SYSTEM_DEV_FINAL="$MMC_DEV_GLOBAL" fi SYS_BOOT_TYPE="Internal Boot" if [[ -n "$SYSTEM_DEV_FINAL" ]] && [[ -b "/dev/$SYSTEM_DEV_FINAL" ]]; then VENDOR_RAW=$(lsblk -no VENDOR "/dev/$SYSTEM_DEV_FINAL" 2>&1 | head -1 | xargs) || true MODEL_RAW=$(lsblk -no MODEL "/dev/$SYSTEM_DEV_FINAL" 2>&1 | head -1 | xargs) || true if [[ "$VENDOR_RAW" == *"error"* ]] || [[ "$MODEL_RAW" == *"error"* ]]; then LOG_TO_FILE "${MSG_WARNING} lsblk for vendor/model reported issues: VENDOR=$VENDOR_RAW MODEL=$MODEL_RAW" fi if [[ -z "$VENDOR_RAW" || "$VENDOR_RAW" == "" ]]; then SYS_VENDOR="(see Model)" else SYS_VENDOR="$VENDOR_RAW" fi SYS_MODEL_CLEAN=$(CLEAN_MODEL_NAME "$MODEL_RAW") fi fi else LOG_TO_FILE "${MSG_WARNING} /dev/disk/by-label/flash not found." fi # Gather License Device Information LIC_VENDOR="-" LIC_MODEL_CLEAN="-" LIC_TYPE="" if [[ "$DEVICE_BINDING" == "USB Stick" ]]; then LSUSB_OUTPUT=$(lsusb 2>&1) || true if echo "$LSUSB_OUTPUT" | grep -qiE "error|command not found"; then LOG_TO_FILE "${MSG_WARNING} lsusb reported issues: $LSUSB_OUTPUT" fi USB_LINE=$(echo "$LSUSB_OUTPUT" | grep -iE "$USB_BRAND_PATTERN" | head -1) if [[ -n "$USB_LINE" ]]; then RAW_FULL=$(echo "$USB_LINE" | sed 's/.*ID [0-9a-f:]* //' | sed 's/ *Corp\. //g' | sed 's/ *Inc\. //g' | xargs) LIC_VENDOR=$(echo "$RAW_FULL" | awk '{print $1}') LIC_VENDOR=$(CLEAN_VENDOR_NAME "$LIC_VENDOR") if [[ "$LIC_VENDOR" == "$RAW_FULL" ]]; then LIC_MODEL_CLEAN="Unknown USB Device" else MOD_PART=$(echo "$RAW_FULL" | sed "s/^${LIC_VENDOR}[[:space:]]*//") LIC_MODEL_CLEAN=$(CLEAN_MODEL_NAME "$MOD_PART") fi fi [[ -z "$LIC_VENDOR" ]] && LIC_VENDOR="Unknown" [[ -z "$LIC_MODEL_CLEAN" ]] && LIC_MODEL_CLEAN="Unknown" LIC_TYPE="USB Stick" else LIC_TYPE="TPM" fi LOG_TO_FILE "BOOT MEDIUM TYPE: ${BOOT_MEDIUM_TYPE}" LOG_TO_FILE "" LOG_TO_FILE "LICENSE DEVICE" LOG_TO_FILE "TYPE: ${LIC_TYPE}" LOG_TO_FILE "MANUFACTURER: ${LIC_VENDOR}" LOG_TO_FILE "MODEL: ${LIC_MODEL_CLEAN}" LOG_TO_FILE "" LOG_TO_FILE "SYSTEM DEVICE (OS)" LOG_TO_FILE "TYPE: ${SYS_BOOT_TYPE}" LOG_TO_FILE "MANUFACTURER: ${SYS_VENDOR}" LOG_TO_FILE "MODEL: ${SYS_MODEL_CLEAN}" # CLI Output (Title Case for UI Terms: USB Stick, TPM, Internal Boot, Backup File, etc.) if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then echo "BOOT MEDIUM TYPE: ${BOOT_MEDIUM_TYPE}" echo "" echo "LICENSE DEVICE" echo "TYPE: ${LIC_TYPE}" echo "MANUFACTURER: ${LIC_VENDOR}" echo "MODEL: ${LIC_MODEL_CLEAN}" echo "" echo "SYSTEM DEVICE (OS)" echo "TYPE: ${SYS_BOOT_TYPE}" echo "MANUFACTURER: ${SYS_VENDOR}" echo "MODEL: ${SYS_MODEL_CLEAN}" fi # Check Boot Medium Status if [[ "$BOOT_MEDIUM_TYPE" == "Unknown" ]]; then LOG_TO_FILE "${MSG_WARNING} Boot Medium could not be identified" OVERALL_STATUS="WARNING" fi LOG_TO_FILE "---[${MOD_TITLE_2} COMPLETED]---" LOG_TO_FILE "" else LOG_TO_FILE "---[MODULE 2: ${MOD_TITLE_2}]---" LOG_TO_FILE "MODULE [DISABLED]" fi # --- MODULE 3: HARDWARE CHECK ----------------------------------------------- if [ "$ENABLE_HARDWARE_CHECK" = "TRUE" ]; then LOG_TO_FILE "---[MODULE 3: ${MOD_TITLE_3}]---" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_SECTION "${MOD_TITLE_3}" fi # TPM Device Check if [ -c /dev/tpm0 ] || [ -c /dev/tpmrm0 ]; then if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "SUCCESS" "TPM Hardware detected." fi LOG_TO_FILE "${MSG_SUCCESS} TPM Hardware detected." else if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "WARNING" "${MSG_NO_TPM_DEVICE}" fi LOG_TO_FILE "${MSG_WARNING} ${MSG_NO_TPM_DEVICE}" fi # USB Stick Check (Only Relevant for USB Flash License) if [[ "$LICENSE_STATUS" == "USB_FLASH" ]]; then LSUSB_OUTPUT=$(lsusb 2>&1) || true if [[ $(echo "$LSUSB_OUTPUT" | grep -ciE "$USB_BRAND_PATTERN") -eq 0 ]]; then if echo "$LSUSB_OUTPUT" | grep -qiE "error|command not found"; then LOG_TO_FILE "${MSG_WARNING} lsusb reported issues during notification: $LSUSB_OUTPUT" fi if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "ERROR" "${MSG_NO_USB_STICK}" fi LOG_TO_FILE "${MSG_ERROR} ${MSG_NO_USB_STICK}" else if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "INFO" "${MSG_USB_STICK_DETECTED}" fi LOG_TO_FILE "${MSG_INFO} ${MSG_USB_STICK_DETECTED}" fi else if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "INFO" "${MSG_TPM_MODE}" fi LOG_TO_FILE "${MSG_INFO} ${MSG_TPM_MODE}" fi # Missing USB Device for Flash License LSUSB_FINAL=$(lsusb 2>&1) || true if [[ "$LICENSE_STATUS" == "USB_FLASH" ]] && [[ $(echo "$LSUSB_FINAL" | grep -ciE "$USB_BRAND_PATTERN") -eq 0 ]]; then if echo "$LSUSB_FINAL" | grep -qiE "error|command not found"; then LOG_TO_FILE "${MSG_WARNING} lsusb reported issues during final USB check: $LSUSB_FINAL" fi OVERALL_STATUS="ERROR" fi LOG_TO_FILE "---[${MOD_TITLE_3} COMPLETED]---" LOG_TO_FILE "" else LOG_TO_FILE "---[MODULE 3: ${MOD_TITLE_3}]---" LOG_TO_FILE "MODULE [DISABLED]" fi # --- MODULE 4: SYSTEM INTEGRITY CHECK --------------------------------------- if [ "$ENABLE_SYSTEM_CHECK" = "TRUE" ]; then LOG_TO_FILE "---[MODULE 4: ${MOD_TITLE_4}]---" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_SECTION "${MOD_TITLE_4}" fi # Show Dirty Check Status if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then echo "DIRTY CHECK OVERRIDE: ${SKIP_DIRTY_CHECK}" echo "AUTO CLEANUP: ${ENABLE_AUTO_CLEANUP}" echo "----------------------------------------" fi LOG_TO_FILE "${MSG_INFO} SKIP_DIRTY_CHECK: ${SKIP_DIRTY_CHECK}" LOG_TO_FILE "${MSG_INFO} AUTO CLEANUP: ${ENABLE_AUTO_CLEANUP}" # CHECK 1: Array Disk Count DISK_COUNT=$(ls -d /mnt/disk* 2>/dev/null | wc -l) if [ "$DISK_COUNT" -lt "$MIN_DISKS" ]; then LOG_TO_FILE "${MSG_ERROR} Only ${DISK_COUNT} Disks found. Minimum ${MIN_DISKS} required." SEND_ERROR_NOTIFICATION "" "" "Process aborted: Only ${DISK_COUNT} Disks found. Minimum ${MIN_DISKS} required." if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "ERROR" "Only ${DISK_COUNT} Disks found. Minimum ${MIN_DISKS} required." fi exit 1 fi if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "SUCCESS" "${MSG_ARRAY_INTEGRITY_CONFIRMED} (${DISK_COUNT} Disks present)" fi LOG_TO_FILE "${MSG_SUCCESS} ${MSG_ARRAY_INTEGRITY_CONFIRMED} (${DISK_COUNT} Disks present)" # CHECK 2: Stale Temporary Files HAS_STALE_FILES=FALSE declare -a STALE_FILES=() # Find Stale Files in WebGUI Temp Directory FIND_OUTPUT=$(find /usr/local/emhttp/ -maxdepth 1 -name "*boot-backup*.zip" \( -type f -o -type l \) 2>&1) FIND_EXIT=$? # Log non-zero exit if not just expected permission denied if [ $FIND_EXIT -ne 0 ] || echo "$FIND_OUTPUT" | grep -qiE "permission denied|no such file|error"; then LOG_TO_FILE "${MSG_WARNING} find in /usr/local/emhttp reported issues:" LOG_TO_FILE "[DEBUG] $FIND_OUTPUT" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "WARNING" "find encountered issues: $FIND_OUTPUT" fi fi while IFS= read -r F; do [[ -n "$F" ]] && STALE_FILES+=("WEBGUI|$F") done <<< "$FIND_OUTPUT" # Find Stale Files in RootFS Directory FIND_OUTPUT=$(find / -maxdepth 1 -name "*boot-backup*.zip" -type f 2>&1) FIND_EXIT=$? if [ $FIND_EXIT -ne 0 ] || echo "$FIND_OUTPUT" | grep -qiE "permission denied|no such file|error"; then LOG_TO_FILE "${MSG_WARNING} find in / reported issues:" LOG_TO_FILE "[DEBUG] $FIND_OUTPUT" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "WARNING" "find encountered issues: $FIND_OUTPUT" fi fi while IFS= read -r F; do [[ -n "$F" ]] && STALE_FILES+=("ROOTFS|$F") done <<< "$FIND_OUTPUT" STALE_COUNT=${#STALE_FILES[@]} if [ "$STALE_COUNT" -gt 0 ]; then HAS_STALE_FILES=TRUE LOG_TO_FILE "${MSG_WARNING} ${STALE_COUNT} ${MSG_STALE_FILES_FOUND}" # Abort if SKIP_DIRTY_CHECK not set if [ -z "$SKIP_DIRTY_CHECK" ] || [ "$SKIP_DIRTY_CHECK" = "FALSE" ]; then OVERALL_STATUS="ERROR" LOG_TO_FILE "${MSG_ERROR} ${MSG_PROCESS_ABORTED}" LOG_TO_FILE "${MSG_INFO} SKIP_DIRTY_CHECK is DISABLED - Aborting" SEND_ERROR_NOTIFICATION "" "" "Process aborted: ${STALE_COUNT} stale Temporary Files found. Manual cleanup required." if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "ERROR" "${STALE_COUNT} ${MSG_STALE_FILES_FOUND} Manual cleanup required." echo "────────────────────────────────────────────────────────────" for ENTRY in "${STALE_FILES[@]}"; do SOURCE_TYPE="${ENTRY%%|*}" FILEPATH="${ENTRY#*|}" case "$SOURCE_TYPE" in WEBGUI) PREFIX="[WebGUI Temp] " ;; ROOTFS) PREFIX="[RootFS Temp] " ;; esac if [[ -f "$FILEPATH" ]]; then # Safe stat with error handling STAT_OUTPUT=$(stat -c '%s %y' "$FILEPATH" 2>&1) if [ $? -ne 0 ]; then LOG_TO_FILE "${MSG_WARNING} Cannot stat File for display: $FILEPATH" SIZE_DISPLAY="Unknown" MTIME="Unknown" else SIZE_BYTES=$(echo "$STAT_OUTPUT" | awk '{print $1}') MTIME=$(echo "$STAT_OUTPUT" | awk '{for(i=2;i<=NF;i++) printf "%s ", $i}' | cut -d'.' -f1 | xargs) if [ -n "$SIZE_BYTES" ] && [ "$SIZE_BYTES" -gt 1073741824 ]; then SIZE_DISPLAY=$(awk "BEGIN {printf \"%.2f\", $SIZE_BYTES/1024/1024/1024}")" GB" elif [ -n "$SIZE_BYTES" ]; then SIZE_DISPLAY=$(awk "BEGIN {printf \"%.2f\", $SIZE_BYTES/1024/1024}")" MB" else SIZE_DISPLAY="Unknown" fi fi echo " ${PREFIX}${FILEPATH}" echo " Size: ${SIZE_DISPLAY} | Modified: ${MTIME}" elif [[ -L "$FILEPATH" ]]; then echo " ${PREFIX}SYMLINK → $(readlink "$FILEPATH" 2>/dev/null)" else echo " ${PREFIX}${FILEPATH} (Missing/Corrupted)" fi done echo "────────────────────────────────────────────────────────────" echo "NOTE: Temporary Files will be cleaned on Reboot." echo "→ Manual cleanup required." echo "→ Set SKIP_DIRTY_CHECK=TRUE to ignore." echo "→ Set ENABLE_AUTO_CLEANUP=TRUE for automatic removal after all Modules complete." fi exit 1 else HAS_STALE_FILES=TRUE SKIP_DIRTY_CHECK_ACTIVE=TRUE OVERALL_STATUS="WARNING" LOG_TO_FILE "${MSG_WARNING} ${MSG_WARNING_IGNORED} (SKIP_DIRTY_CHECK=TRUE)" LOG_TO_FILE "${MSG_WARNING} Dirty Check Bypassed - Continuing anyway." if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "WARNING" "${STALE_COUNT} ${MSG_STALE_FILES_FOUND}" PRINT_MSG "WARNING" "${MSG_WARNING_IGNORED} (SKIP_DIRTY_CHECK=TRUE)" PRINT_MSG "WARNING" "Dirty Check Bypassed - Continuing anyway." echo "────────────────────────────────────────────────────────────" for ENTRY in "${STALE_FILES[@]}"; do SOURCE_TYPE="${ENTRY%%|*}" FILEPATH="${ENTRY#*|}" case "$SOURCE_TYPE" in WEBGUI) PREFIX="[WebGUI Temp] " ;; ROOTFS) PREFIX="[RootFS Temp] " ;; esac if [[ -f "$FILEPATH" ]]; then # Safe stat with error handling STAT_OUTPUT=$(stat -c '%s %y' "$FILEPATH" 2>&1) STAT_EXIT=$? if [ $STAT_EXIT -ne 0 ]; then LOG_TO_FILE "${MSG_WARNING} Cannot stat file for display: $FILEPATH" SIZE_DISPLAY="Unknown" MTIME="Unknown" else SIZE_BYTES=$(echo "$STAT_OUTPUT" | awk '{print $1}') MTIME=$(echo "$STAT_OUTPUT" | awk '{for(i=2;i<=NF;i++) printf "%s ", $i}' | cut -d'.' -f1 | xargs) if [ -n "$SIZE_BYTES" ] && [ "$SIZE_BYTES" -gt 1073741824 ]; then SIZE_DISPLAY=$(awk "BEGIN {printf \"%.2f\", $SIZE_BYTES/1024/1024/1024}")" GB" elif [ -n "$SIZE_BYTES" ]; then SIZE_DISPLAY=$(awk "BEGIN {printf \"%.2f\", $SIZE_BYTES/1024/1024}")" MB" else SIZE_DISPLAY="Unknown" fi fi echo " ${PREFIX}${FILEPATH}" echo " Size: ${SIZE_DISPLAY} | Modified: ${MTIME}" elif [[ -L "$FILEPATH" ]]; then LINK_TARGET=$(readlink "$FILEPATH" 2>&1) if [ $? -ne 0 ]; then LOG_TO_FILE "${MSG_WARNING} Cannot resolve Symlink Target: $FILEPATH" echo " ${PREFIX}SYMLINK → [Unable to resolve]" else echo " ${PREFIX}SYMLINK → $LINK_TARGET" fi else LOG_TO_FILE "${MSG_WARNING} File disappeared during listing: $FILEPATH" echo " ${PREFIX}${FILEPATH} (Missing/Disappeared)" fi done echo "────────────────────────────────────────────────────────────" echo "Session Log contains full Details and Error tracking: $LOG_FILE_PATH" echo "" echo "AUTO CLEANUP STATUS: ${ENABLE_AUTO_CLEANUP}" if [ "$ENABLE_AUTO_CLEANUP" = "TRUE" ]; then echo "→ Will run at end of Script through ${MOD_TITLE_8}." echo "→ Files removed after Backup & Retention completes." else echo "→ Currently DISABLED." echo "→ Set ENABLE_AUTO_CLEANUP=TRUE for automatic removal." echo "" echo "NOTE: ${STALE_COUNT} Stale Files remain on system." fi echo "" PRINT_MSG "WARNING" "Skipping cleanup and continuing despite Stale Files." fi fi else if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "SUCCESS" "${MSG_NO_STALE_FILES}" fi LOG_TO_FILE "${MSG_SUCCESS} ${MSG_NO_STALE_FILES}" fi LOG_TO_FILE "---[${MOD_TITLE_4} COMPLETED]---" LOG_TO_FILE "" else LOG_TO_FILE "---[MODULE 4: ${MOD_TITLE_4}]---" LOG_TO_FILE "MODULE [DISABLED]" fi # --- MODULE 5: CREATE BACKUP ------------------------------------------------ BACKUP_FILE="" if [ "$ENABLE_BACKUP_CREATE" = "TRUE" ]; then LOG_TO_FILE "---[MODULE 5: ${MOD_TITLE_5}]---" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_SECTION "${MOD_TITLE_5}" fi LOG_TO_FILE "Target Directory: ${BACKUP_BASE_DIR}" LOG_TO_FILE "Starting Native Flash Backup Script..." if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then echo "Target Directory: ${BACKUP_BASE_DIR}" echo "Starting Native Flash Backup Script..." fi # Check 1: unzip Utility available if ! command -v unzip >/dev/null 2>&1; then HANDLE_ERROR_EXIT "'unzip' Command not found. Integrity Check impossible." \ "'unzip' Command not found. Integrity Check impossible." fi # Execute Native Flash Backup PHP Script BACKUP_RAW_OUTPUT=$(/usr/bin/php -q /usr/local/emhttp/webGui/scripts/flash_backup 2>&1) PHP_EXIT_CODE=$? if [ $PHP_EXIT_CODE -ne 0 ]; then LOG_TO_FILE "[DEBUG] PHP Output: ${BACKUP_RAW_OUTPUT}" HANDLE_ERROR_EXIT "Flash Backup Tool failed with Exit Code ${PHP_EXIT_CODE}." \ "Flash Backup Tool failed with Exit Code ${PHP_EXIT_CODE}." fi BACKUP_FILE=$(echo "$BACKUP_RAW_OUTPUT" | tail -n 1 | xargs) if [ -z "$BACKUP_FILE" ]; then LOG_TO_FILE "[DEBUG] PHP Output: ${BACKUP_RAW_OUTPUT}" HANDLE_ERROR_EXIT "Invalid Filename generated (Empty Output)." \ "Invalid Filename generated (Empty Output)." fi SYMLINK_PATH="/usr/local/emhttp/${BACKUP_FILE}" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "SUCCESS" "PHP flash_backup Script executed and Symlink at ${SYMLINK_PATH} created." fi LOG_TO_FILE "${MSG_SUCCESS} PHP flash_backup Script executed and Symlink at ${SYMLINK_PATH} created." # Check 2: Symlink exists if [ ! -f "$SYMLINK_PATH" ] && [ ! -L "$SYMLINK_PATH" ]; then HANDLE_ERROR_EXIT "Source File does not exist: ${SYMLINK_PATH}." \ "Source File does not exist: ${SYMLINK_PATH}." fi # Resolve Real Path READLINK_OUTPUT=$(readlink -f "$SYMLINK_PATH" 2>&1) || true if [[ -z "$READLINK_OUTPUT" ]] || [[ "$READLINK_OUTPUT" == *"error"* ]] || [[ "$READLINK_OUTPUT" == *"cannot"* ]]; then LOG_TO_FILE "${MSG_WARNING} readlink failed, falling back to symlink path: $READLINK_OUTPUT" SOURCE_FILE="$SYMLINK_PATH" else SOURCE_FILE="$READLINK_OUTPUT" fi DEST_FILE="${BACKUP_BASE_DIR}/${BACKUP_FILE}" # Check 3: Resolved Source File exists if [ ! -f "$SOURCE_FILE" ]; then HANDLE_ERROR_EXIT "Resolved Source File missing: ${SOURCE_FILE}." \ "Resolved Source File missing: ${SOURCE_FILE}." fi # Copy Backup File if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "INFO" "Copying Backup File to: ${DEST_FILE}" fi # Capture ALL Output including Errors CP_OUTPUT=$(cp "$SOURCE_FILE" "$DEST_FILE" 2>&1) COPY_EXIT=$? if [ $COPY_EXIT -ne 0 ]; then LOG_TO_FILE "${MSG_ERROR} Copy Operation failed with Exit Code ${COPY_EXIT}." LOG_TO_FILE "[DEBUG] cp Output: $CP_OUTPUT" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "ERROR" "${MSG_COPY_FAILED}" echo "→ See Session Log for Details: $LOG_FILE_PATH" fi HANDLE_ERROR_EXIT "${MSG_COPY_FAILED}" \ "Copy Error Details logged in Session Log." fi LOG_TO_FILE "Copied Backup File to: ${DEST_FILE}" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "SUCCESS" "Copy completed successfully." fi # Integrity Check if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "INFO" "Checking Integrity of File: ${DEST_FILE}" fi # Capture ALL Output including Errors UNZIP_OUTPUT=$(unzip -t -q "$DEST_FILE" 2>&1) UNZIP_EXIT=$? if [ $UNZIP_EXIT -ne 0 ]; then LOG_TO_FILE "${MSG_ERROR} Integrity Check failed with Exit Code ${UNZIP_EXIT}." LOG_TO_FILE "[DEBUG] unzip Output: $UNZIP_OUTPUT" LOG_TO_FILE "${MSG_INFO} Removing corrupted Backup File: $(basename "$DEST_FILE")" rm -f "$DEST_FILE" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "ERROR" "${MSG_BACKUP_CORRUPTED}: $(basename "${DEST_FILE}")." echo "→ Corruption Details in Session Log: $LOG_FILE_PATH" fi HANDLE_ERROR_EXIT "${MSG_BACKUP_CORRUPTED}: $(basename "${DEST_FILE}")." \ "Corruption Details logged in Session Log." fi LOG_TO_FILE "${MSG_SUCCESS} Integrity Check passed." if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "SUCCESS" "Integrity Check passed." fi # Cleanup Temporary Files if [ -f "$SOURCE_FILE" ]; then if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "INFO" "Temporary Backup File Location: ${SOURCE_FILE}" fi rm -f "$SOURCE_FILE" LOG_TO_FILE "${MSG_INFO} Temporary Backup File Location: ${SOURCE_FILE}" LOG_TO_FILE "${MSG_SUCCESS} Temporary Backup File removed: $(basename "$SOURCE_FILE")" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "SUCCESS" "Temporary Backup File removed: $(basename "$SOURCE_FILE")" fi fi if [ -L "$SYMLINK_PATH" ]; then if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "INFO" "Temporary Symlink Location: ${SYMLINK_PATH}" fi rm -f "$SYMLINK_PATH" LOG_TO_FILE "${MSG_INFO} Temporary Symlink Location: ${SYMLINK_PATH}" LOG_TO_FILE "${MSG_SUCCESS} Temporary Symlink removed." if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "SUCCESS" "Temporary Symlink removed." fi fi LOG_TO_FILE "---[BACKUP CREATION COMPLETED]---" LOG_TO_FILE "" # Update Last Run Log LOG_TO_FILE "---[UPDATE ARCHIVE LOG]---" { echo "=== LAST RUN: ${HOST_NAME} ===" echo "TIMESTAMP: $(date '+%Y-%m-%d %H:%M:%S')" echo "BACKUP FILE: ${BACKUP_FILE}" echo "BACKUP PATH: ${DEST_FILE}" echo "ARCHIVE LOG PATH: ${ARCHIVE_LOG_PATH}" echo "STATUS: SUCCESSFUL" [ -n "$LOCAL_LICENSE_TYPE" ] && echo "License Type: ${LOCAL_LICENSE_TYPE}" [ "${HAS_STALE_FILES:-FALSE}" = "TRUE" ] && echo "[WARNING] Stale Files present" echo "===============================" } > "$LAST_RUN_LOG" LOG_TO_FILE "Status Log saved: ${LAST_RUN_LOG}" LOG_TO_FILE "Session Log archived: ${ARCHIVE_LOG_PATH}" LOG_TO_FILE "---[UPDATE ARCHIVE LOG COMPLETED]---" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_SECTION "UPDATE ARCHIVE LOG" echo "Status Log saved: ${LAST_RUN_LOG}" echo "Session Log archived: ${ARCHIVE_LOG_PATH}" fi else LOG_TO_FILE "---[MODULE 5: ${MOD_TITLE_5}]---" LOG_TO_FILE "MODULE [DISABLED]" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_SECTION "${MOD_TITLE_5} SKIPPED" fi fi # --- MODULE 6: APPLY BACKUP RETENTION --------------------------------------- if [ "$ENABLE_BACKUP_RETENTION" = "TRUE" ]; then LOG_TO_FILE "---[MODULE 6: ${MOD_TITLE_6}]---" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_SECTION "${MOD_TITLE_6}" fi cd "$BACKUP_BASE_DIR" || { HANDLE_ERROR_EXIT "${MSG_CHANGE_DIR_FAILED}" \ "Change to Backup Directory failed." } # Delete Backups older than Retention Period FIND_AGE_OUTPUT=$(find . -maxdepth 1 -name "*boot-backup*.zip" -type f -mtime +"${BACKUP_RETENTION_DAYS}" -print 2>&1) FIND_AGE_EXIT=$? DELETED_BY_AGE=$(echo "$FIND_AGE_OUTPUT" | grep -c "\.zip$" || true) if [ "$FIND_AGE_EXIT" -ne 0 ]; then LOG_TO_FILE "${MSG_WARNING} find for age-based deletion reported issues: $FIND_AGE_OUTPUT" fi if [ "$DELETED_BY_AGE" -gt 0 ]; then LOG_TO_FILE "${MSG_INFO} ${DELETED_BY_AGE} Backup Files older than ${BACKUP_RETENTION_DAYS} Days removed." find . -maxdepth 1 -name "*boot-backup*.zip" -type f -mtime +"${BACKUP_RETENTION_DAYS}" -delete if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "INFO" "${DELETED_BY_AGE} Backup Files older than ${BACKUP_RETENTION_DAYS} Days removed." fi fi # Enforce Maximum Backup Count Limit if [[ -n "$MAX_BACKUPS_TO_KEEP" ]] && [[ "$MAX_BACKUPS_TO_KEEP" -gt 0 ]]; then LS_OUTPUT=$(ls -1 *boot-backup*.zip 2>&1) || true CURRENT_COUNT=$(echo "$LS_OUTPUT" | grep -c "\.zip$" || true) if echo "$LS_OUTPUT" | grep -qiE "error|cannot|no such"; then LOG_TO_FILE "${MSG_WARNING} ls reported issues: $LS_OUTPUT" fi if [ "$CURRENT_COUNT" -gt "$MAX_BACKUPS_TO_KEEP" ]; then TO_DELETE=$((CURRENT_COUNT - MAX_BACKUPS_TO_KEEP)) LOG_TO_FILE "${MSG_INFO} Current Backup Files: ${CURRENT_COUNT} | Max: ${MAX_BACKUPS_TO_KEEP} → Deleting ${TO_DELETE} Files." mapfile -t BACKUP_FILES_TO_DELETE < <(ls -t *boot-backup*.zip 2>&1 | tail -n +$((MAX_BACKUPS_TO_KEEP + 1))) for OLD_BACKUP in "${BACKUP_FILES_TO_DELETE[@]}"; do if [ -n "$OLD_BACKUP" ]; then rm -f "$OLD_BACKUP" LOG_TO_FILE "${MSG_INFO} Deleted: $(basename "$OLD_BACKUP")" fi done if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "INFO" "Deleted ${TO_DELETE} old Backup Files to stay within limit of ${MAX_BACKUPS_TO_KEEP}." for OLD_BACKUP in "${BACKUP_FILES_TO_DELETE[@]}"; do if [ -n "$OLD_BACKUP" ]; then PRINT_MSG "INFO" "Deleted: $(basename "$OLD_BACKUP")" fi done fi else if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "INFO" "Backup File Count (${CURRENT_COUNT}) is within limit (${MAX_BACKUPS_TO_KEEP})." fi fi fi FINAL_LS_OUTPUT=$(ls -1 *boot-backup*.zip 2>&1) || true FINAL_COUNT=$(echo "$FINAL_LS_OUTPUT" | grep -c "\.zip$" || true) LOG_TO_FILE "${MSG_INFO} Result: ${FINAL_COUNT} Flash or Boot Backup Files remaining." LOG_TO_FILE "---[${MOD_TITLE_6} COMPLETED]---" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "INFO" "Total Backup Files remaining: ${FINAL_COUNT}" fi else LOG_TO_FILE "---[MODULE 6: ${MOD_TITLE_6}]---" LOG_TO_FILE "MODULE [DISABLED]" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_SECTION "${MOD_TITLE_6} SKIPPED" fi fi # --- MODULE 7: APPLY LOG RETENTION ------------------------------------------ if [ "$ENABLE_LOG_RETENTION" = "TRUE" ]; then LOG_TO_FILE "---[MODULE 7: ${MOD_TITLE_7}]---" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_SECTION "${MOD_TITLE_7}" fi cd "$ARCHIVE_LOG_DIR" || { HANDLE_ERROR_EXIT "${MSG_CHANGE_DIR_FAILED}" \ "Change to Archive Log Directory failed." } # Delete Logs older than Retention Period (Exclude Last_Run.log) FIND_LOG_AGE_OUTPUT=$(find . -maxdepth 1 -name "*.log" -type f ! -name "Last_Run.log" -mtime +"${LOG_RETENTION_DAYS}" -print 2>&1) FIND_LOG_AGE_EXIT=$? DELETED_LOGS_BY_AGE=$(echo "$FIND_LOG_AGE_OUTPUT" | grep -c "\.log$" || true) if [ "$FIND_LOG_AGE_EXIT" -ne 0 ]; then LOG_TO_FILE "${MSG_WARNING} find for log age-based deletion reported issues: $FIND_LOG_AGE_OUTPUT" fi if [ "$DELETED_LOGS_BY_AGE" -gt 0 ]; then LOG_TO_FILE "${MSG_INFO} ${DELETED_LOGS_BY_AGE} Log Files older than ${LOG_RETENTION_DAYS} Days removed." find . -maxdepth 1 -name "*.log" -type f ! -name "Last_Run.log" -mtime +"${LOG_RETENTION_DAYS}" -delete if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "INFO" "${DELETED_LOGS_BY_AGE} Log Files older than ${LOG_RETENTION_DAYS} Days removed." fi fi # Enforce Maximum Log File Count if [[ -n "$MAX_LOG_FILES_TO_KEEP" ]] && [[ "$MAX_LOG_FILES_TO_KEEP" -gt 0 ]]; then FIND_COUNT_OUTPUT=$(find . -maxdepth 1 -name "${LOG_FILE_PATTERN}" -type f 2>&1) || true LOG_FILE_COUNT=$(echo "$FIND_COUNT_OUTPUT" | grep -c "\.log$" || true) if echo "$FIND_COUNT_OUTPUT" | grep -qiE "error|cannot|permission"; then LOG_TO_FILE "${MSG_WARNING} find for Log count reported issues: $FIND_COUNT_OUTPUT" fi if [ "$LOG_FILE_COUNT" -gt "$MAX_LOG_FILES_TO_KEEP" ]; then TO_DELETE_COUNT=$((LOG_FILE_COUNT - MAX_LOG_FILES_TO_KEEP)) LOG_TO_FILE "${MSG_INFO} Current Log Files: ${LOG_FILE_COUNT} | Max: ${MAX_LOG_FILES_TO_KEEP} → Deleting ${TO_DELETE_COUNT} Files." # Timestamp based sorting for accurate oldest first deletion mapfile -t FILES_TO_DELETE < <( for F in $(find . -maxdepth 1 -name "${LOG_FILE_PATTERN}" -type f 2>&1); do TS=$(stat -c '%Y' "$F" 2>&1) [[ -z "$TS" ]] && TS=0 printf '%s %s\n' "$TS" "$F" done | sort -n | head -n "$TO_DELETE_COUNT" | awk '{print $2}' ) for OLD_LOG in "${FILES_TO_DELETE[@]}"; do if [ -n "$OLD_LOG" ]; then rm -f "$OLD_LOG" LOG_TO_FILE "${MSG_INFO} Deleted: $(basename "$OLD_LOG")" fi done if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "INFO" "Deleted ${TO_DELETE_COUNT} old Log Files to stay within limit of ${MAX_LOG_FILES_TO_KEEP}." for OLD_LOG in "${FILES_TO_DELETE[@]}"; do if [ -n "$OLD_LOG" ]; then PRINT_MSG "INFO" "Deleted: $(basename "$OLD_LOG")" fi done fi else if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "INFO" "Log File Count (${LOG_FILE_COUNT}) is within limit (${MAX_LOG_FILES_TO_KEEP})." fi fi fi FINAL_FIND_OUTPUT=$(find . -maxdepth 1 -name "${LOG_FILE_PATTERN}" -type f 2>&1) || true FINAL_LOG_COUNT=$(echo "$FINAL_FIND_OUTPUT" | grep -c "\.log$" || true) if echo "$FINAL_FIND_OUTPUT" | grep -qiE "error|cannot|permission"; then LOG_TO_FILE "${MSG_WARNING} find for final Log count reported issues: $FINAL_FIND_OUTPUT" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "WARNING" "Log File search encountered issues: $FINAL_FIND_OUTPUT" fi fi LOG_TO_FILE "${MSG_INFO} Result: ${FINAL_LOG_COUNT} Log Files remaining (Excluding Last_Run.log)." LOG_TO_FILE "---[${MOD_TITLE_7} COMPLETED]---" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "INFO" "Total Log Files remaining: ${FINAL_LOG_COUNT}" fi else LOG_TO_FILE "---[MODULE 7: ${MOD_TITLE_7}]---" LOG_TO_FILE "MODULE [DISABLED]" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_SECTION "${MOD_TITLE_7} SKIPPED" fi fi # --- MODULE 8: AUTO CLEANUP ----------------------------------------------- if [ "$ENABLE_AUTO_CLEANUP" = "TRUE" ]; then LOG_TO_FILE "---[MODULE 8: ${MOD_TITLE_8}]---" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_SECTION "${MOD_TITLE_8}" echo "AUTO CLEANUP STATUS: ENABLED" echo "STALE FILES FROM ${MOD_TITLE_4}: ${HAS_STALE_FILES:-N/A}" echo "----------------------------------------" fi # Proceed only if Stale Files were detected (from Module 4) OR new ones exist declare -a ALL_STALE_FILES=() # WebGUI Temp Directory CLEANUP_FIND_WEBGUI=$(find /usr/local/emhttp/ -maxdepth 1 -name "*boot-backup*.zip" \( -type f -o -type l \) 2>&1) if echo "$CLEANUP_FIND_WEBGUI" | grep -qiE "error|cannot|permission"; then LOG_TO_FILE "${MSG_WARNING} find in /usr/local/emhttp during cleanup reported issues: $CLEANUP_FIND_WEBGUI" fi while IFS= read -r F; do [[ -n "$F" ]] && ALL_STALE_FILES+=("WEBGUI|$F") done <<< "$CLEANUP_FIND_WEBGUI" # RootFS Directory CLEANUP_FIND_ROOTFS=$(find / -maxdepth 1 -name "*boot-backup*.zip" -type f 2>&1) if echo "$CLEANUP_FIND_ROOTFS" | grep -qiE "error|cannot|permission"; then LOG_TO_FILE "${MSG_WARNING} find in / during cleanup reported issues: $CLEANUP_FIND_ROOTFS" fi while IFS= read -r F; do [[ -n "$F" ]] && ALL_STALE_FILES+=("ROOTFS|$F") done <<< "$CLEANUP_FIND_ROOTFS" CLEANUP_TOTAL=${#ALL_STALE_FILES[@]} CLEANUP_SUCCESS=0 CLEANUP_FAILED=0 # No Stale Files — Skip Cleanup if [ $CLEANUP_TOTAL -eq 0 ]; then LOG_TO_FILE "${MSG_INFO} No Stale Files detected. Cleanup not required." if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "INFO" "No Stale Files detected. Cleanup not required." fi CLEANUP_EXECUTED=FALSE LOG_TO_FILE "---[${MOD_TITLE_8} SKIPPED - NO STALE FILES]---" LOG_TO_FILE "" # Continue to next Module (Notification) else # Stale Files exist — proceed with cleanup CLEANUP_EXECUTED=TRUE LOG_TO_FILE "${MSG_INFO} ${MSG_CLEANUP_STARTED}" LOG_TO_FILE "${MSG_INFO} Total files to process: ${CLEANUP_TOTAL}" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "INFO" "${MSG_CLEANUP_STARTED}" echo "Total Stale Files to process: ${CLEANUP_TOTAL}" fi # Attempt Removal DELETED_FILES=() FAILED_FILES=() for ENTRY in "${ALL_STALE_FILES[@]}"; do SOURCE_TYPE="${ENTRY%%|*}" FILEPATH="${ENTRY#*|}" if [[ -f "$FILEPATH" ]] || [[ -L "$FILEPATH" ]]; then # Capture rm Output for logging RM_OUTPUT=$(rm -f "$FILEPATH" 2>&1) DELETE_EXIT=$? if [ $DELETE_EXIT -eq 0 ]; then ((CLEANUP_SUCCESS++)) if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then case "$SOURCE_TYPE" in WEBGUI) echo " [SUCCESS] Removed: [WebGUI Temp] $(basename "$FILEPATH")" ;; ROOTFS) echo " [SUCCESS] Removed: [RootFS Temp] $(basename "$FILEPATH")" ;; esac fi LOG_TO_FILE "${MSG_SUCCESS} Removed: ${SOURCE_TYPE}|${FILEPATH}" DELETED_FILES+=("${FILEPATH}") else ((CLEANUP_FAILED++)) FAILED_FILES+=("${SOURCE_TYPE}|${FILEPATH}") LOG_TO_FILE "${MSG_ERROR} Failed to remove: ${SOURCE_TYPE}|${FILEPATH}" if [ -n "$RM_OUTPUT" ]; then LOG_TO_FILE "[DEBUG] rm output: $RM_OUTPUT" fi fi else # File already gone or inaccessible ((CLEANUP_FAILED++)) FAILED_FILES+=("${SOURCE_TYPE}|${FILEPATH}") LOG_TO_FILE "${MSG_WARNING} File inaccessible (already removed?): ${SOURCE_TYPE}|${FILEPATH}" fi done # Final Status determination if [ $CLEANUP_SUCCESS -gt 0 ] && [ $CLEANUP_FAILED -eq 0 ]; then # Perfect cleanup if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "SUCCESS" "All ${CLEANUP_SUCCESS} Stale Files removed successfully." fi LOG_TO_FILE "${MSG_SUCCESS} All ${CLEANUP_SUCCESS} Files removed successfully." elif [ $CLEANUP_SUCCESS -gt 0 ] && [ $CLEANUP_FAILED -gt 0 ]; then # Partial success — WARNING if [ "$OVERALL_STATUS" != "ERROR" ]; then OVERALL_STATUS="WARNING" fi if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "WARNING" "${CLEANUP_SUCCESS} removed, ${CLEANUP_FAILED} failed. Partial cleanup completed." fi LOG_TO_FILE "${MSG_WARNING} Partial cleanup: ${CLEANUP_SUCCESS} removed, ${CLEANUP_FAILED} failed" # List failed Files for user reference if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then echo " Failed Files (manual cleanup required):" for FAIL_ENTRY in "${FAILED_FILES[@]}"; do FILE_PATH="${FAIL_ENTRY#*|}" echo " ${FILE_PATH}" done fi else # Complete Failure if [ "$OVERALL_STATUS" != "ERROR" ]; then OVERALL_STATUS="ERROR" fi if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "ERROR" "Cleanup failed. No Files removed. Manual intervention required." fi LOG_TO_FILE "${MSG_ERROR} Cleanup failed. No Files removed. Manual intervention required." LOG_TO_FILE "${MSG_ERROR} ${CLEANUP_TOTAL} Stale Files remain. Manual cleanup required." # Send Error Notification immediately for cleanup failure SEND_ERROR_NOTIFICATION "Flash Backup Auto cleanup failed" \ "Flash Backup Auto cleanup" \ "Failed to remove ${CLEANUP_TOTAL} Stale Temporary Files. Manual cleanup required. See Log: $LOG_FILE_PATH" fi LOG_TO_FILE "---[${MOD_TITLE_8} COMPLETED]---" LOG_TO_FILE "Stats: ${CLEANUP_TOTAL} total, ${CLEANUP_SUCCESS} succeeded, ${CLEANUP_FAILED} failed" LOG_TO_FILE "" # NOTE: HAS_STALE_FILES remains TRUE to preserve Module 4's Original detection State fi else # Auto cleanup disabled LOG_TO_FILE "---[MODULE 8: ${MOD_TITLE_8}]---" LOG_TO_FILE "MODULE [DISABLED]" LOG_TO_FILE "${MSG_INFO} ${MSG_CLEANUP_SKIPPED}" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_SECTION "${MOD_TITLE_8}" echo "AUTO CLEANUP STATUS: DISABLED" PRINT_MSG "INFO" "${MSG_CLEANUP_SKIPPED}" fi CLEANUP_EXECUTED=FALSE CLEANUP_SUCCESS=0 CLEANUP_FAILED=0 CLEANUP_TOTAL=0 fi # --- MODULE 9: SEND NOTIFICATION -------------------------------------------- if [ "$ENABLE_NOTIFICATION" = "TRUE" ]; then LOG_TO_FILE "---[MODULE 9: ${MOD_TITLE_9}]---" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_SECTION "${MOD_TITLE_9}" fi # Initialize Notification Variables NOTIFY_TITLE="Flash or Boot Backup completed - ${HOST_NAME}" NOTIFY_MSG="Flash or Boot Backup '${BACKUP_FILE:-N/A}' successfully created." NOTIFY_PRIORITY="success" NOTIFY_SUBJECT="Flash or Boot Backup success" DETAIL_MSG="" # Condition 1: Invalid License Configuration if [ "$ENABLE_LICENSE_CHECK" = "TRUE" ] && [[ "$LICENSE_STATUS" == "INVALID_CONFIG" ]]; then NOTIFY_PRIORITY="error" NOTIFY_SUBJECT="Flash or Boot Backup - Error" DETAIL_MSG="${DETAIL_MSG:+${DETAIL_MSG} | }[ERROR] License Configuration invalid" fi # Condition 2: Missing USB License Device if [ "$ENABLE_HARDWARE_CHECK" = "TRUE" ] && [[ "$LICENSE_STATUS" == "USB_FLASH" ]]; then LSUSB_OUTPUT=$(lsusb 2>&1) || true if [[ $(echo "$LSUSB_OUTPUT" | grep -ciE "$USB_BRAND_PATTERN") -eq 0 ]]; then if echo "$LSUSB_OUTPUT" | grep -qiE "error|command not found"; then LOG_TO_FILE "${MSG_WARNING} lsusb reported issues during notification: $LSUSB_OUTPUT" fi NOTIFY_PRIORITY="error" NOTIFY_SUBJECT="Flash or Boot Backup - Error" DETAIL_MSG="${DETAIL_MSG:+${DETAIL_MSG} | }[ERROR] Required USB License Device not detected" fi fi # Condition 3: Stale Temporary Files (Warning if no Error) if [[ "${HAS_STALE_FILES:-FALSE}" == "TRUE" ]]; then if [[ "$NOTIFY_PRIORITY" != "error" ]]; then NOTIFY_PRIORITY="warning" NOTIFY_SUBJECT="Flash or Boot Backup - Warning" fi DETAIL_MSG="${DETAIL_MSG:+${DETAIL_MSG} | }[WARNING] Stale Temporary Files present" fi # Condition 4: Unknown Boot Medium (Warning if Success) if [ "$ENABLE_BOOT_CHECK" = "TRUE" ] && [[ "$BOOT_MEDIUM_TYPE" == "Unknown" ]]; then if [[ "$NOTIFY_PRIORITY" == "success" ]]; then NOTIFY_PRIORITY="warning" NOTIFY_SUBJECT="Flash or Boot Backup - Warning" fi DETAIL_MSG="${DETAIL_MSG:+${DETAIL_MSG} | }[WARNING] Boot Medium could not be identified" fi # Add License Information if [ "$ENABLE_LICENSE_CHECK" = "TRUE" ]; then case "$DEVICE_BINDING" in "USB Stick") DETAIL_MSG="${DETAIL_MSG:+${DETAIL_MSG} | }License: ${LICENSE_TYPE:-Unknown} (USB)" ;; "TPM") DETAIL_MSG="${DETAIL_MSG:+${DETAIL_MSG} | }License: ${LICENSE_TYPE:-Unknown} (TPM)" ;; esac fi # Add Boot Medium Information if [ "$ENABLE_BOOT_CHECK" = "TRUE" ] && [[ "$BOOT_MEDIUM_TYPE" != "Unknown" ]]; then DETAIL_MSG="${DETAIL_MSG:+${DETAIL_MSG} | }Boot: ${BOOT_MEDIUM_TYPE}" fi # Calculate Backup File Size BACKUP_SIZE_DISPLAY="N/A" if [ -f "$DEST_FILE" ]; then STAT_SIZE_OUTPUT=$(stat -c '%s' "$DEST_FILE" 2>&1) if echo "$STAT_SIZE_OUTPUT" | grep -qiE "error|cannot|no such"; then LOG_TO_FILE "${MSG_WARNING} stat for backup size failed: $STAT_SIZE_OUTPUT" BACKUP_SIZE_BYTES="" else BACKUP_SIZE_BYTES="$STAT_SIZE_OUTPUT" fi if [[ -n "$BACKUP_SIZE_BYTES" ]]; then BACKUP_SIZE_MB=$((BACKUP_SIZE_BYTES / 1024 / 1024)) BACKUP_SIZE_GB=$(awk "BEGIN {printf \"%.2f\", $BACKUP_SIZE_BYTES / 1024 / 1024 / 1024}") if [ "$BACKUP_SIZE_BYTES" -gt 1073741824 ]; then BACKUP_SIZE_DISPLAY="${BACKUP_SIZE_GB} GB" else BACKUP_SIZE_DISPLAY="${BACKUP_SIZE_MB} MB" fi if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then echo "Backup File Size: ${BACKUP_SIZE_DISPLAY}" fi LOG_TO_FILE "Backup File Size: ${BACKUP_SIZE_DISPLAY} / ${BACKUP_SIZE_BYTES} bytes" fi fi # ADD Final Cleanup Information if [ "$ENABLE_AUTO_CLEANUP" = "TRUE" ]; then if [[ "${CLEANUP_EXECUTED:-FALSE}" == "TRUE" ]]; then if [ $CLEANUP_FAILED -eq 0 ]; then DETAIL_MSG="${DETAIL_MSG:+${DETAIL_MSG} | }[SUCCESS] Cleaned ${CLEANUP_SUCCESS} Stale Files" elif [ $CLEANUP_SUCCESS -gt 0 ]; then DETAIL_MSG="${DETAIL_MSG:+${DETAIL_MSG} | }[PARTIAL] Cleaned ${CLEANUP_SUCCESS}/${CLEANUP_TOTAL} (${CLEANUP_FAILED} failed)" else DETAIL_MSG="${DETAIL_MSG:+${DETAIL_MSG} | }[FAILED] Auto cleanup unsuccessful" fi else DETAIL_MSG="${DETAIL_MSG:+${DETAIL_MSG} | }[NOT ACTIVE] No stale files to clean" fi fi DETAIL_MSG="${DETAIL_MSG:+${DETAIL_MSG} | }Size: ${BACKUP_SIZE_DISPLAY}" # Send Notification "$NOTIFICATION_SCRIPT" \ -e "$NOTIFY_TITLE" \ -s "$NOTIFY_SUBJECT" \ -d "${NOTIFY_MSG} - ${DETAIL_MSG}" \ -i "$NOTIFY_PRIORITY" LOG_TO_FILE "${MSG_INFO} Notification sent (Priority: ${NOTIFY_PRIORITY})." LOG_TO_FILE "---[${MOD_TITLE_9} COMPLETED]---" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_MSG "INFO" "Notification sent successfully (Priority: ${NOTIFY_PRIORITY})." fi else LOG_TO_FILE "---[MODULE 9: ${MOD_TITLE_9}]---" LOG_TO_FILE "MODULE [DISABLED]" if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then PRINT_SECTION "${MOD_TITLE_9} SKIPPED" fi fi # ============================================================================= # 8. FINALIZATION & STATUS REPORT # ============================================================================= # Determine Final Status Text based on OVERALL_STATUS # Mapping: Internal Status Value → Human-readable Messages and Symbols. # States: SUCCESS, WARNING, ERROR, DEFAULT (Catch-all Fallback). case "$OVERALL_STATUS" in SUCCESS) FINAL_MESSAGE="All Routine Steps completed successfully." FINAL_SYMBOL="[SUCCESS]" ;; WARNING) FINAL_MESSAGE="All Routine Steps completed. One or more Warnings occurred." FINAL_SYMBOL="[WARNING]" ;; ERROR) FINAL_MESSAGE="All Routine Steps completed. One or more Errors occurred." FINAL_SYMBOL="[ERROR]" ;; *) FINAL_MESSAGE="All Routine Steps completed." FINAL_SYMBOL="[INFO]" ;; esac # Final CLI Banner # Display Condition: SHOW_CLI_OUTPUT=TRUE required. # Timing: After all Modules complete. if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; then echo "" echo "==============================================================" # Visual differentiation based on OVERALL_STATUS if [[ "$OVERALL_STATUS" == "ERROR" ]]; then echo " *** ERROR - SCRIPT ABORTED ***" echo "==============================================================" echo "FINAL STATUS: ${FINAL_SYMBOL} ${FINAL_MESSAGE}" echo "──────────────────────────────────────────────────────────────" echo "WARNING: Manual intervention required immediately!" elif [[ "$OVERALL_STATUS" == "WARNING" ]]; then echo " *** COMPLETED WITH WARNINGS ***" echo "==============================================================" echo "FINAL STATUS: ${FINAL_SYMBOL} ${FINAL_MESSAGE}" echo "──────────────────────────────────────────────────────────────" echo "NOTE: Review Warnings in Log File below." else echo " *** BACKUP, ANALYSIS & MAINTENANCE COMPLETED ***" echo "==============================================================" echo "FINAL STATUS: ${FINAL_SYMBOL} ${FINAL_MESSAGE}" echo "──────────────────────────────────────────────────────────────" fi # Add Log Path hint if Warnings or Errors occurred if [[ "$OVERALL_STATUS" == "WARNING" ]] || [[ "$OVERALL_STATUS" == "ERROR" ]]; then echo "→ For Details, check Session Log: $LOG_FILE_PATH" echo "→ Or view Archive Logs: $ARCHIVE_LOG_DIR" fi echo "==============================================================" fi # Final Logging # Action: Completion Event Recorded to Log File. # Visual Display Covered by Final Banner Above. LOG_TO_FILE "FINAL STATUS: ${FINAL_SYMBOL} ${FINAL_MESSAGE}" # Final Archive Log Sync # Action: Local Session Log Copied to Archive Location. # Purpose: Permanent Record Established in Reports Directory. # NOTE: Cleanup details already logged in Module 8; Session Log preserves full history. LOG_TO_FILE "Final Archive Log Sync completed." cp "$LOCAL_LOG_PATH" "$ARCHIVE_LOG_PATH" # Normal Script Termination # Exit Code 0: Successful Completion (Warnings Permitted). exit 0Old Script#!/bin/bash############################################################################## Flash or Boot Backup, License & Boot Analysis# Description: Automates Flash or Boot Backup Creation, Retention, License & Boot Analysis############################################################################## =============================================================================# 1. CONFIGURATION & IDENTIFIERS# =============================================================================# -----------------------------------------------------------------------------# GENERAL SETTINGS# -----------------------------------------------------------------------------HOST_NAME=$(hostname)MIN_DISKS=3# -----------------------------------------------------------------------------# CENTRALIZED IDENTIFIER# -----------------------------------------------------------------------------SCRIPT_TITLE="Flash or Boot Backup, License & Boot Analysis"# File Naming ConventionsLOG_BASENAME="Boot_Backup_Analysis"LOG_FILE_PATTERN="${LOG_BASENAME}_*.log"# -----------------------------------------------------------------------------# RETENTION POLICIES# -----------------------------------------------------------------------------BACKUP_RETENTION_DAYS=150MAX_BACKUPS_TO_KEEP=15LOG_RETENTION_DAYS=90MAX_LOG_FILES_TO_KEEP=30# -----------------------------------------------------------------------------# FEATURE FLAGS# -----------------------------------------------------------------------------ENABLE_LICENSE_CHECK=trueENABLE_BOOT_CHECK=trueENABLE_HARDWARE_CHECK=trueENABLE_SYSTEM_CHECK=trueENABLE_BACKUP_CREATE=trueENABLE_BACKUP_RETENTION=trueENABLE_LOG_RETENTION=trueENABLE_NOTIFICATION=trueSHOW_CLI_OUTPUT=true# -----------------------------------------------------------------------------# PATH DEFINITIONS# -----------------------------------------------------------------------------BACKUP_BASE_DIR="/mnt/user/Backup_${HOST_NAME}/boot"ARCHIVE_SUBDIR="Backup_Reports"LOG_LOCAL_SUBDIR="Backup_Logs_on_Boot"ARCHIVE_LOG_DIR="${BACKUP_BASE_DIR}/${ARCHIVE_SUBDIR}"LOCAL_LOG_DIR="/boot/${LOG_LOCAL_SUBDIR}"LOG_FILE_NAME="${LOG_BASENAME}_${HOST_NAME}_$(date '+%Y-%m-%d_%H-%M-%S').log"LOCAL_LOG_PATH="${LOCAL_LOG_DIR}/${LOG_FILE_NAME}"ARCHIVE_LOG_PATH="${ARCHIVE_LOG_DIR}/${LOG_FILE_NAME}"LAST_RUN_LOG="${ARCHIVE_LOG_DIR}/Last_Run.log"LOG_FILE_PATH="$ARCHIVE_LOG_PATH"VAR_INI_PATH="/var/local/emhttp/var.ini"# -----------------------------------------------------------------------------# MODULE NAMES# -----------------------------------------------------------------------------MOD_TITLE_1="LICENSE STATUS"MOD_TITLE_2="BOOT MEDIUM ANALYSIS"MOD_TITLE_3="HARDWARE CHECK"MOD_TITLE_4="SYSTEM INTEGRITY CHECK"MOD_TITLE_5="CREATE BACKUP"MOD_TITLE_6="APPLY BACKUP RETENTION"MOD_TITLE_7="APPLY LOG RETENTION"MOD_TITLE_8="SEND NOTIFICATION"# -----------------------------------------------------------------------------# MESSAGE TEXTS# -----------------------------------------------------------------------------MSG_SUCCESS="SUCCESS"MSG_ERROR="ERROR"MSG_WARNING="WARNING"MSG_INFO="INFO"MSG_COPYING_BACKUP="Copying Backup to:"MSG_SUCCESS_COPY="Copy completed successfully."MSG_INTEGRITY_CHECK="Checking Integrity Check of"MSG_SUCCESS_INTEGRITY="Integrity Check passed."MSG_SYMLINK_REMOVED="Temporary Symlink removed."MSG_BACKUP_CREATED="Backup File created:"MSG_LOCATION="Location:"MSG_SUCCESS_ALL_STEPS="All Routine Steps completed successfully."MSG_BACKUP_ANALYSIS_MAINTENANCE_COMPLETED="Backup, Analysis & Maintenance completed."MSG_CONFIG_FILE_NOT_FOUND="Configuration File not found."MSG_VARINI_NOT_FOUND="var.ini not found."MSG_NO_TPM_DEVICE="No TPM Device available."MSG_USB_STICK_DETECTED="USB Stick detected for License."MSG_NO_USB_STICK="No USB Stick detected, but Flash License active!"MSG_TP_M_MODE="TPM Mode: No USB Stick required for License."MSG_UNZIP_NOT_FOUND="'unzip' Command not found. Integrity Check impossible."MSG_FLASH_BACKUP_FAILED="Flash Backup Tool failed."MSG_INVALID_FILENAME="Invalid Filename generated."MSG_SOURCE_FILE_MISSING="Source File does not exist."MSG_COPY_FAILED="Copying Backup File failed."MSG_BACKUP_CORRUPTED="Backup File corrupted! Removing defective File."MSG_CHANGE_DIR_FAILED="Change to Directory failed."MSG_PROCESS_ABORTED="Process aborted: Manual Cleanup required."MSG_WARNING_IGNORED="Warning ignored."MSG_NO_STALE_FILES="No stale temporary Files found."MSG_STALE_FILES_FOUND="stale temporary Files found."MSG_ARRAY_INTEGRITY_CONFIRMED="Array Integrity confirmed."MSG_NO_LICENSE_DETECTED="License Type not detected or invalid."# =============================================================================# 2. HELPER FUNCTIONS# =============================================================================print_header() {echo "=============================================================="echo " ${SCRIPT_TITLE}"echo "=============================================================="echo "HOST: $(hostname)"echo "TIMESTAMP: $(date '+%d.%m.%Y %H:%M')"echo "----------------------------------------"}print_section() {echo ""echo "[${1}]"echo "--------------------------------------------------------------"}print_msg() {local level="$1" msg="$2"case "$level" inSUCCESS) echo "[${MSG_SUCCESS}] $msg" ;;ERROR) echo "[${MSG_ERROR}] $msg" ;;WARNING) echo "[${MSG_WARNING}] $msg" ;;INFO) echo "[${MSG_INFO}] $msg" ;;*) echo "$msg" ;;esac}log_to_file() {local msg="$1"local timestamped_msg="[$(date '+%Y-%m-%d %H:%M:%S')] [$(hostname)] $msg"echo "$timestamped_msg" >> "$LOG_FILE_PATH"}mask_guid_format() {local guid="$1"if [[ -z "$guid" ]]; then echo "---"; return; filocal result="" sep=""IFS='-' read -ra PARTS <<< "$guid"for part in "${PARTS[@]}"; doif [[ ${#part} -gt 2 ]]; then result+="${sep}${part:0:2}-****"else result+="${sep}${part}-****"; fisep="-"doneecho "$result"}clean_vendor_name() {local raw_vendor="$1"local vendor=$(echo "$raw_vendor" | awk '{print $1}')if [[ "$vendor" == "Cruzer" ]]; then vendor=$(echo "$vendor" | sed 's/Cruzer$//g')elif [[ "$vendor" == "DataTraveler" ]]; then vendor=$(echo "$vendor" | sed 's/DataTraveler$//g')elif [[ "$vendor" == "Flash" ]]; then vendor=$(echo "$vendor" | sed 's/Flash$//g'); fiecho "$vendor" | xargs}clean_model_name() {local raw_model="$1"local cleaned=$(echo "$raw_model" | sed -E 's/[[:space:]]*[0-9]+\.?[0-9]*(GB|TB|MB|GiB|MiB)[[:space:]]*$//gi')if [[ -z "$cleaned" ]]; then echo "$raw_model"; else echo "$cleaned"; fi}# =============================================================================# 3. INITIALIZATION# =============================================================================mkdir -p "$BACKUP_BASE_DIR" || { echo "[${MSG_ERROR}] Cannot create Backup Directory."; exit 1; }mkdir -p "${ARCHIVE_LOG_DIR}" || { echo "[${MSG_ERROR}] Cannot create Archive Log Directory."; exit 1; }mkdir -p "${LOCAL_LOG_DIR}" || { echo "[${MSG_ERROR}] Cannot create Local Log Directory."; exit 1; }touch "$LOCAL_LOG_PATH" 2>/dev/null || exit 1touch "$ARCHIVE_LOG_PATH" 2>/dev/null || exit 1{echo "=========================================="echo "START ${SCRIPT_TITLE}"echo "Hostname: ${HOST_NAME}"echo "Backup Target: ${BACKUP_BASE_DIR}"echo "Local Log: ${LOCAL_LOG_PATH}"echo "Archive Log: ${ARCHIVE_LOG_PATH}"echo "=========================================="} > "$LOG_FILE_PATH"if [ "$SHOW_CLI_OUTPUT" = true ]; thenprint_headerfi# =============================================================================# 4. MODULES# =============================================================================# --- MODULE 1: LICENSE STATUS ---if [ "$ENABLE_LICENSE_CHECK" = true ]; thenlog_to_file "---[MODULE 1: ${MOD_TITLE_1}]---"if [ "$SHOW_CLI_OUTPUT" = true ]; then print_section "${MOD_TITLE_1}"; fiSERVER_NAME="" OS_VERSION="" LICENSE_TYPE="" REGTY_RAW=""FLASH_GUID="" TPM_GUID="" REG_GUID=""if [ -f "$VAR_INI_PATH" ]; thenSERVER_NAME=$(awk -F'=' '/^NAME=/ {gsub(/"/, "", $2); gsub(/ /, "_", $2); print $2}' "$VAR_INI_PATH")OS_VERSION=$(awk -F'=' '/^version=/ {gsub(/"/, "", $2); print $2}' "$VAR_INI_PATH")REGTY_RAW=$(awk -F'=' '/^regTy=/ {gsub(/"/, "", $2); print $2}' "$VAR_INI_PATH" 2>/dev/null)LICENSE_TYPE="$REGTY_RAW"FLASH_GUID=$(grep '^flashGUID=' "$VAR_INI_PATH" 2>/dev/null | cut -d'"' -f2 | tr -d '[:space:]')TPM_GUID=$(grep '^tpmGUID=' "$VAR_INI_PATH" 2>/dev/null | cut -d'"' -f2 | tr -d '[:space:]')REG_GUID=$(grep '^regGUID=' "$VAR_INI_PATH" 2>/dev/null | cut -d'"' -f2 | tr -d '[:space:]')elseprint_msg "$MSG_ERROR" "$MSG_CONFIG_FILE_NOT_FOUND"log_to_file "[${MSG_WARNING}] $MSG_VARINI_NOT_FOUND"fiLICENSE_STATUS="UNKNOWN" DEVICE_BINDING="NONE"if [[ "$FLASH_GUID" == "$REG_GUID" ]] && [[ "$FLASH_GUID" != "$TPM_GUID" || -z "$TPM_GUID" ]]; thenLICENSE_STATUS="USB_FLASH" DEVICE_BINDING="USB Stick"if [[ -n "$TPM_GUID" ]] && [[ "$FLASH_GUID" != "$TPM_GUID" ]]; thenif [ "$SHOW_CLI_OUTPUT" = true ]; then print_msg "$MSG_INFO" "TPM Key present, currently not used."; filog_to_file "[${MSG_INFO}] TPM Key present, currently not used."fielif [[ "$FLASH_GUID" "$TPM_GUID" ]] && [[ "$FLASH_GUID" "$REG_GUID" ]]; thenLICENSE_STATUS="FULL_TPM" DEVICE_BINDING="TPM"if [ "$SHOW_CLI_OUTPUT" = true ]; then print_msg "$MSG_SUCCESS" "System running on full TPM Basis."; filog_to_file "Binding detected: TPM."elif [[ "$REG_GUID" == "$TPM_GUID" ]]; thenLICENSE_STATUS="TPM_MIGRATION" DEVICE_BINDING="TPM"log_to_file "Binding detected: Migration to TPM."elseLICENSE_STATUS="INVALID_CONFIG" DEVICE_BINDING="UNKNOWN"log_to_file "[${MSG_WARNING}] GUID Configuration could not be uniquely identified."fiFLASH_MASKED=$(mask_guid_format "$FLASH_GUID")TPM_MASKED=$(mask_guid_format "$TPM_GUID")REG_MASKED=$(mask_guid_format "$REG_GUID")if [ "$SHOW_CLI_OUTPUT" = true ]; thenecho "USB LICENSE ID: $FLASH_MASKED"echo "TPM KEY: $TPM_MASKED"echo "ACTIVE REGISTRATION: $REG_MASKED"print_section "${MOD_TITLE_1}"echo "SERVER NAME: ${SERVER_NAME:-Unavailable}"echo "OS VERSION: ${OS_VERSION:-Unavailable}"echo "LICENSE TYPE: ${LICENSE_TYPE:-$MSG_NO_LICENSE_DETECTED}"echo "DETECTED BINDING: ${DEVICE_BINDING} (${LICENSE_STATUS})"echo "----------------------------------------"echo "DETECTED MODE: $LICENSE_STATUS"echo "LICENSE DEVICE TYPE: ${DEVICE_BINDING}"filog_to_file "Server Name: ${SERVER_NAME:-Unavailable}"log_to_file "OS Version: ${OS_VERSION:-Unavailable}"log_to_file "License Type: ${LICENSE_TYPE:-UNDETERMINED}"log_to_file "Detected Binding: ${DEVICE_BINDING} (${LICENSE_STATUS})"elselog_to_file "---[MODULE 1: ${MOD_TITLE_1}]---"log_to_file "[DISABLED]"fi# --- MODULE 2: BOOT MEDIUM ANALYSIS ---if [ "$ENABLE_BOOT_CHECK" = true ]; thenlog_to_file "---[MODULE 2: ${MOD_TITLE_2}]---"BOOT_MEDIUM_TYPE="UNKNOWN"system_dev_final="" sys_boot_type="Unknown" sys_vendor="" sys_model_clean=""nvme_dev_global=$(lsblk -d -no NAME,TYPE 2>/dev/null | grep 'nvme' | awk '{print $1}' | head -1)sda_dev_global=$(lsblk -d -no NAME,TYPE 2>/dev/null | grep -E '^sd[a-z]$' | awk '{print $1}' | head -1)mmc_dev_global=$(lsblk -d -no NAME,TYPE 2>/dev/null | grep 'mmc' | awk '{print $1}' | head -1)if [ -e "/dev/disk/by-label/flash" ]; thenREAL_LINK=$(readlink -f "/dev/disk/by-label/flash")DEVICE_NAME=$(basename "$REAL_LINK")if [[ "$DEVICE_NAME" =~ [0-9]+$ ]]; then PARENT_DEVICE="${DEVICE_NAME%[0-9]*}"; else PARENT_DEVICE="$DEVICE_NAME"; fiTRANSPORT=$(lsblk -no TRAN "$PARENT_DEVICE" 2>/dev/null | tr -d '[:space:]')case "$TRANSPORT" inusb) BOOT_MEDIUM_TYPE="USB Stick Boot" ;;*)SYS_PATH="/sys/block/$PARENT_DEVICE/device"if [ -L "$SYS_PATH" ]; thenFULL_PATH=$(readlink -f "$SYS_PATH")if [[ "$FULL_PATH" =~ usb ]]; thenBOOT_MEDIUM_TYPE="USB Stick Boot"elseBOOT_MEDIUM_TYPE="Internal Boot"fielseBOOT_MEDIUM_TYPE="Internal Boot"fi;;esacif [[ "$BOOT_MEDIUM_TYPE" == "Internal Boot" ]]; thenif [[ -n "$nvme_dev_global" ]]; then system_dev_final="$nvme_dev_global"elif [[ -n "$sda_dev_global" ]]; then system_dev_final="$sda_dev_global"elif [[ -n "$mmc_dev_global" ]]; then system_dev_final="$mmc_dev_global"; fisys_boot_type="Internal Boot"if [[ -n "$system_dev_final" ]] && [[ -b "/dev/$system_dev_final" ]]; thenvendor_raw=$(lsblk -no VENDOR "/dev/$system_dev_final" 2>/dev/null | head -1 | xargs)model_raw=$(lsblk -no MODEL "/dev/$system_dev_final" 2>/dev/null | head -1 | xargs)if [[ -z "$vendor_raw" || "$vendor_raw" == "" ]]; then sys_vendor="(see Model)"else sys_vendor="$vendor_raw"; fisys_model_clean=$(clean_model_name "$model_raw")fifilog_to_file "Boot Medium Type: ${BOOT_MEDIUM_TYPE}"elselog_to_file "[${MSG_WARNING}] /dev/disk/by-label/flash not found."fiif [ "$SHOW_CLI_OUTPUT" = true ]; thenecho "BOOT MEDIUM TYPE: ${BOOT_MEDIUM_TYPE}"echo "----------------------------------------"print_section "${MOD_TITLE_2}"lic_vendor="-" lic_model_clean="-" lic_type=""if [[ "$DEVICE_BINDING" == "USB Stick" ]]; thenusb_line=$(lsusb 2>/dev/null | grep -iE 'Kingston|Sandisk|Lexar|Transcend|ADATA|SanDisk' | head -1)if [[ -n "$usb_line" ]]; thenraw_full=$(echo "$usb_line" | sed 's/.*ID [0-9a-f:]* //' | sed 's/ Corp\. //g' | sed 's/ Inc\. //g' | xargs)lic_vendor=$(echo "$raw_full" | awk '{print $1}')lic_vendor=$(clean_vendor_name "$lic_vendor")if [[ "$lic_vendor" == "$raw_full" ]]; then lic_model_clean="Unknown USB Device"else mod_part=$(echo "$raw_full" | sed "s/^${lic_vendor}[[:space:]]*//"); lic_model_clean=$(clean_model_name "$mod_part"); fifi[[ -z "$lic_vendor" ]] && lic_vendor="Unknown"[[ -z "$lic_model_clean" ]] && lic_model_clean="Unknown"lic_type="USB Stick"elselic_type="TPM"fiecho "LICENSE DEVICE:"echo " TYPE: $lic_type"echo " MANUFACTURER: $lic_vendor"echo " MODEL: $lic_model_clean"echo ""echo "SYSTEM DEVICE (OS):"echo " TYPE: $sys_boot_type"echo " MANUFACTURER: $sys_vendor"echo " MODEL: $sys_model_clean"fielselog_to_file "---[MODULE 2: ${MOD_TITLE_2}]---"log_to_file "[DISABLED]"fi# --- MODULE 3: HARDWARE CHECK ---if [ "$ENABLE_HARDWARE_CHECK" = true ]; thenif [ "$SHOW_CLI_OUTPUT" = true ]; then print_section "${MOD_TITLE_3}"; filog_to_file "---[MODULE 3: ${MOD_TITLE_3}]---"if [ -c /dev/tpm0 ] 2>/dev/null || [ -c /dev/tpmrm0 ] 2>/dev/null; thenif [ "$SHOW_CLI_OUTPUT" = true ]; then print_msg "$MSG_SUCCESS" "TPM Hardware detected and usable."; filog_to_file "[${MSG_SUCCESS}] TPM Hardware detected."elseif [ "$SHOW_CLI_OUTPUT" = true ]; then print_msg "$MSG_WARNING" "$MSG_NO_TPM_DEVICE"; filog_to_file "[${MSG_WARNING}] $MSG_NO_TPM_DEVICE"fiif [[ "$LICENSE_STATUS" == "USB_FLASH" ]]; thenif [[ $(lsusb 2>/dev/null | grep -ciE 'Kingston|Sandisk|Lexar') -eq 0 ]]; thenif [ "$SHOW_CLI_OUTPUT" = true ]; then print_msg "$MSG_ERROR" "$MSG_NO_USB_STICK"; filog_to_file "[${MSG_ERROR}] $MSG_NO_USB_STICK"elseif [ "$SHOW_CLI_OUTPUT" = true ]; then print_msg "$MSG_INFO" "$MSG_USB_STICK_DETECTED"; filog_to_file "[${MSG_INFO}] $MSG_USB_STICK_DETECTED"fielseif [ "$SHOW_CLI_OUTPUT" = true ]; then print_msg "$MSG_INFO" "$MSG_TP_M_MODE"; filog_to_file "[${MSG_INFO}] $MSG_TP_M_MODE"filog_to_file "---[${MOD_TITLE_3} COMPLETED]---"log_to_file ""elselog_to_file "---[MODULE 3: ${MOD_TITLE_3}]---"log_to_file "[DISABLED]"fi# --- MODULE 4: SYSTEM INTEGRITY CHECK ---if [ "$ENABLE_SYSTEM_CHECK" = true ]; thenlog_to_file "---[MODULE 4: ${MOD_TITLE_4}]---"STALE_COUNT=$(find /usr/local/emhttp/ -maxdepth 1 -name "*boot-backup*.zip" \( -type f -o -xtype l \) 2>/dev/null | wc -l)if [ "$STALE_COUNT" -gt 0 ]; thenlog_to_file "[${MSG_WARNING}] ${STALE_COUNT} $MSG_STALE_FILES_FOUND"HAS_STALE_FILES=trueif [ -z "$SKIP_DIRTY_CHECK" ] || [ "$SKIP_DIRTY_CHECK" = false ]; thenlog_to_file "[${MSG_ERROR}] $MSG_PROCESS_ABORTED"exit 1elselog_to_file "[${MSG_INFO}] $MSG_WARNING_IGNORED (SKIP_DIRTY_CHECK=true)."fielseHAS_STALE_FILES=falselog_to_file "[${MSG_SUCCESS}] $MSG_NO_STALE_FILES"fiif [ "$MIN_DISKS" -gt 0 ]; thenDISK_COUNT=$(ls -d /mnt/disk* 2>/dev/null | wc -l)if [ "$DISK_COUNT" -lt "$MIN_DISKS" ]; thenlog_to_file "[${MSG_ERROR}] Only ${DISK_COUNT} Disks found. Minimum ${MIN_DISKS} required."exit 1filog_to_file "[${MSG_SUCCESS}] $MSG_ARRAY_INTEGRITY_CONFIRMED (${DISK_COUNT} Disks present)."filog_to_file "---[${MOD_TITLE_4} COMPLETED]---"log_to_file ""elselog_to_file "---[MODULE 4: ${MOD_TITLE_4}]---"log_to_file "[DISABLED]"fi# --- MODULE 5: CREATE BACKUP ---BACKUP_FILE=""if [ "$ENABLE_BACKUP_CREATE" = true ]; thenlog_to_file "---[MODULE 5: ${MOD_TITLE_5}]---"log_to_file "Target Directory: ${BACKUP_BASE_DIR}"log_to_file "Starting native Flash Backup Script..."if ! command -v unzip >/dev/null 2>&1; thenlog_to_file "[${MSG_ERROR}] $MSG_UNZIP_NOT_FOUND"exit 1fiBACKUP_RAW_OUTPUT=$(/usr/bin/php -q /usr/local/emhttp/webGui/scripts/flash_backup 2>&1)PHP_EXIT_CODE=$?if [ $PHP_EXIT_CODE -ne 0 ]; thenlog_to_file "[${MSG_ERROR}] $MSG_FLASH_BACKUP_FAILED (Exit Code: ${PHP_EXIT_CODE})."log_to_file "[DEBUG] PHP Output: ${BACKUP_RAW_OUTPUT}"exit 1fiBACKUP_FILE=$(echo "$BACKUP_RAW_OUTPUT" | tail -n 1 | xargs)if [ -z "$BACKUP_FILE" ]; thenlog_to_file "[${MSG_ERROR}] Invalid Filename generated (Empty output)."log_to_file "[DEBUG] PHP Output: ${BACKUP_RAW_OUTPUT}"exit 1fiSYMLINK_PATH="/usr/local/emhttp/${BACKUP_FILE}"if [ ! -f "$SYMLINK_PATH" ] && [ ! -L "$SYMLINK_PATH" ]; thenlog_to_file "[${MSG_ERROR}] Source File does not exist: ${SYMLINK_PATH}"exit 1fiSOURCE_FILE=$(readlink -f "$SYMLINK_PATH" 2>/dev/null || echo "$SYMLINK_PATH")DEST_FILE="${BACKUP_BASE_DIR}/${BACKUP_FILE}"if [ ! -f "$SOURCE_FILE" ]; thenlog_to_file "[${MSG_ERROR}] Resolved Source File missing: ${SOURCE_FILE}"exit 1fiif [ "$SHOW_CLI_OUTPUT" = true ]; thenecho "[${MSG_INFO}] ${MSG_COPYING_BACKUP} ${DEST_FILE}"ficp -v "$SOURCE_FILE" "$DEST_FILE" > /dev/null 2>&1COPY_EXIT=$?if [ $COPY_EXIT -ne 0 ]; thenlog_to_file "[${MSG_ERROR}] $MSG_COPY_FAILED"if [ "$SHOW_CLI_OUTPUT" = true ]; then print_msg "$MSG_ERROR" "$MSG_COPY_FAILED"; fiexit 1fiif [ "$SHOW_CLI_OUTPUT" = true ]; thenecho "[${MSG_SUCCESS}] ${MSG_SUCCESS_COPY}"filog_to_file "Copied Backup to: ${DEST_FILE}"if [ "$SHOW_CLI_OUTPUT" = true ]; thenecho "[${MSG_INFO}] ${MSG_INTEGRITY_CHECK} ${BACKUP_FILE}..."fiif ! unzip -t -q "$DEST_FILE" >/dev/null 2>&1; thenlog_to_file "[${MSG_ERROR}] $MSG_BACKUP_CORRUPTED"rm -f "$DEST_FILE"if [ "$SHOW_CLI_OUTPUT" = true ]; then print_msg "$MSG_ERROR" "$MSG_BACKUP_CORRUPTED"; fiexit 1filog_to_file "[${MSG_SUCCESS}] ${MSG_SUCCESS_INTEGRITY}"if [ "$SHOW_CLI_OUTPUT" = true ]; thenecho "[${MSG_SUCCESS}] ${MSG_SUCCESS_INTEGRITY}"fiif [ -L "$SYMLINK_PATH" ]; thenrm -f "$SYMLINK_PATH"log_to_file "[${MSG_SUCCESS}] ${MSG_SYMLINK_REMOVED}"if [ "$SHOW_CLI_OUTPUT" = true ]; thenecho "[${MSG_SUCCESS}] ${MSG_SYMLINK_REMOVED}"fifilog_to_file "---[BACKUP CREATION COMPLETED]---"log_to_file ""if [ "$SHOW_CLI_OUTPUT" = true ]; thenprint_section "${MOD_TITLE_5} COMPLETED"echo "${MSG_BACKUP_CREATED} ${BACKUP_FILE}"echo "${MSG_LOCATION} ${DEST_FILE}"filog_to_file "---[UPDATE ARCHIVE LOG]---"{echo "=== LAST RUN: ${HOST_NAME} ==="echo "Timestamp: $(date '+%Y-%m-%d %H:%M:%S')"echo "Backup File: ${BACKUP_FILE}"echo "Backup Path: ${DEST_FILE}"echo "Archive Log Path: ${ARCHIVE_LOG_PATH}"echo "Status: SUCCESSFUL"[ -n "$LICENSE_TYPE" ] && echo "License Type: ${LICENSE_TYPE}"[ "${HAS_STALE_FILES:-false}" = true ] && echo "WARNING: Stale Files present"echo "==============================="} > "$LAST_RUN_LOG"cp "$LOCAL_LOG_PATH" "$ARCHIVE_LOG_PATH"log_to_file "Status Log saved: ${LAST_RUN_LOG}"log_to_file "Session Log archived: ${ARCHIVE_LOG_PATH}"log_to_file "---[ARCHIVE LOG UPDATED]---"if [ "$SHOW_CLI_OUTPUT" = true ]; thenprint_section "ARCHIVE LOG UPDATED"echo "Log saved to: ${LAST_RUN_LOG}"fielselog_to_file "---[MODULE 5: ${MOD_TITLE_5}]---"log_to_file "[DISABLED]"if [ "$SHOW_CLI_OUTPUT" = true ]; thenprint_section "${MOD_TITLE_5} SKIPPED"fifi# --- MODULE 6: APPLY BACKUP RETENTION ---if [ "$ENABLE_BACKUP_RETENTION" = true ]; thenlog_to_file "---[MODULE 6: ${MOD_TITLE_6}]---"cd "$BACKUP_BASE_DIR" || { log_to_file "[${MSG_ERROR}] $MSG_CHANGE_DIR_FAILED"; exit 1; }DELETED_BY_AGE=$(find . -maxdepth 1 -name "*boot-backup*.zip" -type f -mtime +"${BACKUP_RETENTION_DAYS}" -print 2>/dev/null | wc -l)if [ "$DELETED_BY_AGE" -gt 0 ]; thenlog_to_file "[${MSG_INFO}] ${DELETED_BY_AGE} Backups older than ${BACKUP_RETENTION_DAYS} days removed."find . -maxdepth 1 -name "*boot-backup*.zip" -type f -mtime +"${BACKUP_RETENTION_DAYS}" -delete 2>/dev/nullif [ "$SHOW_CLI_OUTPUT" = true ]; thenecho "[${MSG_INFO}] Removed ${DELETED_BY_AGE} old Backups (older than ${BACKUP_RETENTION_DAYS} days)."fifiif [ -n "$MAX_BACKUPS_TO_KEEP" ] && [ "$MAX_BACKUPS_TO_KEEP" -gt 0 ]; thenCURRENT_COUNT=$(ls -1 boot-backup.zip 2>/dev/null | wc -l)if [ "$CURRENT_COUNT" -gt "$MAX_BACKUPS_TO_KEEP" ]; thenTO_DELETE=$((CURRENT_COUNT - MAX_BACKUPS_TO_KEEP))log_to_file "[${MSG_INFO}] Current Backups: ${CURRENT_COUNT} | Max: ${MAX_BACKUPS_TO_KEEP} → Deleting ${TO_DELETE} Files."ls -t boot-backup.zip 2>/dev/null | tail -n +$((MAX_BACKUPS_TO_KEEP + 1)) | while read -r old_backup; dorm -f "$old_backup"log_to_file "[${MSG_INFO}] Deleted: $(basename "$old_backup")"doneif [ "$SHOW_CLI_OUTPUT" = true ]; thenecho "[${MSG_INFO}] Deleted ${TO_DELETE} old Backups to stay within Limit of ${MAX_BACKUPS_TO_KEEP}."fielseif [ "$SHOW_CLI_OUTPUT" = true ]; thenecho "[${MSG_INFO}] Backup Count (${CURRENT_COUNT}) is within Limit (${MAX_BACKUPS_TO_KEEP})."fififiFINAL_COUNT=$(ls -1 boot-backup.zip 2>/dev/null | wc -l)log_to_file "[${MSG_INFO}] Result: ${FINAL_COUNT} Flash or Boot Backups remaining."log_to_file "---[${MOD_TITLE_6} COMPLETED]---"if [ "$SHOW_CLI_OUTPUT" = true ]; thenprint_section "${MOD_TITLE_6} COMPLETED"echo "Total Backups remaining: ${FINAL_COUNT}"fielselog_to_file "---[MODULE 6: ${MOD_TITLE_6}]---"log_to_file "[DISABLED]"if [ "$SHOW_CLI_OUTPUT" = true ]; thenprint_section "${MOD_TITLE_6} SKIPPED"fifi# --- MODULE 7: APPLY LOG RETENTION ---if [ "$ENABLE_LOG_RETENTION" = true ]; thenlog_to_file "---[MODULE 7: ${MOD_TITLE_7}]---"cd "$ARCHIVE_LOG_DIR" || { log_to_file "[${MSG_ERROR}] $MSG_CHANGE_DIR_FAILED"; exit 1; }DELETED_LOGS_BY_AGE=$(find . -maxdepth 1 -name "*.log" -type f ! -name "last_run.log" -mtime +"${LOG_RETENTION_DAYS}" -print 2>/dev/null | wc -l)if [ "$DELETED_LOGS_BY_AGE" -gt 0 ]; thenlog_to_file "[${MSG_INFO}] ${DELETED_LOGS_BY_AGE} Log Files older than ${LOG_RETENTION_DAYS} Days removed."find . -maxdepth 1 -name "*.log" -type f ! -name "last_run.log" -mtime +"${LOG_RETENTION_DAYS}" -delete 2>/dev/nullif [ "$SHOW_CLI_OUTPUT" = true ]; thenecho "[${MSG_INFO}] Removed ${DELETED_LOGS_BY_AGE} old Logs (older than ${LOG_RETENTION_DAYS} Days)."fifiif [ -n "$MAX_LOG_FILES_TO_KEEP" ] && [ "$MAX_LOG_FILES_TO_KEEP" -gt 0 ]; thenLOG_FILE_COUNT=$(find . -maxdepth 1 -name "${LOG_FILE_PATTERN}" -type f 2>/dev/null | wc -l)if [ "$LOG_FILE_COUNT" -gt "$MAX_LOG_FILES_TO_KEEP" ]; thenTO_DELETE_COUNT=$((LOG_FILE_COUNT - MAX_LOG_FILES_TO_KEEP))log_to_file "[${MSG_INFO}] Current Logs: ${LOG_FILE_COUNT} | Max: ${MAX_LOG_FILES_TO_KEEP} → Deleting ${TO_DELETE_COUNT} Files."mapfile -t files_to_delete < <(for f in $(find . -maxdepth 1 -name "${LOG_FILE_PATTERN}" -type f); dots=$(stat -c '%Y' "$f" 2>/dev/null || stat -f '%m' "$f" 2>/dev/null)printf '%s %s\n' "$ts" "$f"done | sort -n | head -n "$TO_DELETE_COUNT" | awk '{print $2}')for old_log in "${files_to_delete[@]}"; doif [ -n "$old_log" ]; thenrm -f "$old_log"log_to_file "[${MSG_INFO}] Deleted: $(basename "$old_log")"fidoneif [ "$SHOW_CLI_OUTPUT" = true ]; thenecho "[${MSG_INFO}] Deleted ${TO_DELETE_COUNT} old Logs to stay within Limit of ${MAX_LOG_FILES_TO_KEEP}."fielseif [ "$SHOW_CLI_OUTPUT" = true ]; thenecho "[${MSG_INFO}] Log Count (${LOG_FILE_COUNT}) is within Limit (${MAX_LOG_FILES_TO_KEEP})."fififiFINAL_LOG_COUNT=$(find . -maxdepth 1 -name "${LOG_FILE_PATTERN}" -type f | wc -l)log_to_file "[${MSG_INFO}] Result: ${FINAL_LOG_COUNT} Log Files remaining (excl. last_run.log)."log_to_file "---[${MOD_TITLE_7} COMPLETED]---"if [ "$SHOW_CLI_OUTPUT" = true ]; thenprint_section "${MOD_TITLE_7} COMPLETED"echo "Total Logs remaining: ${FINAL_LOG_COUNT}"fielselog_to_file "---[MODULE 7: ${MOD_TITLE_7}]---"log_to_file "[DISABLED]"if [ "$SHOW_CLI_OUTPUT" = true ]; thenprint_section "${MOD_TITLE_7} SKIPPED"fifi# --- MODULE 8: SEND NOTIFICATION ---if [ "$ENABLE_NOTIFICATION" = true ]; thenlog_to_file "---[MODULE 8: ${MOD_TITLE_8}]---"NOTIFY_TITLE="Flash or Boot Backup Completed - ${HOST_NAME}"NOTIFY_MSG="Flash or Boot Backup '${BACKUP_FILE}' successfully created."DETAIL_MSG=""if [ "$ENABLE_LICENSE_CHECK" = true ]; thencase "$DEVICE_BINDING" in"USB Stick") DETAIL_MSG="License: ${LICENSE_TYPE:-Unknown} (USB)" ;;"TPM") DETAIL_MSG="License: ${LICENSE_TYPE:-Unknown} (TPM)" ;;esacfiif [ "$ENABLE_BOOT_CHECK" = true ] && [ "$BOOT_MEDIUM_TYPE" != "UNKNOWN" ]; thenDETAIL_MSG="${DETAIL_MSG:+${DETAIL_MSG} | }Boot: ${BOOT_MEDIUM_TYPE}"fiif [ "${HAS_STALE_FILES:-false}" = true ]; thenDETAIL_MSG="${DETAIL_MSG:+${DETAIL_MSG} | }WARNING: Stale Files"fi/usr/local/emhttp/webGui/scripts/notify -e "$NOTIFY_TITLE" \-s "Flash or Boot Backup Success" \-d "${NOTIFY_MSG} - ${DETAIL_MSG}" \-i "normal"log_to_file "[${MSG_INFO}] Notification sent."log_to_file "---[${MOD_TITLE_8} COMPLETED]---"if [ "$SHOW_CLI_OUTPUT" = true ]; thenprint_section "${MOD_TITLE_8} SENT"echo "Notification sent successfully."fielselog_to_file "---[MODULE 8: ${MOD_TITLE_8}]---"log_to_file "[DISABLED]"if [ "$SHOW_CLI_OUTPUT" = true ]; thenprint_section "${MOD_TITLE_8} SKIPPED"fifi# Final Outputif [ "$SHOW_CLI_OUTPUT" = true ]; thenecho ""echo "=============================================================="echo " ${MSG_BACKUP_ANALYSIS_MAINTENANCE_COMPLETED}"echo "=============================================================="filog_to_file "[${MSG_SUCCESS}] ${MSG_SUCCESS_ALL_STEPS}"if [ "$SHOW_CLI_OUTPUT" = true ]; thenprint_msg "$MSG_SUCCESS" "${MSG_SUCCESS_ALL_STEPS}"fiexit 0 Edited Friday at 11:42 PM4 days by Revan335
July 27Jul 27 On 6/26/2026 at 3:26 PM, Revan335 said:The expanded and corrected versions for V7.3.x and the changes to ‘boot’, ‘Internal Boot’, … instead of ‘flash’. Older operating system versions should also work, but have not really been tested as there are currently no older versions, such as V6.x available.Trying to figure out where to enter my UD remote destination?
July 28Jul 28 5 hours ago, wgstarks said:Trying to figure out where to enter my UD remote destination?What do you mean?The Backup or Log Destination are declared at the PATH DEFINITIONS Config Arrier at the Beginning.
July 28Jul 28 22 minutes ago, Revan335 said:What do you mean?The Backup or Log Destination are declared at the PATH DEFINITIONS Config Arrier at the Beginning.Is that BACKUP_BASE_DIR=?And I’m guessing I need to replace everything after the =?
July 28Jul 28 3 hours ago, wgstarks said:Is that BACKUP_BASE_DIR=?And I’m guessing I need to replace everything after the =?Yes!BACKUP_BASE_DIR="/mnt/user/Backup_${HOST_NAME}/boot"For example "/mnt/disk/WD_Elements/Backup_${HOST_NAME}/boot" Hostname is your Unraid Server Name like Tower ... but you can used a Path what you like with or without the Variable or this Schema.
Saturday at 01:16 AM4 days 1 hour ago, Revan335 said:@wgstarks The New Script Version have more Documentation.Switched to the new version. I got an email notification on the first run but not on repeat runs. The only difference I see is that the script aborted on the first run due to stale files so I'm wondering if the script only notifies on an abort and how to change it to always notify?
Sunday at 06:56 PM2 days On 8/15/2026 at 3:16 AM, wgstarks said:Switched to the new version. I got an email notification on the first run but not on repeat runs. The only difference I see is that the script aborted on the first run due to stale files so I'm wondering if the script only notifies on an abort and how to change it to always notify?When you have Stale Files or other Errors and Warnings you have Details in the Log Files.You mean the E-Mail Notification?
Sunday at 07:21 PM2 days 20 minutes ago, Revan335 said:When you have Stale Files or other Errors and Warnings you have Details in the Log Files.You mean the E-Mail Notification?Yes. I got an email notification with the first run (which aborted due to stale files) but not with subsequent runs with cleanup enabled. They completed successfully but I didn’t get an email. These were all being run using the run script button in the user scripts plugin.
Monday at 07:36 PM1 day On 8/16/2026 at 9:21 PM, wgstarks said:Yes. I got an email notification with the first run (which aborted due to stale files) but not with subsequent runs with cleanup enabled. They completed successfully but I didn’t get an email. These were all being run using the run script button in the user scripts plugin.Can you send the Log and CLI Output?PN is a Option when its not published for all.
Monday at 08:55 PM1 day 1 hour ago, Revan335 said:Can you send the Log and CLI Output?PN is a Option when its not published for all.log-[INFO] Temporary Symlink Location: /usr/local/emhttp/brunnhildetwo-v7.3.2-boot-backup-20260817-1635.zip[SUCCESS] Temporary Symlink removed.[UPDATE ARCHIVE LOG]--------------------------------------------------------------Status Log saved: /mnt/remotes/WALTERS-M4-MINI.DAHONEY.ME_brunnhilde/boot_backup//Backup_Reports/Last_Run.logSession Log archived: /mnt/remotes/WALTERS-M4-MINI.DAHONEY.ME_brunnhilde/boot_backup//Backup_Reports/Boot_Backup_Analysis__2026-08-17_16-35-41.log[APPLY BACKUP RETENTION]--------------------------------------------------------------[INFO] Backup File Count (7) is within limit (15).[INFO] Total Backup Files remaining: 7[APPLY LOG RETENTION]--------------------------------------------------------------[INFO] Log File Count (8) is within limit (15).[INFO] Total Log Files remaining: 8[AUTO CLEANUP]--------------------------------------------------------------AUTO CLEANUP STATUS: ENABLEDSTALE FILES FROM SYSTEM INTEGRITY CHECK: FALSE----------------------------------------[INFO] No Stale Files detected. Cleanup not required.[SEND NOTIFICATION]--------------------------------------------------------------Backup File Size: 1.99 GB[INFO] Notification sent successfully (Priority: success).==============================================================*** BACKUP, ANALYSIS & MAINTENANCE COMPLETED ***==============================================================FINAL STATUS: [SUCCESS] All Routine Steps completed successfully.──────────────────────────────────────────────────────────────==============================================================Script Finished Aug 17, 2026 16:36.53Full logs for this script are available at /tmp/user.scripts/tmpScripts/boot backup/log.txtCLI-Script location: /tmp/user.scripts/tmpScripts/boot backup/scriptNote that closing this window will abort the execution of this script==============================================================Flash Boot Backup License Boot Analysis==============================================================HOST: BrunnhildeTwoTIMESTAMP: 17.08.2026 16:45----------------------------------------[LICENSE STATUS]--------------------------------------------------------------[SUCCESS] TPM Key used for Device Binding and TPM Licensing is active.USB LICENSE ID: 01-ENHAW*******************TPM KEY: 01-ENHAW*******************ACTIVE REGISTRATION: 01-ENHAW*******************----------------------------------------SERVER NAME: BrunnhildeTwoOS VERSION: 7.3.2LICENSE TYPE: LifetimeDETECTED BINDING: TPM (FULL_TPM)----------------------------------------DETECTED MODE: FULL_TPMLICENSE DEVICE TYPE: TPM[BOOT MEDIUM ANALYSIS]--------------------------------------------------------------BOOT MEDIUM TYPE: Internal BootLICENSE DEVICETYPE: TPMMANUFACTURER: -MODEL: -SYSTEM DEVICE (OS)TYPE: Internal BootMANUFACTURER: (see Model)MODEL: Samsung SSD 990 PRO[HARDWARE CHECK]--------------------------------------------------------------[SUCCESS] TPM Hardware detected.[INFO] TPM Mode: No USB Stick required for License.[SYSTEM INTEGRITY CHECK]--------------------------------------------------------------DIRTY CHECK OVERRIDE: TRUEAUTO CLEANUP: TRUE----------------------------------------[SUCCESS] Array Integrity confirmed. (10 Disks present)[SUCCESS] No Stale Temporary Files found.[CREATE BACKUP]--------------------------------------------------------------Target Directory: /mnt/remotes/WALTERS-M4-MINI.DAHONEY.ME_brunnhilde/boot_backup/Starting Native Flash Backup Script...[SUCCESS] PHP flash_backup Script executed and Symlink at /usr/local/emhttp/brunnhildetwo-v7.3.2-boot-backup-20260817-1645.zip created.[INFO] Copying Backup File to: /mnt/remotes/WALTERS-M4-MINI.DAHONEY.ME_brunnhilde/boot_backup//brunnhildetwo-v7.3.2-boot-backup-20260817-1645.zip[SUCCESS] Copy completed successfully.[INFO] Checking Integrity of File: /mnt/remotes/WALTERS-M4-MINI.DAHONEY.ME_brunnhilde/boot_backup//brunnhildetwo-v7.3.2-boot-backup-20260817-1645.zip[SUCCESS] Integrity Check passed.[INFO] Temporary Backup File Location: /brunnhildetwo-v7.3.2-boot-backup-20260817-1645.zip[SUCCESS] Temporary Backup File removed: brunnhildetwo-v7.3.2-boot-backup-20260817-1645.zip[INFO] Temporary Symlink Location: /usr/local/emhttp/brunnhildetwo-v7.3.2-boot-backup-20260817-1645.zip[SUCCESS] Temporary Symlink removed.[UPDATE ARCHIVE LOG]--------------------------------------------------------------Status Log saved: /mnt/remotes/WALTERS-M4-MINI.DAHONEY.ME_brunnhilde/boot_backup//Backup_Reports/Last_Run.logSession Log archived: /mnt/remotes/WALTERS-M4-MINI.DAHONEY.ME_brunnhilde/boot_backup//Backup_Reports/Boot_Backup_Analysis_BrunnhildeTwo_2026-08-17_16-45-17.log[APPLY BACKUP RETENTION]--------------------------------------------------------------[INFO] Backup File Count (9) is within limit (15).[INFO] Total Backup Files remaining: 9[APPLY LOG RETENTION]--------------------------------------------------------------[INFO] Log File Count (10) is within limit (15).[INFO] Total Log Files remaining: 10[AUTO CLEANUP]--------------------------------------------------------------AUTO CLEANUP STATUS: ENABLEDSTALE FILES FROM SYSTEM INTEGRITY CHECK: FALSE----------------------------------------[INFO] No Stale Files detected. Cleanup not required.[SEND NOTIFICATION]--------------------------------------------------------------Backup File Size: 1.99 GB[INFO] Notification sent successfully (Priority: success).==============================================================*** BACKUP, ANALYSIS & MAINTENANCE COMPLETED ***==============================================================FINAL STATUS: [SUCCESS] All Routine Steps completed successfully.──────────────────────────────────────────────────────────────==============================================================DONECurrent customized script-#!/bin/bash############################################################################## Title: Flash Boot Backup License Boot Analysis# Description: Automates Flash or Boot Backup Creation, Retention, License and Boot Analysis## Variable Naming Convention:# --------------------------------------# - GLOBAL VARIABLES: UPPER_SNAKE_CASE (e.g., HOST_NAME, BACKUP_RETENTION_DAYS)# - LOCAL VARIABLES (within modules): UPPER_SNAKE_CASE (e.g., DISK_COUNT, CURRENT_TIME)# - FUNCTIONS: UPPER_SNAKE_CASE (e.g., PRINT_MSG(), LOG_TO_FILE())# - UI DISPLAY TEXTS: Title Case (e.g., "USB Stick", "Backup File", "TPM Mode")## The readonly Keyword Explained:# --------------------------------# "readonly" makes a variable truly immutable (cannot be changed after assignment).# This protects critical system paths from accidental modification during script execution.# Example:# NOTIFICATION_SCRIPT="/usr/local/emhttp/webGui/scripts/notify"# readonly NOTIFICATION_SCRIPT# # Cannot later execute: NOTIFICATION_SCRIPT="/wrong/path" (will fail with error)## Benefits of readonly:# - Prevents bugs from accidental variable reassignment# - Documents intent clearly for other developers# - Helps with security (sensitive paths cannot be hijacked)############################################################################### =============================================================================# 1. CONFIGURATION & IDENTIFIERS# =============================================================================# -----------------------------------------------------------------------------# GENERAL SETTINGS (USER-CUSTOMIZABLE)# Configurable Values for Script Operation. User adjustments permitted.# readonly keyword omitted to allow user overrides via Environment or File edit.# -----------------------------------------------------------------------------SCRIPT_TITLE="Flash Boot Backup License Boot Analysis"HOST_NAME=$(hostname)MIN_DISKS=3 # Minimum Disks required in Array# -----------------------------------------------------------------------------# RETENTION POLICIES (USER-CUSTOMIZABLE)# Controls Backup and Log File Retention Duration and Quantity Limits.# Age-based deletion executes first, followed by count-based Enforcement.# readonly keyword omitted to allow user Adjustments.# -----------------------------------------------------------------------------BACKUP_RETENTION_DAYS=90 # Backup Files kept for specified DaysMAX_BACKUPS_TO_KEEP=15 # Maximum Backup Files retainedLOG_RETENTION_DAYS=30 # Log Files kept for specified DaysMAX_LOG_FILES_TO_KEEP=15 # Maximum Log Files retained# -----------------------------------------------------------------------------# FEATURE FLAGS (USER-CUSTOMIZABLE - ENABLE/DISABLE)# Controls execution of Individual Modules. Accepts TRUE or FALSE.# readonly keyword omitted to allow user overrides.# -----------------------------------------------------------------------------ENABLE_LICENSE_CHECK=TRUE # Check License Status (TPM/USB)ENABLE_BOOT_CHECK=TRUE # Analyze Boot Medium TypeENABLE_HARDWARE_CHECK=TRUE # Verify TPM/USB Hardware availabilityENABLE_SYSTEM_CHECK=TRUE # Check Array Disks and Stale FilesENABLE_BACKUP_CREATE=TRUE # Create New Backup FileENABLE_BACKUP_RETENTION=TRUE # Apply Backup Retention PolicyENABLE_LOG_RETENTION=TRUE # Apply Log Retention PolicyENABLE_NOTIFICATION=TRUE # Send System NotificationSHOW_CLI_OUTPUT=TRUE # Display progress in Console/Terminal# -----------------------------------------------------------------------------# DIRTY CHECK OVERRIDE (USER-CUSTOMIZABLE)# Controls behavior when Stale Temporary Files are detected.# TRUE permits Continuation with Warning, FALSE triggers Abort.# Default: FALSE (abort on Stale Files).# -----------------------------------------------------------------------------SKIP_DIRTY_CHECK=TRUE# -----------------------------------------------------------------------------# AUTO CLEANUP SETTINGS (USER-CUSTOMIZABLE)# Controls automatic deletion of Stale Temporary Files at Script end.# When FALSE (default), Stale Files trigger Abort or Warning (see SKIP_DIRTY_CHECK).# When TRUE, Stale Files are automatically deleted AFTER all other Modules complete.# -----------------------------------------------------------------------------ENABLE_AUTO_CLEANUP=TRUE# -----------------------------------------------------------------------------# FILE NAMING CONVENTIONS (USER-CUSTOMIZABLE)# Defines Log File Base Name and glob Pattern for Retention Matching.# -----------------------------------------------------------------------------LOG_BASENAME="Boot_Backup_Analysis"LOG_FILE_PATTERN="${LOG_BASENAME}_*.log"# -----------------------------------------------------------------------------# NOTIFICATION SETTINGS (USER-CUSTOMIZABLE)# Defines Title and Subject for Error Notifications via Unraid Notify Script.# -----------------------------------------------------------------------------NOTIFY_ERROR_TITLE="Flash Backup Aborted"NOTIFY_ERROR_SUBJECT="Flash or Boot Backup - Error"# -----------------------------------------------------------------------------# USB BRAND PATTERNS (USER-CUSTOMIZABLE)# Pipe-separated list of USB Brand Names for License Device Detection.# Extended Lists accommodate additional USB Manufacturers.# -----------------------------------------------------------------------------USB_BRAND_PATTERN='ADATA|Corsair|Crucial|G.Skill|HP|Imation|Integral|Kingston|Lexar|Netac|PNY|Samsung|SanDisk|Silicon Power|Team Group|Toshiba|Transcend|Verbatim'# -----------------------------------------------------------------------------# MODULE TITLES (STATIC - NEVER MODIFIED AT RUNTIME)# Fixed Labels for CLI Section Headers and Log Entries.# readonly applied: Assigned once during initialization, No runtime modification.# -----------------------------------------------------------------------------readonly MOD_TITLE_1="LICENSE STATUS"readonly MOD_TITLE_2="BOOT MEDIUM ANALYSIS"readonly MOD_TITLE_3="HARDWARE CHECK"readonly MOD_TITLE_4="SYSTEM INTEGRITY CHECK"readonly MOD_TITLE_5="CREATE BACKUP"readonly MOD_TITLE_6="APPLY BACKUP RETENTION"readonly MOD_TITLE_7="APPLY LOG RETENTION"readonly MOD_TITLE_8="AUTO CLEANUP"readonly MOD_TITLE_9="SEND NOTIFICATION"# =============================================================================# 2. SYSTEM CONSTANTS# =============================================================================# -----------------------------------------------------------------------------# SYSTEM PATHS (UNRAID-SPECIFIC - FIXED)# Operating System-defined Paths. Immutable during Script execution.# readonly applied: Prevents accidental or malicious Path Modification.# -----------------------------------------------------------------------------readonly NOTIFICATION_SCRIPT="/usr/local/emhttp/webGui/scripts/notify"readonly VAR_INI_PATH="/var/local/emhttp/var.ini"# -----------------------------------------------------------------------------# DIRECTORY PATHS (DERIVED - AUTO-COMPUTED)# Computed at Script Startup from HOST_NAME and configurable Values above.# Represents Physical Directory Structure on Target System.# readonly applied: Calculated once, Never modified Subsequently.# Dependency order Preserved: Derived Paths Reference earlier Variables.# -----------------------------------------------------------------------------readonly BACKUP_BASE_DIR="/mnt/remotes/WALTERS-M4-MINI.DAHONEY.ME_brunnhilde/boot_backup/"readonly ARCHIVE_SUBDIR="Backup_Reports"readonly LOG_LOCAL_SUBDIR="Backup_Logs_on_Boot"readonly ARCHIVE_LOG_DIR="${BACKUP_BASE_DIR}/${ARCHIVE_SUBDIR}"readonly LOCAL_LOG_DIR="/boot/${LOG_LOCAL_SUBDIR}"# -----------------------------------------------------------------------------# LOG FILE PATHS (DERIVED - AUTO-COMPUTED PER EXECUTION)# Unique per run via embedded Timestamp in Filename. Prevents File Collisions.# LOG_FILE_PATH: Active Session Log (Write Target)# ARCHIVE_LOG_PATH: Permanent Archive Copy (Final Destination)# readonly applied: Calculated once, Never modified Subsequently.# -----------------------------------------------------------------------------readonly LOG_FILE_NAME="${LOG_BASENAME}_${HOST_NAME}_$(date '+%Y-%m-%d_%H-%M-%S').log"readonly LOCAL_LOG_PATH="${LOCAL_LOG_DIR}/${LOG_FILE_NAME}"readonly ARCHIVE_LOG_PATH="${ARCHIVE_LOG_DIR}/${LOG_FILE_NAME}"readonly LAST_RUN_LOG="${ARCHIVE_LOG_DIR}/Last_Run.log"readonly LOG_FILE_PATH="$LOCAL_LOG_PATH"# =============================================================================# 3. GLOBAL VARIABLES (RUNTIME STATE)# =============================================================================# NOTE: Variables below remain mutable (readonly NOT Applied).# Purpose: Carry Initial/Default Values that are overwritten during Module execution.# Applying readonly would cause Runtime Failure (Variable Assignment Rejection).# Pattern: Initialize → Module Updates → Final State persisted# -----------------------------------------------------------------------------# LICENSE & BOOT STATE (INITIAL VALUES - MODULES 1-3 POPULATE)# Neutral Default Values updated upon detection of actual System Configuration.# Transition: Unknown/Empty → Detected Value (USB Flash, TPM, Migration, Invalid).# -----------------------------------------------------------------------------LICENSE_TYPE="" # Initial: empty → Set by Module 1LICENSE_STATUS="Unknown" # Initial: Unknown → Set by Module 1DEVICE_BINDING="None" # Initial: None → Set by Module 1BOOT_MEDIUM_TYPE="Unknown" # Initial: Unknown → Set by Module 2# -----------------------------------------------------------------------------# SYSTEM STATE (INITIAL VALUES - MODULE 4 POPULATES)# Stale File detection initialized clean. Updated during Module 4 execution.# HAS_STALE_FILES: FALSE → TRUE if Stale Files discovered# SKIP_DIRTY_CHECK_ACTIVE: FALSE → TRUE if Bypass activated# -----------------------------------------------------------------------------HAS_STALE_FILES=FALSE # Initial: FALSE → TRUE by Module 4 if Stale Files foundSKIP_DIRTY_CHECK_ACTIVE=FALSE # Initial: FALSE → TRUE by Module 4 if Bypass activated# -----------------------------------------------------------------------------# BACKUP STATE (INITIAL VALUES - MODULE 5 POPULATES)# Empty until Module 5 Creates Backup and populates with generated Values.# -----------------------------------------------------------------------------BACKUP_FILE="" # Initial: empty → Set by Module 5DEST_FILE="" # Initial: empty → Set by Module 5# -----------------------------------------------------------------------------# CLEANUP STATE (INITIAL VALUES - MODULE 8 POPULATES)# Tracks automatic cleanup execution and outcomes.# Auto-computed by Module 8 based on actual file operations performed.# -----------------------------------------------------------------------------CLEANUP_EXECUTED=FALSE # Initial: FALSE → TRUE by Module 8 if cleanup runsCLEANUP_SUCCESS=0 # Initial: 0 → Files by Module 8 successfully removedCLEANUP_FAILED=0 # Initial: 0 → Files by Module 8 that failed removalCLEANUP_TOTAL=0 # Initial: 0 → Total files by Module 8 processed# -----------------------------------------------------------------------------# OVERALL STATUS TRACKING (INITIAL VALUE - ALL MODULES UPDATE)# Cumulative Result Tracker across all executed Modules.# Escalation Priority: ERROR > WARNING > SUCCESS (Monotonic Escalation only).# Modules may increase Severity; Deescalation not supported.# Final Value determines completion Message and Notification Priority.# -----------------------------------------------------------------------------OVERALL_STATUS="SUCCESS" # Initial: SUCCESS → WARNING or ERROR by any Module# =============================================================================# 4. MESSAGE TEXTS (REUSABLE CLI MESSAGES)# =============================================================================# -----------------------------------------------------------------------------# LEVEL LABELS (STATIC - FIXED STRINGS)# Visual indicators for CLI Output and Log Entries.# readonly applied: Never modified after initialization.# -----------------------------------------------------------------------------readonly MSG_SUCCESS="[SUCCESS]"readonly MSG_ERROR="[ERROR]"readonly MSG_WARNING="[WARNING]"readonly MSG_INFO="[INFO]"# -----------------------------------------------------------------------------# REUSABLE ERROR/WARNING MESSAGES (STATIC - FIXED STRINGS)# Predefined Messages used at Multiple Points throughout Script execution.# readonly applied: Never modified after initialization.# Centralization ensures consistent Wording across all Modules.# Single-Point modification enables future text adjustments.# -----------------------------------------------------------------------------readonly MSG_STALE_FILES_FOUND="Stale Temporary Files found."readonly MSG_NO_STALE_FILES="No Stale Temporary Files found."readonly MSG_ARRAY_INTEGRITY_CONFIRMED="Array Integrity confirmed."readonly MSG_NO_USB_STICK="No USB Stick detected, but Flash License active."readonly MSG_USB_STICK_DETECTED="USB Stick detected for License."readonly MSG_TPM_MODE="TPM Mode: No USB Stick required for License."readonly MSG_CONFIG_FILE_NOT_FOUND="Configuration File not found."readonly MSG_VARINI_NOT_FOUND="var.ini not found."readonly MSG_NO_TPM_DEVICE="No TPM Device available."readonly MSG_UNZIP_NOT_FOUND="'unzip' Command not found. Integrity Check impossible."readonly MSG_FLASH_BACKUP_FAILED="Flash Backup Tool failed."readonly MSG_INVALID_FILENAME="Invalid Filename generated."readonly MSG_SOURCE_FILE_MISSING="Source File does not exist."readonly MSG_COPY_FAILED="Copying Backup File failed."readonly MSG_BACKUP_CORRUPTED="Backup File corrupted! Removing defective File."readonly MSG_CHANGE_DIR_FAILED="Change to Directory failed."readonly MSG_PROCESS_ABORTED="Process aborted: Manual cleanup required."readonly MSG_WARNING_IGNORED="Warning ignored."readonly MSG_CLEANUP_STARTED="Auto cleanup initiated."readonly MSG_CLEANUP_FAILED="Cleanup failed. No Files removed. Manual intervention required."readonly MSG_CLEANUP_SKIPPED="Auto cleanup skipped (Disabled or no Stale Files)."# =============================================================================# 5. HELPER FUNCTIONS# =============================================================================# -----------------------------------------------------------------------------# PRINT HEADER BANNER (CLI OUTPUT - STARTUP)# Displays Script Title, Host Name, and Timestamp at Initialization.# Single Invocation before any Module Execution.# Visibility Controlled by SHOW_CLI_OUTPUT Feature Flag.# -----------------------------------------------------------------------------PRINT_HEADER() {echo "=============================================================="echo " ${SCRIPT_TITLE}"echo "=============================================================="echo "HOST: $(hostname)"echo "TIMESTAMP: $(date '+%d.%m.%Y %H:%M')"echo "----------------------------------------"}# -----------------------------------------------------------------------------# PRINT SECTION HEADER (CLI OUTPUT - BETWEEN MODULES)# Displays formatted Section Title between Module Blocks.# Parameter: $1 = Section Title (from MOD_TITLE_* Variables)# Visibility Controlled by SHOW_CLI_OUTPUT Feature Flag.# -----------------------------------------------------------------------------PRINT_SECTION() {echo ""echo "[${1}]"echo "--------------------------------------------------------------"}# -----------------------------------------------------------------------------# PRINT MESSAGE WITH LEVEL PREFIX (CLI OUTPUT - UNIFIED)# Centralized Message formatting for consistent CLI presentation.# Parameters: $1 = Level (SUCCESS/ERROR/WARNING/INFO), $2 = Message Text# Constructs Formatted Output String in Local Variable OUTPUT.# stdbuf Applies Line-buffered Terminal Display when SHOW_CLI_OUTPUT=TRUE.# -----------------------------------------------------------------------------PRINT_MSG() {local LEVEL="$1"local MSG="$2"local OUTPUT=""case "$LEVEL" inSUCCESS) OUTPUT="${MSG_SUCCESS} ${MSG}" ;;ERROR) OUTPUT="${MSG_ERROR} ${MSG}" ;;WARNING) OUTPUT="${MSG_WARNING} ${MSG}" ;;INFO) OUTPUT="${MSG_INFO} ${MSG}" ;;*) OUTPUT="$MSG" ;;esac# Line-buffered Output for immediate Displaystdbuf -oL echo "$OUTPUT"}# -----------------------------------------------------------------------------# WRITE TIMESTAMPED ENTRY TO LOG FILE (LOGGING - UNIFIED)# Appends Single Log Entry with Timestamp and Host Name.# Parameter: $1 = Message Text# Destination: LOG_FILE_PATH (Current Session Log)# Invocation: All Modules (Independent of SHOW_CLI_OUTPUT Setting).# -----------------------------------------------------------------------------LOG_TO_FILE() {local MSG="$1"local TIMESTAMPED_MSG="[$(date '+%Y-%m-%d %H:%M:%S')] [$(hostname)] $MSG"echo "$TIMESTAMPED_MSG" >> "$LOG_FILE_PATH"}# -----------------------------------------------------------------------------# MASK GUID FORMAT (SENSITIVE DATA OBSCURATION)# Preserves GUID Structure while obscuring sensitive Content.## UNRAID GUID STRUCTURE (Uppercase Alphanumeric: 0-9, A-Z only):## - TPM GUIDs: Starts with "01-", 2 Segments (1 Dash)# Example: "01-ABC23D123456GHIJ7890KLMN"# → Masked: "01-ABC23*******************"# Recognition: "01-" Prefix, first 5 Chars of Segment 2 visible.## - Flash GUIDs: 4 Segments (3 Dashes)# Example: "0789-1012-3456-789AABE12345"# → Masked: "0789-10**-34**-789AB*******"# Recognition: All 4 Segments visible, specific show/mask per Segment.## MASKING LOGIC:# - TPM: Show "01-" + first 5 Chars of Segment 2 + rest as ****# - Flash Seg 1: Show all if ≤4 Chars, else first 4 + ****# - Flash Seg 2: Show first 2 Chars + **** (Pad to 4 total)# - Flash Seg 3: Show first 2 Chars + **** (Pad to 4 total)# - Flash Seg 4: Show first 5 Chars + rest as ****## PARAMETER: $1 = Original GUID String (expected Uppercase Alphanumeric)# RETURN: Masked GUID String preserving original Format Structure# -----------------------------------------------------------------------------MASK_GUID_FORMAT() {local GUID="$1"if [[ -z "$GUID" ]]; then echo "---"; return; fi# TPM GUIDif [[ "$GUID" == 01-* ]]; thenlocal TPM_PART="${GUID#01-}"local PART_LEN=${#TPM_PART}if [[ ${PART_LEN} -gt 5 ]]; thenlocal SHOW_CHARS="${TPM_PART:0:5}"local MASK_LEN=$((PART_LEN - 5))echo "01-${SHOW_CHARS}$(printf '*%.0s' $(seq 1 ${MASK_LEN}))"elif [[ ${PART_LEN} -gt 0 ]]; thenecho "01-${TPM_PART}****"elseecho "01-****"fireturnfi# Flash GUIDlocal -a SEGMENTSIFS='-' read -ra SEGMENTS <<< "$GUID"if [[ ${#SEGMENTS[@]} -ge 4 ]]; thenlocal S1="${SEGMENTS[0]}"local S2="${SEGMENTS[1]}"local S3="${SEGMENTS[2]}"local S4="${SEGMENTS[3]}"# Segment 1: Show all if ≤4 Chars, else first 4 + ****local S1_LEN=${#S1}local MASKED_S1if [[ ${S1_LEN} -le 4 ]]; thenMASKED_S1="${S1}"elseMASKED_S1="${S1:0:4}$(printf '*%.0s' $(seq 1 $((S1_LEN - 4))))"fi# Segment 2: Show first 2 Chars + ** paddinglocal S2_LEN=${#S2}local MASKED_S2if [[ ${S2_LEN} -ge 2 ]]; thenMASKED_S2="${S2:0:2}**"elif [[ ${S2_LEN} -eq 1 ]]; thenMASKED_S2="${S2}***"elseMASKED_S2="****"fi# Segment 3: Show first 2 Chars + ** paddinglocal S3_LEN=${#S3}local MASKED_S3if [[ ${S3_LEN} -ge 2 ]]; thenMASKED_S3="${S3:0:2}**"elif [[ ${S3_LEN} -eq 1 ]]; thenMASKED_S3="${S3}***"elseMASKED_S3="****"fi# Segment 4: Show first 5 Chars + rest as ****local S4_LEN=${#S4}local MASKED_S4if [[ ${S4_LEN} -gt 5 ]]; thenMASKED_S4="${S4:0:5}$(printf '*%.0s' $(seq 1 $((S4_LEN - 5))))"elif [[ ${S4_LEN} -gt 0 ]]; thenMASKED_S4="${S4}****"elseMASKED_S4="****"fiecho "${MASKED_S1}-${MASKED_S2}-${MASKED_S3}-${MASKED_S4}"elif [[ ${#SEGMENTS[@]} -ge 2 ]]; then# 2+ Segments but not 4 — generic fallbacklocal FIRST="${SEGMENTS[0]}"local SECOND="${SEGMENTS[1]}"if [[ ${#SECOND} -gt 5 ]]; thenecho "${FIRST}-${SECOND:0:5}$(printf '*%.0s' $(seq 1 $(${#SECOND} - 5)))"elif [[ ${#SECOND} -gt 0 ]]; thenecho "${FIRST}-${SECOND}****"elseecho "${FIRST}-****"fielse# Single Segmentif [[ ${#GUID} -gt 5 ]]; thenecho "${GUID:0:5}$(printf '*%.0s' $(seq 1 $(${#GUID} - 5)))"elif [[ ${#GUID} -gt 0 ]]; thenecho "${GUID}****"elseecho "****"fifi}# -----------------------------------------------------------------------------# CLEAN VENDOR NAME (DEVICE STRING NORMALIZATION)# Extracts Primary Vendor Name from USB Device String.# Removes Common Model Suffixes (Cruzer, DataTraveler, Flash).# Input Example: "SanDisk Cruzer Blade" → "SanDisk"# Parameter: $1 = Raw Vendor String from lsusb Output# Return: Cleaned Vendor Name Only# -----------------------------------------------------------------------------CLEAN_VENDOR_NAME() {local RAW_VENDOR="$1"local VENDOR=$(echo "$RAW_VENDOR" | awk '{print $1}')if [[ "$VENDOR" == "Cruzer" ]]; thenVENDOR=$(echo "$VENDOR" | sed 's/Cruzer$//g')elif [[ "$VENDOR" == "DataTraveler" ]]; thenVENDOR=$(echo "$VENDOR" | sed 's/DataTraveler$//g')elif [[ "$VENDOR" == "Flash" ]]; thenVENDOR=$(echo "$VENDOR" | sed 's/Flash$//g')fiecho "$VENDOR" | xargs}# -----------------------------------------------------------------------------# CLEAN MODEL NAME (DEVICE STRING NORMALIZATION)# Strips Storage Capacity Suffixes from Device Model Strings.# Matches and Removes: GB, TB, MB, GiB, MiB with Numeric Prefixes.# Input Example: "WD Red SN700 1TB" → "WD Red SN700"# Parameter: $1 = Raw Model String# Return: Cleaned Model Name without Capacity Information# -----------------------------------------------------------------------------CLEAN_MODEL_NAME() {local RAW_MODEL="$1"local CLEANED=$(echo "$RAW_MODEL" | sed -E 's/[[:space:]]*[0-9]+\.?[0-9]*(GB|TB|MB|GiB|MiB)[[:space:]]*$//gi')if [[ -z "$CLEANED" ]]; thenecho "$RAW_MODEL"elseecho "$CLEANED"fi}# -----------------------------------------------------------------------------# SEND ERROR NOTIFICATION (CRITICAL ERROR HANDLER)# Triggers Unraid System Notification for Critical Errors.# Parameters: $1 = Title (Optional, Defaults to NOTIFY_ERROR_TITLE)# $2 = Subject (Optional, Defaults to NOTIFY_ERROR_SUBJECT)# $3 = Message (Required)# Hardcoded "-i error" Icon for Unraid Notify Script Compatibility.# -----------------------------------------------------------------------------SEND_ERROR_NOTIFICATION() {local TITLE="${1:-${NOTIFY_ERROR_TITLE}}"local SUBJECT="${2:-${NOTIFY_ERROR_SUBJECT}}"local MESSAGE="$3""$NOTIFICATION_SCRIPT" \-e "$TITLE" \-s "$SUBJECT" \-d "$MESSAGE" \-i "error"}# -----------------------------------------------------------------------------# HANDLE CRITICAL ERROR AND EXIT (FATAL CONDITION HANDLER)# Centralized Error Handler for Fatal Conditions in Module 5.# Parameters: $1 = Log Message (Written to Log File)# $2 = Notification Message (Sent via System Alert)# Sequential Actions: Log Entry → Notification Send → Exit with Code 1.# Exclusive Use by Module 5 for Backup Creation Failures.# -----------------------------------------------------------------------------HANDLE_ERROR_EXIT() {local LOG_MSG="$1"local NOTIFY_MSG="$2"local LOG_PATH="$LOG_FILE_PATH"LOG_TO_FILE "${MSG_ERROR} ${LOG_MSG}"SEND_ERROR_NOTIFICATION "" "" "${NOTIFY_MSG}"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "ERROR" "${MSG_ERROR} ${LOG_MSG}"echo "────────────────────────────────────────────────────────────"echo "→ For detailed Error Information, check Log File:"echo " $LOG_PATH"echo "────────────────────────────────────────────────────────────"fiexit 1}# =============================================================================# 6. INITIALIZATION# =============================================================================# Create Required Directories# Precedence: Created before any Module execution.# Failure Condition: Script Termination with Error Notification.# mkdir -p Behavior: Parent Directories Created Automatically as needed.mkdir -p "$BACKUP_BASE_DIR" || {SEND_ERROR_NOTIFICATION "" "" "Cannot create Backup Directory."exit 1}mkdir -p "${ARCHIVE_LOG_DIR}" || {SEND_ERROR_NOTIFICATION "" "" "Cannot create Archive Log Directory."exit 1}mkdir -p "${LOCAL_LOG_DIR}" || {SEND_ERROR_NOTIFICATION "" "" "Cannot create Local Log Directory."exit 1}# Create Log Files# Command: touch Creates Empty Files if Non-existent.# Failure Condition: Script Termination with Error Notification.touch "$LOCAL_LOG_PATH" 2>/dev/null || {SEND_ERROR_NOTIFICATION "" "" "Cannot create Local Log File."exit 1}touch "$ARCHIVE_LOG_PATH" 2>/dev/null || {SEND_ERROR_NOTIFICATION "" "" "Cannot create Archive Log File."exit 1}# Write Initial Log Entry# Mechanism: Block-Redirect Overwrites Log Files with Startup Metadata.# Purpose: Establishes Baseline for Session Tracking.{echo "=========================================="echo "START ${SCRIPT_TITLE}"echo "TIMESTAMP: $(date '+%d.%m.%Y %H:%M')"echo "HOSTNAME: ${HOST_NAME}"echo "BACKUP TARGET: ${BACKUP_BASE_DIR}"echo "LOCAL LOG: ${LOCAL_LOG_PATH}"echo "ARCHIVE LOG: ${ARCHIVE_LOG_PATH}"echo "LAST RUN LOG: ${LAST_RUN_LOG}"echo "=========================================="} > "$LOG_FILE_PATH"# Print CLI Header (Conditional)# Visibility Controlled by SHOW_CLI_OUTPUT Feature Flag.if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_HEADERfi# =============================================================================# 7. MODULES# =============================================================================# Execution Model: 9 Modular Blocks Process Distinct Tasks Sequentially.# Conditional Execution: Each Module Respects Corresponding ENABLE_* Feature Flag.# Status Accumulation: OVERALL_STATUS Collects Warnings and Errors Across Modules.# Final Determination: Overall Status Sets Completion Message and Notification Priority.# Functional Categorization:# - Modules 1-4: Validation and Analysis (Pre-Backup Checks)# - Modules 5-7: Core Operations (Backup + Retention)# - Module 8: Auto Cleanup (Deferred Stale File removal)# - Module 9: Send Notification (Final Report with complete Data)# Execution Characteristics:# - Conditional Execution Based on ENABLE_* Flags# - Sequential Processing (Order Preserved as Listed)# - State Propagation via Global Variables# - Error Handling via Centralized Handlers# - Log Recording Independent of CLI Output Setting# - Status Escalation Preserves Highest Severity (ERROR > WARNING > SUCCESS)# --- MODULE 1: LICENSE STATUS -----------------------------------------------if [ "$ENABLE_LICENSE_CHECK" = "TRUE" ]; thenLOG_TO_FILE "---[MODULE 1: ${MOD_TITLE_1}]---"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_SECTION "${MOD_TITLE_1}"fi# Local Module VariablesLOCAL_SERVER_NAME=""LOCAL_OS_VERSION=""LOCAL_LICENSE_TYPE=""LOCAL_FLASH_GUID=""LOCAL_TPM_GUID=""LOCAL_REG_GUID=""# Parse var.ini Configuration Fileif [ -f "$VAR_INI_PATH" ]; thenLOCAL_SERVER_NAME=$(awk -F'=' '/^NAME=/ {gsub(/"/, "", $2); gsub(/ /, "_", $2); print $2}' "$VAR_INI_PATH")LOCAL_OS_VERSION=$(awk -F'=' '/^version=/ {gsub(/"/, "", $2); print $2}' "$VAR_INI_PATH")LOCAL_LICENSE_TYPE=$(awk -F'=' '/^regTy=/ {gsub(/"/, "", $2); print $2}' "$VAR_INI_PATH" 2>&1) || trueif [[ "$LOCAL_LICENSE_TYPE" "error" ]] || [[ "$LOCAL_LICENSE_TYPE" "cannot" ]]; thenLOG_TO_FILE "${MSG_WARNING} awk for regTy reported issues: $LOCAL_LICENSE_TYPE"LOCAL_LICENSE_TYPE=""fiLOCAL_FLASH_GUID=$(grep '^flashGUID=' "$VAR_INI_PATH" 2>&1 | cut -d'"' -f2 | tr -d '[:space:]') || trueif [[ "$LOCAL_FLASH_GUID" "error" ]] || [[ "$LOCAL_FLASH_GUID" "No such" ]]; thenLOG_TO_FILE "${MSG_WARNING} grep for flashGUID reported issues: $LOCAL_FLASH_GUID"LOCAL_FLASH_GUID=""fiLOCAL_TPM_GUID=$(grep '^tpmGUID=' "$VAR_INI_PATH" 2>&1 | cut -d'"' -f2 | tr -d '[:space:]') || trueif [[ "$LOCAL_TPM_GUID" "error" ]] || [[ "$LOCAL_TPM_GUID" "No such" ]]; thenLOG_TO_FILE "${MSG_WARNING} grep for tpmGUID reported issues: $LOCAL_TPM_GUID"LOCAL_TPM_GUID=""fiLOCAL_REG_GUID=$(grep '^regGUID=' "$VAR_INI_PATH" 2>&1 | cut -d'"' -f2 | tr -d '[:space:]') || trueif [[ "$LOCAL_REG_GUID" "error" ]] || [[ "$LOCAL_REG_GUID" "No such" ]]; thenLOG_TO_FILE "${MSG_WARNING} grep for regGUID reported issues: $LOCAL_REG_GUID"LOCAL_REG_GUID=""fiLICENSE_TYPE="$LOCAL_LICENSE_TYPE"elsePRINT_MSG "ERROR" "${MSG_ERROR} ${MSG_CONFIG_FILE_NOT_FOUND}"LOG_TO_FILE "${MSG_WARNING} ${MSG_VARINI_NOT_FOUND}"fi# Determine License Binding TypeLICENSE_STATUS="Unknown"DEVICE_BINDING="None"if [[ "$LOCAL_FLASH_GUID" == "$LOCAL_REG_GUID" ]] && [[ "$LOCAL_FLASH_GUID" != "$LOCAL_TPM_GUID" || -z "$LOCAL_TPM_GUID" ]]; thenLICENSE_STATUS="USB_FLASH"DEVICE_BINDING="USB Stick"if [[ -n "$LOCAL_TPM_GUID" ]] && [[ "$LOCAL_FLASH_GUID" != "$LOCAL_TPM_GUID" ]]; thenif [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "INFO" "TPM Key present, currently not used."fiLOG_TO_FILE "${MSG_INFO} TPM Key present, currently not used."fielif [[ "$LOCAL_FLASH_GUID" "$LOCAL_TPM_GUID" ]] && [[ "$LOCAL_FLASH_GUID" "$LOCAL_REG_GUID" ]]; thenLICENSE_STATUS="FULL_TPM"DEVICE_BINDING="TPM"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "SUCCESS" "TPM Key used for Device Binding and TPM Licensing is active."fiLOG_TO_FILE "${MSG_SUCCESS} TPM Key used for Device Binding and TPM Licensing is active."elif [[ "$LOCAL_REG_GUID" == "$LOCAL_TPM_GUID" ]]; thenLICENSE_STATUS="TPM_MIGRATION"DEVICE_BINDING="TPM"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "SUCCESS" "TPM Key used for Device Binding and TPM Licensing is available."fiLOG_TO_FILE "${MSG_INFO} TPM Key used for Device Binding and TPM Licensing is available."elseLICENSE_STATUS="INVALID_CONFIG"DEVICE_BINDING="Unknown"LOG_TO_FILE "${MSG_WARNING} GUID Configuration could not be uniquely identified."fi# Mask GUIDs for Secure DisplayLOCAL_FLASH_MASKED=$(MASK_GUID_FORMAT "$LOCAL_FLASH_GUID")LOCAL_TPM_MASKED=$(MASK_GUID_FORMAT "$LOCAL_TPM_GUID")LOCAL_REG_MASKED=$(MASK_GUID_FORMAT "$LOCAL_REG_GUID")# CLI Output (Consistent Format - Title Case for UI Terms)if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenecho "USB LICENSE ID: ${LOCAL_FLASH_MASKED:-N/A}"echo "TPM KEY: ${LOCAL_TPM_MASKED:-N/A}"echo "ACTIVE REGISTRATION: ${LOCAL_REG_MASKED:-N/A}"echo "----------------------------------------"echo "SERVER NAME: ${LOCAL_SERVER_NAME:-Unavailable}"echo "OS VERSION: ${LOCAL_OS_VERSION:-Unavailable}"echo "LICENSE TYPE: ${LOCAL_LICENSE_TYPE:-Not detected or invalid}"echo "DETECTED BINDING: ${DEVICE_BINDING} (${LICENSE_STATUS})"echo "----------------------------------------"echo "DETECTED MODE: ${LICENSE_STATUS}"echo "LICENSE DEVICE TYPE: ${DEVICE_BINDING}"fi# Check License Status and Update Overall Statusif [[ "$LICENSE_STATUS" == "INVALID_CONFIG" ]]; thenLOG_TO_FILE "${MSG_WARNING} License Configuration invalid"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "WARNING" "License Configuration invalid"fiOVERALL_STATUS="WARNING"fi# Log Module ResultsLOG_TO_FILE "USB LICENSE ID: ${LOCAL_FLASH_MASKED:-N/A}"LOG_TO_FILE "TPM KEY: ${LOCAL_TPM_MASKED:-N/A}"LOG_TO_FILE "ACTIVE REGISTRATION: ${LOCAL_REG_MASKED:-N/A}"LOG_TO_FILE "SERVER NAME: ${LOCAL_SERVER_NAME:-Unavailable}"LOG_TO_FILE "OS VERSION: ${LOCAL_OS_VERSION:-Unavailable}"LOG_TO_FILE "LICENSE TYPE: ${LOCAL_LICENSE_TYPE:-Undetermined}"LOG_TO_FILE "DETECTED BINDING: ${DEVICE_BINDING} (${LICENSE_STATUS})"LOG_TO_FILE "---[${MOD_TITLE_1} COMPLETED]---"LOG_TO_FILE ""elseLOG_TO_FILE "---[MODULE 1: ${MOD_TITLE_1}]---"LOG_TO_FILE "MODULE [DISABLED]"fi# --- MODULE 2: BOOT MEDIUM ANALYSIS -----------------------------------------if [ "$ENABLE_BOOT_CHECK" = "TRUE" ]; thenLOG_TO_FILE "---[MODULE 2: ${MOD_TITLE_2}]---"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_SECTION "${MOD_TITLE_2}"fiBOOT_MEDIUM_TYPE="Unknown"# Local Variables for Boot AnalysisBOOT_DEVICE_NAME=""PARENT_DEVICE=""TRANSPORT=""SYSTEM_DEV_FINAL=""SYS_BOOT_TYPE="Unknown"SYS_VENDOR=""SYS_MODEL_CLEAN=""# Detect Storage Types (NVMe, SATA, MMC)LSBLK_OUTPUT=$(lsblk -d -no NAME,TYPE 2>&1) || trueif echo "$LSBLK_OUTPUT" | grep -qiE "error|cannot"; thenLOG_TO_FILE "${MSG_WARNING} lsblk reported issues: $LSBLK_OUTPUT"fiNVME_DEV_GLOBAL=$(echo "$LSBLK_OUTPUT" | grep 'nvme' | awk '{print $1}' | head -1)SDA_DEV_GLOBAL=$(echo "$LSBLK_OUTPUT" | grep -E '^sd[a-z]$' | awk '{print $1}' | head -1)MMC_DEV_GLOBAL=$(echo "$LSBLK_OUTPUT" | grep 'mmc' | awk '{print $1}' | head -1)# Analyze Flash Label Linkif [ -e "/dev/disk/by-label/flash" ]; thenREAL_LINK=$(readlink -f "/dev/disk/by-label/flash")BOOT_DEVICE_NAME=$(basename "$REAL_LINK")# Remove Partition Number Suffixif [[ "$BOOT_DEVICE_NAME" =~ [0-9]+$ ]]; thenPARENT_DEVICE="${BOOT_DEVICE_NAME%[0-9]*}"elsePARENT_DEVICE="$BOOT_DEVICE_NAME"fi# Determine Transport Protocol (USB, SAS, etc.)TRANSPORT=$(lsblk -no TRAN "$PARENT_DEVICE" 2>&1 | tr -d '[:space:]') || trueif [[ "$TRANSPORT" "error" ]] || [[ "$TRANSPORT" "cannot" ]]; thenLOG_TO_FILE "${MSG_WARNING} lsblk for transport reported issues: $TRANSPORT"TRANSPORT=""ficase "$TRANSPORT" inusb)BOOT_MEDIUM_TYPE="USB Stick Boot";;*)SYS_PATH="/sys/block/$PARENT_DEVICE/device"if [ -L "$SYS_PATH" ]; thenFULL_PATH=$(readlink -f "$SYS_PATH")if [[ "$FULL_PATH" =~ usb ]]; thenBOOT_MEDIUM_TYPE="USB Stick Boot"elseBOOT_MEDIUM_TYPE="Internal Boot"fielseBOOT_MEDIUM_TYPE="Internal Boot"fi;;esac# Get Device Details for Internal Bootif [[ "$BOOT_MEDIUM_TYPE" == "Internal Boot" ]]; thenif [[ -n "$NVME_DEV_GLOBAL" ]]; thenSYSTEM_DEV_FINAL="$NVME_DEV_GLOBAL"elif [[ -n "$SDA_DEV_GLOBAL" ]]; thenSYSTEM_DEV_FINAL="$SDA_DEV_GLOBAL"elif [[ -n "$MMC_DEV_GLOBAL" ]]; thenSYSTEM_DEV_FINAL="$MMC_DEV_GLOBAL"fiSYS_BOOT_TYPE="Internal Boot"if [[ -n "$SYSTEM_DEV_FINAL" ]] && [[ -b "/dev/$SYSTEM_DEV_FINAL" ]]; thenVENDOR_RAW=$(lsblk -no VENDOR "/dev/$SYSTEM_DEV_FINAL" 2>&1 | head -1 | xargs) || trueMODEL_RAW=$(lsblk -no MODEL "/dev/$SYSTEM_DEV_FINAL" 2>&1 | head -1 | xargs) || trueif [[ "$VENDOR_RAW" "error" ]] || [[ "$MODEL_RAW" "error" ]]; thenLOG_TO_FILE "${MSG_WARNING} lsblk for vendor/model reported issues: VENDOR=$VENDOR_RAW MODEL=$MODEL_RAW"fiif [[ -z "$VENDOR_RAW" || "$VENDOR_RAW" == "" ]]; thenSYS_VENDOR="(see Model)"elseSYS_VENDOR="$VENDOR_RAW"fiSYS_MODEL_CLEAN=$(CLEAN_MODEL_NAME "$MODEL_RAW")fifielseLOG_TO_FILE "${MSG_WARNING} /dev/disk/by-label/flash not found."fi# Gather License Device InformationLIC_VENDOR="-"LIC_MODEL_CLEAN="-"LIC_TYPE=""if [[ "$DEVICE_BINDING" == "USB Stick" ]]; thenLSUSB_OUTPUT=$(lsusb 2>&1) || trueif echo "$LSUSB_OUTPUT" | grep -qiE "error|command not found"; thenLOG_TO_FILE "${MSG_WARNING} lsusb reported issues: $LSUSB_OUTPUT"fiUSB_LINE=$(echo "$LSUSB_OUTPUT" | grep -iE "$USB_BRAND_PATTERN" | head -1)if [[ -n "$USB_LINE" ]]; thenRAW_FULL=$(echo "$USB_LINE" | sed 's/.*ID [0-9a-f:]* //' | sed 's/ Corp\. //g' | sed 's/ Inc\. //g' | xargs)LIC_VENDOR=$(echo "$RAW_FULL" | awk '{print $1}')LIC_VENDOR=$(CLEAN_VENDOR_NAME "$LIC_VENDOR")if [[ "$LIC_VENDOR" == "$RAW_FULL" ]]; thenLIC_MODEL_CLEAN="Unknown USB Device"elseMOD_PART=$(echo "$RAW_FULL" | sed "s/^${LIC_VENDOR}[[:space:]]*//")LIC_MODEL_CLEAN=$(CLEAN_MODEL_NAME "$MOD_PART")fifi[[ -z "$LIC_VENDOR" ]] && LIC_VENDOR="Unknown"[[ -z "$LIC_MODEL_CLEAN" ]] && LIC_MODEL_CLEAN="Unknown"LIC_TYPE="USB Stick"elseLIC_TYPE="TPM"fiLOG_TO_FILE "BOOT MEDIUM TYPE: ${BOOT_MEDIUM_TYPE}"LOG_TO_FILE ""LOG_TO_FILE "LICENSE DEVICE"LOG_TO_FILE "TYPE: ${LIC_TYPE}"LOG_TO_FILE "MANUFACTURER: ${LIC_VENDOR}"LOG_TO_FILE "MODEL: ${LIC_MODEL_CLEAN}"LOG_TO_FILE ""LOG_TO_FILE "SYSTEM DEVICE (OS)"LOG_TO_FILE "TYPE: ${SYS_BOOT_TYPE}"LOG_TO_FILE "MANUFACTURER: ${SYS_VENDOR}"LOG_TO_FILE "MODEL: ${SYS_MODEL_CLEAN}"# CLI Output (Title Case for UI Terms: USB Stick, TPM, Internal Boot, Backup File, etc.)if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenecho "BOOT MEDIUM TYPE: ${BOOT_MEDIUM_TYPE}"echo ""echo "LICENSE DEVICE"echo "TYPE: ${LIC_TYPE}"echo "MANUFACTURER: ${LIC_VENDOR}"echo "MODEL: ${LIC_MODEL_CLEAN}"echo ""echo "SYSTEM DEVICE (OS)"echo "TYPE: ${SYS_BOOT_TYPE}"echo "MANUFACTURER: ${SYS_VENDOR}"echo "MODEL: ${SYS_MODEL_CLEAN}"fi# Check Boot Medium Statusif [[ "$BOOT_MEDIUM_TYPE" == "Unknown" ]]; thenLOG_TO_FILE "${MSG_WARNING} Boot Medium could not be identified"OVERALL_STATUS="WARNING"fiLOG_TO_FILE "---[${MOD_TITLE_2} COMPLETED]---"LOG_TO_FILE ""elseLOG_TO_FILE "---[MODULE 2: ${MOD_TITLE_2}]---"LOG_TO_FILE "MODULE [DISABLED]"fi# --- MODULE 3: HARDWARE CHECK -----------------------------------------------if [ "$ENABLE_HARDWARE_CHECK" = "TRUE" ]; thenLOG_TO_FILE "---[MODULE 3: ${MOD_TITLE_3}]---"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_SECTION "${MOD_TITLE_3}"fi# TPM Device Checkif [ -c /dev/tpm0 ] || [ -c /dev/tpmrm0 ]; thenif [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "SUCCESS" "TPM Hardware detected."fiLOG_TO_FILE "${MSG_SUCCESS} TPM Hardware detected."elseif [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "WARNING" "${MSG_NO_TPM_DEVICE}"fiLOG_TO_FILE "${MSG_WARNING} ${MSG_NO_TPM_DEVICE}"fi# USB Stick Check (Only Relevant for USB Flash License)if [[ "$LICENSE_STATUS" == "USB_FLASH" ]]; thenLSUSB_OUTPUT=$(lsusb 2>&1) || trueif [[ $(echo "$LSUSB_OUTPUT" | grep -ciE "$USB_BRAND_PATTERN") -eq 0 ]]; thenif echo "$LSUSB_OUTPUT" | grep -qiE "error|command not found"; thenLOG_TO_FILE "${MSG_WARNING} lsusb reported issues during notification: $LSUSB_OUTPUT"fiif [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "ERROR" "${MSG_NO_USB_STICK}"fiLOG_TO_FILE "${MSG_ERROR} ${MSG_NO_USB_STICK}"elseif [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "INFO" "${MSG_USB_STICK_DETECTED}"fiLOG_TO_FILE "${MSG_INFO} ${MSG_USB_STICK_DETECTED}"fielseif [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "INFO" "${MSG_TPM_MODE}"fiLOG_TO_FILE "${MSG_INFO} ${MSG_TPM_MODE}"fi# Missing USB Device for Flash LicenseLSUSB_FINAL=$(lsusb 2>&1) || trueif [[ "$LICENSE_STATUS" == "USB_FLASH" ]] && [[ $(echo "$LSUSB_FINAL" | grep -ciE "$USB_BRAND_PATTERN") -eq 0 ]]; thenif echo "$LSUSB_FINAL" | grep -qiE "error|command not found"; thenLOG_TO_FILE "${MSG_WARNING} lsusb reported issues during final USB check: $LSUSB_FINAL"fiOVERALL_STATUS="ERROR"fiLOG_TO_FILE "---[${MOD_TITLE_3} COMPLETED]---"LOG_TO_FILE ""elseLOG_TO_FILE "---[MODULE 3: ${MOD_TITLE_3}]---"LOG_TO_FILE "MODULE [DISABLED]"fi# --- MODULE 4: SYSTEM INTEGRITY CHECK ---------------------------------------if [ "$ENABLE_SYSTEM_CHECK" = "TRUE" ]; thenLOG_TO_FILE "---[MODULE 4: ${MOD_TITLE_4}]---"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_SECTION "${MOD_TITLE_4}"fi# Show Dirty Check Statusif [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenecho "DIRTY CHECK OVERRIDE: ${SKIP_DIRTY_CHECK}"echo "AUTO CLEANUP: ${ENABLE_AUTO_CLEANUP}"echo "----------------------------------------"fiLOG_TO_FILE "${MSG_INFO} SKIP_DIRTY_CHECK: ${SKIP_DIRTY_CHECK}"LOG_TO_FILE "${MSG_INFO} AUTO CLEANUP: ${ENABLE_AUTO_CLEANUP}"# CHECK 1: Array Disk CountDISK_COUNT=$(ls -d /mnt/disk* 2>/dev/null | wc -l)if [ "$DISK_COUNT" -lt "$MIN_DISKS" ]; thenLOG_TO_FILE "${MSG_ERROR} Only ${DISK_COUNT} Disks found. Minimum ${MIN_DISKS} required."SEND_ERROR_NOTIFICATION "" "" "Process aborted: Only ${DISK_COUNT} Disks found. Minimum ${MIN_DISKS} required."if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "ERROR" "Only ${DISK_COUNT} Disks found. Minimum ${MIN_DISKS} required."fiexit 1fiif [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "SUCCESS" "${MSG_ARRAY_INTEGRITY_CONFIRMED} (${DISK_COUNT} Disks present)"fiLOG_TO_FILE "${MSG_SUCCESS} ${MSG_ARRAY_INTEGRITY_CONFIRMED} (${DISK_COUNT} Disks present)"# CHECK 2: Stale Temporary FilesHAS_STALE_FILES=FALSEdeclare -a STALE_FILES=()# Find Stale Files in WebGUI Temp DirectoryFIND_OUTPUT=$(find /usr/local/emhttp/ -maxdepth 1 -name "*boot-backup*.zip" \( -type f -o -type l \) 2>&1)FIND_EXIT=$?# Log non-zero exit if not just expected permission deniedif [ $FIND_EXIT -ne 0 ] || echo "$FIND_OUTPUT" | grep -qiE "permission denied|no such file|error"; thenLOG_TO_FILE "${MSG_WARNING} find in /usr/local/emhttp reported issues:"LOG_TO_FILE "[DEBUG] $FIND_OUTPUT"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "WARNING" "find encountered issues: $FIND_OUTPUT"fifiwhile IFS= read -r F; do[[ -n "$F" ]] && STALE_FILES+=("WEBGUI|$F")done <<< "$FIND_OUTPUT"# Find Stale Files in RootFS DirectoryFIND_OUTPUT=$(find / -maxdepth 1 -name "*boot-backup*.zip" -type f 2>&1)FIND_EXIT=$?if [ $FIND_EXIT -ne 0 ] || echo "$FIND_OUTPUT" | grep -qiE "permission denied|no such file|error"; thenLOG_TO_FILE "${MSG_WARNING} find in / reported issues:"LOG_TO_FILE "[DEBUG] $FIND_OUTPUT"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "WARNING" "find encountered issues: $FIND_OUTPUT"fifiwhile IFS= read -r F; do[[ -n "$F" ]] && STALE_FILES+=("ROOTFS|$F")done <<< "$FIND_OUTPUT"STALE_COUNT=${#STALE_FILES[@]}if [ "$STALE_COUNT" -gt 0 ]; thenHAS_STALE_FILES=TRUELOG_TO_FILE "${MSG_WARNING} ${STALE_COUNT} ${MSG_STALE_FILES_FOUND}"# Abort if SKIP_DIRTY_CHECK not setif [ -z "$SKIP_DIRTY_CHECK" ] || [ "$SKIP_DIRTY_CHECK" = "FALSE" ]; thenOVERALL_STATUS="ERROR"LOG_TO_FILE "${MSG_ERROR} ${MSG_PROCESS_ABORTED}"LOG_TO_FILE "${MSG_INFO} SKIP_DIRTY_CHECK is DISABLED - Aborting"SEND_ERROR_NOTIFICATION "" "" "Process aborted: ${STALE_COUNT} stale Temporary Files found. Manual cleanup required."if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "ERROR" "${STALE_COUNT} ${MSG_STALE_FILES_FOUND} Manual cleanup required."echo "────────────────────────────────────────────────────────────"for ENTRY in "${STALE_FILES[@]}"; doSOURCE_TYPE="${ENTRY%%|*}"FILEPATH="${ENTRY#*|}"case "$SOURCE_TYPE" inWEBGUI) PREFIX="[WebGUI Temp] " ;;ROOTFS) PREFIX="[RootFS Temp] " ;;esacif [[ -f "$FILEPATH" ]]; then# Safe stat with error handlingSTAT_OUTPUT=$(stat -c '%s %y' "$FILEPATH" 2>&1)if [ $? -ne 0 ]; thenLOG_TO_FILE "${MSG_WARNING} Cannot stat File for display: $FILEPATH"SIZE_DISPLAY="Unknown"MTIME="Unknown"elseSIZE_BYTES=$(echo "$STAT_OUTPUT" | awk '{print $1}')MTIME=$(echo "$STAT_OUTPUT" | awk '{for(i=2;i<=NF;i++) printf "%s ", $i}' | cut -d'.' -f1 | xargs)if [ -n "$SIZE_BYTES" ] && [ "$SIZE_BYTES" -gt 1073741824 ]; thenSIZE_DISPLAY=$(awk "BEGIN {printf \"%.2f\", $SIZE_BYTES/1024/1024/1024}")" GB"elif [ -n "$SIZE_BYTES" ]; thenSIZE_DISPLAY=$(awk "BEGIN {printf \"%.2f\", $SIZE_BYTES/1024/1024}")" MB"elseSIZE_DISPLAY="Unknown"fifiecho " ${PREFIX}${FILEPATH}"echo " Size: ${SIZE_DISPLAY} | Modified: ${MTIME}"elif [[ -L "$FILEPATH" ]]; thenecho " ${PREFIX}SYMLINK → $(readlink "$FILEPATH" 2>/dev/null)"elseecho " ${PREFIX}${FILEPATH} (Missing/Corrupted)"fidoneecho "────────────────────────────────────────────────────────────"echo "NOTE: Temporary Files will be cleaned on Reboot."echo "→ Manual cleanup required."echo "→ Set SKIP_DIRTY_CHECK=TRUE to ignore."echo "→ Set ENABLE_AUTO_CLEANUP=TRUE for automatic removal after all Modules complete."fiexit 1elseHAS_STALE_FILES=TRUESKIP_DIRTY_CHECK_ACTIVE=TRUEOVERALL_STATUS="WARNING"LOG_TO_FILE "${MSG_WARNING} ${MSG_WARNING_IGNORED} (SKIP_DIRTY_CHECK=TRUE)"LOG_TO_FILE "${MSG_WARNING} Dirty Check Bypassed - Continuing anyway."if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "WARNING" "${STALE_COUNT} ${MSG_STALE_FILES_FOUND}"PRINT_MSG "WARNING" "${MSG_WARNING_IGNORED} (SKIP_DIRTY_CHECK=TRUE)"PRINT_MSG "WARNING" "Dirty Check Bypassed - Continuing anyway."echo "────────────────────────────────────────────────────────────"for ENTRY in "${STALE_FILES[@]}"; doSOURCE_TYPE="${ENTRY%%|*}"FILEPATH="${ENTRY#*|}"case "$SOURCE_TYPE" inWEBGUI) PREFIX="[WebGUI Temp] " ;;ROOTFS) PREFIX="[RootFS Temp] " ;;esacif [[ -f "$FILEPATH" ]]; then# Safe stat with error handlingSTAT_OUTPUT=$(stat -c '%s %y' "$FILEPATH" 2>&1)STAT_EXIT=$?if [ $STAT_EXIT -ne 0 ]; thenLOG_TO_FILE "${MSG_WARNING} Cannot stat file for display: $FILEPATH"SIZE_DISPLAY="Unknown"MTIME="Unknown"elseSIZE_BYTES=$(echo "$STAT_OUTPUT" | awk '{print $1}')MTIME=$(echo "$STAT_OUTPUT" | awk '{for(i=2;i<=NF;i++) printf "%s ", $i}' | cut -d'.' -f1 | xargs)if [ -n "$SIZE_BYTES" ] && [ "$SIZE_BYTES" -gt 1073741824 ]; thenSIZE_DISPLAY=$(awk "BEGIN {printf \"%.2f\", $SIZE_BYTES/1024/1024/1024}")" GB"elif [ -n "$SIZE_BYTES" ]; thenSIZE_DISPLAY=$(awk "BEGIN {printf \"%.2f\", $SIZE_BYTES/1024/1024}")" MB"elseSIZE_DISPLAY="Unknown"fifiecho " ${PREFIX}${FILEPATH}"echo " Size: ${SIZE_DISPLAY} | Modified: ${MTIME}"elif [[ -L "$FILEPATH" ]]; thenLINK_TARGET=$(readlink "$FILEPATH" 2>&1)if [ $? -ne 0 ]; thenLOG_TO_FILE "${MSG_WARNING} Cannot resolve Symlink Target: $FILEPATH"echo " ${PREFIX}SYMLINK → [Unable to resolve]"elseecho " ${PREFIX}SYMLINK → $LINK_TARGET"fielseLOG_TO_FILE "${MSG_WARNING} File disappeared during listing: $FILEPATH"echo " ${PREFIX}${FILEPATH} (Missing/Disappeared)"fidoneecho "────────────────────────────────────────────────────────────"echo "Session Log contains full Details and Error tracking: $LOG_FILE_PATH"echo ""echo "AUTO CLEANUP STATUS: ${ENABLE_AUTO_CLEANUP}"if [ "$ENABLE_AUTO_CLEANUP" = "TRUE" ]; thenecho "→ Will run at end of Script through ${MOD_TITLE_8}."echo "→ Files removed after Backup & Retention completes."elseecho "→ Currently DISABLED."echo "→ Set ENABLE_AUTO_CLEANUP=TRUE for automatic removal."echo ""echo "NOTE: ${STALE_COUNT} Stale Files remain on system."fiecho ""PRINT_MSG "WARNING" "Skipping cleanup and continuing despite Stale Files."fifielseif [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "SUCCESS" "${MSG_NO_STALE_FILES}"fiLOG_TO_FILE "${MSG_SUCCESS} ${MSG_NO_STALE_FILES}"fiLOG_TO_FILE "---[${MOD_TITLE_4} COMPLETED]---"LOG_TO_FILE ""elseLOG_TO_FILE "---[MODULE 4: ${MOD_TITLE_4}]---"LOG_TO_FILE "MODULE [DISABLED]"fi# --- MODULE 5: CREATE BACKUP ------------------------------------------------BACKUP_FILE=""if [ "$ENABLE_BACKUP_CREATE" = "TRUE" ]; thenLOG_TO_FILE "---[MODULE 5: ${MOD_TITLE_5}]---"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_SECTION "${MOD_TITLE_5}"fiLOG_TO_FILE "Target Directory: ${BACKUP_BASE_DIR}"LOG_TO_FILE "Starting Native Flash Backup Script..."if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenecho "Target Directory: ${BACKUP_BASE_DIR}"echo "Starting Native Flash Backup Script..."fi# Check 1: unzip Utility availableif ! command -v unzip >/dev/null 2>&1; thenHANDLE_ERROR_EXIT "'unzip' Command not found. Integrity Check impossible." \"'unzip' Command not found. Integrity Check impossible."fi# Execute Native Flash Backup PHP ScriptBACKUP_RAW_OUTPUT=$(/usr/bin/php -q /usr/local/emhttp/webGui/scripts/flash_backup 2>&1)PHP_EXIT_CODE=$?if [ $PHP_EXIT_CODE -ne 0 ]; thenLOG_TO_FILE "[DEBUG] PHP Output: ${BACKUP_RAW_OUTPUT}"HANDLE_ERROR_EXIT "Flash Backup Tool failed with Exit Code ${PHP_EXIT_CODE}." \"Flash Backup Tool failed with Exit Code ${PHP_EXIT_CODE}."fiBACKUP_FILE=$(echo "$BACKUP_RAW_OUTPUT" | tail -n 1 | xargs)if [ -z "$BACKUP_FILE" ]; thenLOG_TO_FILE "[DEBUG] PHP Output: ${BACKUP_RAW_OUTPUT}"HANDLE_ERROR_EXIT "Invalid Filename generated (Empty Output)." \"Invalid Filename generated (Empty Output)."fiSYMLINK_PATH="/usr/local/emhttp/${BACKUP_FILE}"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "SUCCESS" "PHP flash_backup Script executed and Symlink at ${SYMLINK_PATH} created."fiLOG_TO_FILE "${MSG_SUCCESS} PHP flash_backup Script executed and Symlink at ${SYMLINK_PATH} created."# Check 2: Symlink existsif [ ! -f "$SYMLINK_PATH" ] && [ ! -L "$SYMLINK_PATH" ]; thenHANDLE_ERROR_EXIT "Source File does not exist: ${SYMLINK_PATH}." \"Source File does not exist: ${SYMLINK_PATH}."fi# Resolve Real PathREADLINK_OUTPUT=$(readlink -f "$SYMLINK_PATH" 2>&1) || trueif [[ -z "$READLINK_OUTPUT" ]] || [[ "$READLINK_OUTPUT" "error" ]] || [[ "$READLINK_OUTPUT" "cannot" ]]; thenLOG_TO_FILE "${MSG_WARNING} readlink failed, falling back to symlink path: $READLINK_OUTPUT"SOURCE_FILE="$SYMLINK_PATH"elseSOURCE_FILE="$READLINK_OUTPUT"fiDEST_FILE="${BACKUP_BASE_DIR}/${BACKUP_FILE}"# Check 3: Resolved Source File existsif [ ! -f "$SOURCE_FILE" ]; thenHANDLE_ERROR_EXIT "Resolved Source File missing: ${SOURCE_FILE}." \"Resolved Source File missing: ${SOURCE_FILE}."fi# Copy Backup Fileif [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "INFO" "Copying Backup File to: ${DEST_FILE}"fi# Capture ALL Output including ErrorsCP_OUTPUT=$(cp "$SOURCE_FILE" "$DEST_FILE" 2>&1)COPY_EXIT=$?if [ $COPY_EXIT -ne 0 ]; thenLOG_TO_FILE "${MSG_ERROR} Copy Operation failed with Exit Code ${COPY_EXIT}."LOG_TO_FILE "[DEBUG] cp Output: $CP_OUTPUT"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "ERROR" "${MSG_COPY_FAILED}"echo "→ See Session Log for Details: $LOG_FILE_PATH"fiHANDLE_ERROR_EXIT "${MSG_COPY_FAILED}" \"Copy Error Details logged in Session Log."fiLOG_TO_FILE "Copied Backup File to: ${DEST_FILE}"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "SUCCESS" "Copy completed successfully."fi# Integrity Checkif [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "INFO" "Checking Integrity of File: ${DEST_FILE}"fi# Capture ALL Output including ErrorsUNZIP_OUTPUT=$(unzip -t -q "$DEST_FILE" 2>&1)UNZIP_EXIT=$?if [ $UNZIP_EXIT -ne 0 ]; thenLOG_TO_FILE "${MSG_ERROR} Integrity Check failed with Exit Code ${UNZIP_EXIT}."LOG_TO_FILE "[DEBUG] unzip Output: $UNZIP_OUTPUT"LOG_TO_FILE "${MSG_INFO} Removing corrupted Backup File: $(basename "$DEST_FILE")"rm -f "$DEST_FILE"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "ERROR" "${MSG_BACKUP_CORRUPTED}: $(basename "${DEST_FILE}")."echo "→ Corruption Details in Session Log: $LOG_FILE_PATH"fiHANDLE_ERROR_EXIT "${MSG_BACKUP_CORRUPTED}: $(basename "${DEST_FILE}")." \"Corruption Details logged in Session Log."fiLOG_TO_FILE "${MSG_SUCCESS} Integrity Check passed."if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "SUCCESS" "Integrity Check passed."fi# Cleanup Temporary Filesif [ -f "$SOURCE_FILE" ]; thenif [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "INFO" "Temporary Backup File Location: ${SOURCE_FILE}"firm -f "$SOURCE_FILE"LOG_TO_FILE "${MSG_INFO} Temporary Backup File Location: ${SOURCE_FILE}"LOG_TO_FILE "${MSG_SUCCESS} Temporary Backup File removed: $(basename "$SOURCE_FILE")"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "SUCCESS" "Temporary Backup File removed: $(basename "$SOURCE_FILE")"fifiif [ -L "$SYMLINK_PATH" ]; thenif [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "INFO" "Temporary Symlink Location: ${SYMLINK_PATH}"firm -f "$SYMLINK_PATH"LOG_TO_FILE "${MSG_INFO} Temporary Symlink Location: ${SYMLINK_PATH}"LOG_TO_FILE "${MSG_SUCCESS} Temporary Symlink removed."if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "SUCCESS" "Temporary Symlink removed."fifiLOG_TO_FILE "---[BACKUP CREATION COMPLETED]---"LOG_TO_FILE ""# Update Last Run LogLOG_TO_FILE "---[UPDATE ARCHIVE LOG]---"{echo "=== LAST RUN: ${HOST_NAME} ==="echo "TIMESTAMP: $(date '+%Y-%m-%d %H:%M:%S')"echo "BACKUP FILE: ${BACKUP_FILE}"echo "BACKUP PATH: ${DEST_FILE}"echo "ARCHIVE LOG PATH: ${ARCHIVE_LOG_PATH}"echo "STATUS: SUCCESSFUL"[ -n "$LOCAL_LICENSE_TYPE" ] && echo "License Type: ${LOCAL_LICENSE_TYPE}"[ "${HAS_STALE_FILES:-FALSE}" = "TRUE" ] && echo "[WARNING] Stale Files present"echo "==============================="} > "$LAST_RUN_LOG"LOG_TO_FILE "Status Log saved: ${LAST_RUN_LOG}"LOG_TO_FILE "Session Log archived: ${ARCHIVE_LOG_PATH}"LOG_TO_FILE "---[UPDATE ARCHIVE LOG COMPLETED]---"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_SECTION "UPDATE ARCHIVE LOG"echo "Status Log saved: ${LAST_RUN_LOG}"echo "Session Log archived: ${ARCHIVE_LOG_PATH}"fielseLOG_TO_FILE "---[MODULE 5: ${MOD_TITLE_5}]---"LOG_TO_FILE "MODULE [DISABLED]"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_SECTION "${MOD_TITLE_5} SKIPPED"fifi# --- MODULE 6: APPLY BACKUP RETENTION ---------------------------------------if [ "$ENABLE_BACKUP_RETENTION" = "TRUE" ]; thenLOG_TO_FILE "---[MODULE 6: ${MOD_TITLE_6}]---"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_SECTION "${MOD_TITLE_6}"ficd "$BACKUP_BASE_DIR" || {HANDLE_ERROR_EXIT "${MSG_CHANGE_DIR_FAILED}" \"Change to Backup Directory failed."}# Delete Backups older than Retention PeriodFIND_AGE_OUTPUT=$(find . -maxdepth 1 -name "*boot-backup*.zip" -type f -mtime +"${BACKUP_RETENTION_DAYS}" -print 2>&1)FIND_AGE_EXIT=$?DELETED_BY_AGE=$(echo "$FIND_AGE_OUTPUT" | grep -c "\.zip$" || true)if [ "$FIND_AGE_EXIT" -ne 0 ]; thenLOG_TO_FILE "${MSG_WARNING} find for age-based deletion reported issues: $FIND_AGE_OUTPUT"fiif [ "$DELETED_BY_AGE" -gt 0 ]; thenLOG_TO_FILE "${MSG_INFO} ${DELETED_BY_AGE} Backup Files older than ${BACKUP_RETENTION_DAYS} Days removed."find . -maxdepth 1 -name "*boot-backup*.zip" -type f -mtime +"${BACKUP_RETENTION_DAYS}" -deleteif [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "INFO" "${DELETED_BY_AGE} Backup Files older than ${BACKUP_RETENTION_DAYS} Days removed."fifi# Enforce Maximum Backup Count Limitif [[ -n "$MAX_BACKUPS_TO_KEEP" ]] && [[ "$MAX_BACKUPS_TO_KEEP" -gt 0 ]]; thenLS_OUTPUT=$(ls -1 boot-backup.zip 2>&1) || trueCURRENT_COUNT=$(echo "$LS_OUTPUT" | grep -c "\.zip$" || true)if echo "$LS_OUTPUT" | grep -qiE "error|cannot|no such"; thenLOG_TO_FILE "${MSG_WARNING} ls reported issues: $LS_OUTPUT"fiif [ "$CURRENT_COUNT" -gt "$MAX_BACKUPS_TO_KEEP" ]; thenTO_DELETE=$((CURRENT_COUNT - MAX_BACKUPS_TO_KEEP))LOG_TO_FILE "${MSG_INFO} Current Backup Files: ${CURRENT_COUNT} | Max: ${MAX_BACKUPS_TO_KEEP} → Deleting ${TO_DELETE} Files."mapfile -t BACKUP_FILES_TO_DELETE < <(ls -t boot-backup.zip 2>&1 | tail -n +$((MAX_BACKUPS_TO_KEEP + 1)))for OLD_BACKUP in "${BACKUP_FILES_TO_DELETE[@]}"; doif [ -n "$OLD_BACKUP" ]; thenrm -f "$OLD_BACKUP"LOG_TO_FILE "${MSG_INFO} Deleted: $(basename "$OLD_BACKUP")"fidoneif [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "INFO" "Deleted ${TO_DELETE} old Backup Files to stay within limit of ${MAX_BACKUPS_TO_KEEP}."for OLD_BACKUP in "${BACKUP_FILES_TO_DELETE[@]}"; doif [ -n "$OLD_BACKUP" ]; thenPRINT_MSG "INFO" "Deleted: $(basename "$OLD_BACKUP")"fidonefielseif [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "INFO" "Backup File Count (${CURRENT_COUNT}) is within limit (${MAX_BACKUPS_TO_KEEP})."fififiFINAL_LS_OUTPUT=$(ls -1 boot-backup.zip 2>&1) || trueFINAL_COUNT=$(echo "$FINAL_LS_OUTPUT" | grep -c "\.zip$" || true)LOG_TO_FILE "${MSG_INFO} Result: ${FINAL_COUNT} Flash or Boot Backup Files remaining."LOG_TO_FILE "---[${MOD_TITLE_6} COMPLETED]---"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "INFO" "Total Backup Files remaining: ${FINAL_COUNT}"fielseLOG_TO_FILE "---[MODULE 6: ${MOD_TITLE_6}]---"LOG_TO_FILE "MODULE [DISABLED]"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_SECTION "${MOD_TITLE_6} SKIPPED"fifi# --- MODULE 7: APPLY LOG RETENTION ------------------------------------------if [ "$ENABLE_LOG_RETENTION" = "TRUE" ]; thenLOG_TO_FILE "---[MODULE 7: ${MOD_TITLE_7}]---"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_SECTION "${MOD_TITLE_7}"ficd "$ARCHIVE_LOG_DIR" || {HANDLE_ERROR_EXIT "${MSG_CHANGE_DIR_FAILED}" \"Change to Archive Log Directory failed."}# Delete Logs older than Retention Period (Exclude Last_Run.log)FIND_LOG_AGE_OUTPUT=$(find . -maxdepth 1 -name "*.log" -type f ! -name "Last_Run.log" -mtime +"${LOG_RETENTION_DAYS}" -print 2>&1)FIND_LOG_AGE_EXIT=$?DELETED_LOGS_BY_AGE=$(echo "$FIND_LOG_AGE_OUTPUT" | grep -c "\.log$" || true)if [ "$FIND_LOG_AGE_EXIT" -ne 0 ]; thenLOG_TO_FILE "${MSG_WARNING} find for log age-based deletion reported issues: $FIND_LOG_AGE_OUTPUT"fiif [ "$DELETED_LOGS_BY_AGE" -gt 0 ]; thenLOG_TO_FILE "${MSG_INFO} ${DELETED_LOGS_BY_AGE} Log Files older than ${LOG_RETENTION_DAYS} Days removed."find . -maxdepth 1 -name "*.log" -type f ! -name "Last_Run.log" -mtime +"${LOG_RETENTION_DAYS}" -deleteif [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "INFO" "${DELETED_LOGS_BY_AGE} Log Files older than ${LOG_RETENTION_DAYS} Days removed."fifi# Enforce Maximum Log File Countif [[ -n "$MAX_LOG_FILES_TO_KEEP" ]] && [[ "$MAX_LOG_FILES_TO_KEEP" -gt 0 ]]; thenFIND_COUNT_OUTPUT=$(find . -maxdepth 1 -name "${LOG_FILE_PATTERN}" -type f 2>&1) || trueLOG_FILE_COUNT=$(echo "$FIND_COUNT_OUTPUT" | grep -c "\.log$" || true)if echo "$FIND_COUNT_OUTPUT" | grep -qiE "error|cannot|permission"; thenLOG_TO_FILE "${MSG_WARNING} find for Log count reported issues: $FIND_COUNT_OUTPUT"fiif [ "$LOG_FILE_COUNT" -gt "$MAX_LOG_FILES_TO_KEEP" ]; thenTO_DELETE_COUNT=$((LOG_FILE_COUNT - MAX_LOG_FILES_TO_KEEP))LOG_TO_FILE "${MSG_INFO} Current Log Files: ${LOG_FILE_COUNT} | Max: ${MAX_LOG_FILES_TO_KEEP} → Deleting ${TO_DELETE_COUNT} Files."# Timestamp based sorting for accurate oldest first deletionmapfile -t FILES_TO_DELETE < <(for F in $(find . -maxdepth 1 -name "${LOG_FILE_PATTERN}" -type f 2>&1); doTS=$(stat -c '%Y' "$F" 2>&1)[[ -z "$TS" ]] && TS=0printf '%s %s\n' "$TS" "$F"done | sort -n | head -n "$TO_DELETE_COUNT" | awk '{print $2}')for OLD_LOG in "${FILES_TO_DELETE[@]}"; doif [ -n "$OLD_LOG" ]; thenrm -f "$OLD_LOG"LOG_TO_FILE "${MSG_INFO} Deleted: $(basename "$OLD_LOG")"fidoneif [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "INFO" "Deleted ${TO_DELETE_COUNT} old Log Files to stay within limit of ${MAX_LOG_FILES_TO_KEEP}."for OLD_LOG in "${FILES_TO_DELETE[@]}"; doif [ -n "$OLD_LOG" ]; thenPRINT_MSG "INFO" "Deleted: $(basename "$OLD_LOG")"fidonefielseif [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "INFO" "Log File Count (${LOG_FILE_COUNT}) is within limit (${MAX_LOG_FILES_TO_KEEP})."fififiFINAL_FIND_OUTPUT=$(find . -maxdepth 1 -name "${LOG_FILE_PATTERN}" -type f 2>&1) || trueFINAL_LOG_COUNT=$(echo "$FINAL_FIND_OUTPUT" | grep -c "\.log$" || true)if echo "$FINAL_FIND_OUTPUT" | grep -qiE "error|cannot|permission"; thenLOG_TO_FILE "${MSG_WARNING} find for final Log count reported issues: $FINAL_FIND_OUTPUT"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "WARNING" "Log File search encountered issues: $FINAL_FIND_OUTPUT"fifiLOG_TO_FILE "${MSG_INFO} Result: ${FINAL_LOG_COUNT} Log Files remaining (Excluding Last_Run.log)."LOG_TO_FILE "---[${MOD_TITLE_7} COMPLETED]---"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "INFO" "Total Log Files remaining: ${FINAL_LOG_COUNT}"fielseLOG_TO_FILE "---[MODULE 7: ${MOD_TITLE_7}]---"LOG_TO_FILE "MODULE [DISABLED]"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_SECTION "${MOD_TITLE_7} SKIPPED"fifi# --- MODULE 8: AUTO CLEANUP -----------------------------------------------if [ "$ENABLE_AUTO_CLEANUP" = "TRUE" ]; thenLOG_TO_FILE "---[MODULE 8: ${MOD_TITLE_8}]---"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_SECTION "${MOD_TITLE_8}"echo "AUTO CLEANUP STATUS: ENABLED"echo "STALE FILES FROM ${MOD_TITLE_4}: ${HAS_STALE_FILES:-N/A}"echo "----------------------------------------"fi# Proceed only if Stale Files were detected (from Module 4) OR new ones existdeclare -a ALL_STALE_FILES=()# WebGUI Temp DirectoryCLEANUP_FIND_WEBGUI=$(find /usr/local/emhttp/ -maxdepth 1 -name "*boot-backup*.zip" \( -type f -o -type l \) 2>&1)if echo "$CLEANUP_FIND_WEBGUI" | grep -qiE "error|cannot|permission"; thenLOG_TO_FILE "${MSG_WARNING} find in /usr/local/emhttp during cleanup reported issues: $CLEANUP_FIND_WEBGUI"fiwhile IFS= read -r F; do[[ -n "$F" ]] && ALL_STALE_FILES+=("WEBGUI|$F")done <<< "$CLEANUP_FIND_WEBGUI"# RootFS DirectoryCLEANUP_FIND_ROOTFS=$(find / -maxdepth 1 -name "*boot-backup*.zip" -type f 2>&1)if echo "$CLEANUP_FIND_ROOTFS" | grep -qiE "error|cannot|permission"; thenLOG_TO_FILE "${MSG_WARNING} find in / during cleanup reported issues: $CLEANUP_FIND_ROOTFS"fiwhile IFS= read -r F; do[[ -n "$F" ]] && ALL_STALE_FILES+=("ROOTFS|$F")done <<< "$CLEANUP_FIND_ROOTFS"CLEANUP_TOTAL=${#ALL_STALE_FILES[@]}CLEANUP_SUCCESS=0CLEANUP_FAILED=0# No Stale Files — Skip Cleanupif [ $CLEANUP_TOTAL -eq 0 ]; thenLOG_TO_FILE "${MSG_INFO} No Stale Files detected. Cleanup not required."if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "INFO" "No Stale Files detected. Cleanup not required."fiCLEANUP_EXECUTED=FALSELOG_TO_FILE "---[${MOD_TITLE_8} SKIPPED - NO STALE FILES]---"LOG_TO_FILE ""# Continue to next Module (Notification)else# Stale Files exist — proceed with cleanupCLEANUP_EXECUTED=TRUELOG_TO_FILE "${MSG_INFO} ${MSG_CLEANUP_STARTED}"LOG_TO_FILE "${MSG_INFO} Total files to process: ${CLEANUP_TOTAL}"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "INFO" "${MSG_CLEANUP_STARTED}"echo "Total Stale Files to process: ${CLEANUP_TOTAL}"fi# Attempt RemovalDELETED_FILES=()FAILED_FILES=()for ENTRY in "${ALL_STALE_FILES[@]}"; doSOURCE_TYPE="${ENTRY%%|*}"FILEPATH="${ENTRY#*|}"if [[ -f "$FILEPATH" ]] || [[ -L "$FILEPATH" ]]; then# Capture rm Output for loggingRM_OUTPUT=$(rm -f "$FILEPATH" 2>&1)DELETE_EXIT=$?if [ $DELETE_EXIT -eq 0 ]; then((CLEANUP_SUCCESS++))if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thencase "$SOURCE_TYPE" inWEBGUI) echo " [SUCCESS] Removed: [WebGUI Temp] $(basename "$FILEPATH")" ;;ROOTFS) echo " [SUCCESS] Removed: [RootFS Temp] $(basename "$FILEPATH")" ;;esacfiLOG_TO_FILE "${MSG_SUCCESS} Removed: ${SOURCE_TYPE}|${FILEPATH}"DELETED_FILES+=("${FILEPATH}")else((CLEANUP_FAILED++))FAILED_FILES+=("${SOURCE_TYPE}|${FILEPATH}")LOG_TO_FILE "${MSG_ERROR} Failed to remove: ${SOURCE_TYPE}|${FILEPATH}"if [ -n "$RM_OUTPUT" ]; thenLOG_TO_FILE "[DEBUG] rm output: $RM_OUTPUT"fifielse# File already gone or inaccessible((CLEANUP_FAILED++))FAILED_FILES+=("${SOURCE_TYPE}|${FILEPATH}")LOG_TO_FILE "${MSG_WARNING} File inaccessible (already removed?): ${SOURCE_TYPE}|${FILEPATH}"fidone# Final Status determinationif [ $CLEANUP_SUCCESS -gt 0 ] && [ $CLEANUP_FAILED -eq 0 ]; then# Perfect cleanupif [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "SUCCESS" "All ${CLEANUP_SUCCESS} Stale Files removed successfully."fiLOG_TO_FILE "${MSG_SUCCESS} All ${CLEANUP_SUCCESS} Files removed successfully."elif [ $CLEANUP_SUCCESS -gt 0 ] && [ $CLEANUP_FAILED -gt 0 ]; then# Partial success — WARNINGif [ "$OVERALL_STATUS" != "ERROR" ]; thenOVERALL_STATUS="WARNING"fiif [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "WARNING" "${CLEANUP_SUCCESS} removed, ${CLEANUP_FAILED} failed. Partial cleanup completed."fiLOG_TO_FILE "${MSG_WARNING} Partial cleanup: ${CLEANUP_SUCCESS} removed, ${CLEANUP_FAILED} failed"# List failed Files for user referenceif [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenecho " Failed Files (manual cleanup required):"for FAIL_ENTRY in "${FAILED_FILES[@]}"; doFILE_PATH="${FAIL_ENTRY#*|}"echo " ${FILE_PATH}"donefielse# Complete Failureif [ "$OVERALL_STATUS" != "ERROR" ]; thenOVERALL_STATUS="ERROR"fiif [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "ERROR" "Cleanup failed. No Files removed. Manual intervention required."fiLOG_TO_FILE "${MSG_ERROR} Cleanup failed. No Files removed. Manual intervention required."LOG_TO_FILE "${MSG_ERROR} ${CLEANUP_TOTAL} Stale Files remain. Manual cleanup required."# Send Error Notification immediately for cleanup failureSEND_ERROR_NOTIFICATION "Flash Backup Auto cleanup failed" \"Flash Backup Auto cleanup" \"Failed to remove ${CLEANUP_TOTAL} Stale Temporary Files. Manual cleanup required. See Log: $LOG_FILE_PATH"fiLOG_TO_FILE "---[${MOD_TITLE_8} COMPLETED]---"LOG_TO_FILE "Stats: ${CLEANUP_TOTAL} total, ${CLEANUP_SUCCESS} succeeded, ${CLEANUP_FAILED} failed"LOG_TO_FILE ""# NOTE: HAS_STALE_FILES remains TRUE to preserve Module 4's Original detection Statefielse# Auto cleanup disabledLOG_TO_FILE "---[MODULE 8: ${MOD_TITLE_8}]---"LOG_TO_FILE "MODULE [DISABLED]"LOG_TO_FILE "${MSG_INFO} ${MSG_CLEANUP_SKIPPED}"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_SECTION "${MOD_TITLE_8}"echo "AUTO CLEANUP STATUS: DISABLED"PRINT_MSG "INFO" "${MSG_CLEANUP_SKIPPED}"fiCLEANUP_EXECUTED=FALSECLEANUP_SUCCESS=0CLEANUP_FAILED=0CLEANUP_TOTAL=0fi# --- MODULE 9: SEND NOTIFICATION --------------------------------------------if [ "$ENABLE_NOTIFICATION" = "TRUE" ]; thenLOG_TO_FILE "---[MODULE 9: ${MOD_TITLE_9}]---"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_SECTION "${MOD_TITLE_9}"fi# Initialize Notification VariablesNOTIFY_TITLE="Flash or Boot Backup completed - ${HOST_NAME}"NOTIFY_MSG="Flash or Boot Backup '${BACKUP_FILE:-N/A}' successfully created."NOTIFY_PRIORITY="success"NOTIFY_SUBJECT="Flash or Boot Backup success"DETAIL_MSG=""# Condition 1: Invalid License Configurationif [ "$ENABLE_LICENSE_CHECK" = "TRUE" ] && [[ "$LICENSE_STATUS" == "INVALID_CONFIG" ]]; thenNOTIFY_PRIORITY="error"NOTIFY_SUBJECT="Flash or Boot Backup - Error"DETAIL_MSG="${DETAIL_MSG:+${DETAIL_MSG} | }[ERROR] License Configuration invalid"fi# Condition 2: Missing USB License Deviceif [ "$ENABLE_HARDWARE_CHECK" = "TRUE" ] && [[ "$LICENSE_STATUS" == "USB_FLASH" ]]; thenLSUSB_OUTPUT=$(lsusb 2>&1) || trueif [[ $(echo "$LSUSB_OUTPUT" | grep -ciE "$USB_BRAND_PATTERN") -eq 0 ]]; thenif echo "$LSUSB_OUTPUT" | grep -qiE "error|command not found"; thenLOG_TO_FILE "${MSG_WARNING} lsusb reported issues during notification: $LSUSB_OUTPUT"fiNOTIFY_PRIORITY="error"NOTIFY_SUBJECT="Flash or Boot Backup - Error"DETAIL_MSG="${DETAIL_MSG:+${DETAIL_MSG} | }[ERROR] Required USB License Device not detected"fifi# Condition 3: Stale Temporary Files (Warning if no Error)if [[ "${HAS_STALE_FILES:-FALSE}" == "TRUE" ]]; thenif [[ "$NOTIFY_PRIORITY" != "error" ]]; thenNOTIFY_PRIORITY="warning"NOTIFY_SUBJECT="Flash or Boot Backup - Warning"fiDETAIL_MSG="${DETAIL_MSG:+${DETAIL_MSG} | }[WARNING] Stale Temporary Files present"fi# Condition 4: Unknown Boot Medium (Warning if Success)if [ "$ENABLE_BOOT_CHECK" = "TRUE" ] && [[ "$BOOT_MEDIUM_TYPE" == "Unknown" ]]; thenif [[ "$NOTIFY_PRIORITY" == "success" ]]; thenNOTIFY_PRIORITY="warning"NOTIFY_SUBJECT="Flash or Boot Backup - Warning"fiDETAIL_MSG="${DETAIL_MSG:+${DETAIL_MSG} | }[WARNING] Boot Medium could not be identified"fi# Add License Informationif [ "$ENABLE_LICENSE_CHECK" = "TRUE" ]; thencase "$DEVICE_BINDING" in"USB Stick") DETAIL_MSG="${DETAIL_MSG:+${DETAIL_MSG} | }License: ${LICENSE_TYPE:-Unknown} (USB)" ;;"TPM") DETAIL_MSG="${DETAIL_MSG:+${DETAIL_MSG} | }License: ${LICENSE_TYPE:-Unknown} (TPM)" ;;esacfi# Add Boot Medium Informationif [ "$ENABLE_BOOT_CHECK" = "TRUE" ] && [[ "$BOOT_MEDIUM_TYPE" != "Unknown" ]]; thenDETAIL_MSG="${DETAIL_MSG:+${DETAIL_MSG} | }Boot: ${BOOT_MEDIUM_TYPE}"fi# Calculate Backup File SizeBACKUP_SIZE_DISPLAY="N/A"if [ -f "$DEST_FILE" ]; thenSTAT_SIZE_OUTPUT=$(stat -c '%s' "$DEST_FILE" 2>&1)if echo "$STAT_SIZE_OUTPUT" | grep -qiE "error|cannot|no such"; thenLOG_TO_FILE "${MSG_WARNING} stat for backup size failed: $STAT_SIZE_OUTPUT"BACKUP_SIZE_BYTES=""elseBACKUP_SIZE_BYTES="$STAT_SIZE_OUTPUT"fiif [[ -n "$BACKUP_SIZE_BYTES" ]]; thenBACKUP_SIZE_MB=$((BACKUP_SIZE_BYTES / 1024 / 1024))BACKUP_SIZE_GB=$(awk "BEGIN {printf \"%.2f\", $BACKUP_SIZE_BYTES / 1024 / 1024 / 1024}")if [ "$BACKUP_SIZE_BYTES" -gt 1073741824 ]; thenBACKUP_SIZE_DISPLAY="${BACKUP_SIZE_GB} GB"elseBACKUP_SIZE_DISPLAY="${BACKUP_SIZE_MB} MB"fiif [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenecho "Backup File Size: ${BACKUP_SIZE_DISPLAY}"fiLOG_TO_FILE "Backup File Size: ${BACKUP_SIZE_DISPLAY} / ${BACKUP_SIZE_BYTES} bytes"fifi# ADD Final Cleanup Informationif [ "$ENABLE_AUTO_CLEANUP" = "TRUE" ]; thenif [[ "${CLEANUP_EXECUTED:-FALSE}" == "TRUE" ]]; thenif [ $CLEANUP_FAILED -eq 0 ]; thenDETAIL_MSG="${DETAIL_MSG:+${DETAIL_MSG} | }[SUCCESS] Cleaned ${CLEANUP_SUCCESS} Stale Files"elif [ $CLEANUP_SUCCESS -gt 0 ]; thenDETAIL_MSG="${DETAIL_MSG:+${DETAIL_MSG} | }[PARTIAL] Cleaned ${CLEANUP_SUCCESS}/${CLEANUP_TOTAL} (${CLEANUP_FAILED} failed)"elseDETAIL_MSG="${DETAIL_MSG:+${DETAIL_MSG} | }[FAILED] Auto cleanup unsuccessful"fielseDETAIL_MSG="${DETAIL_MSG:+${DETAIL_MSG} | }[NOT ACTIVE] No stale files to clean"fifiDETAIL_MSG="${DETAIL_MSG:+${DETAIL_MSG} | }Size: ${BACKUP_SIZE_DISPLAY}"# Send Notification"$NOTIFICATION_SCRIPT" \-e "$NOTIFY_TITLE" \-s "$NOTIFY_SUBJECT" \-d "${NOTIFY_MSG} - ${DETAIL_MSG}" \-i "$NOTIFY_PRIORITY"LOG_TO_FILE "${MSG_INFO} Notification sent (Priority: ${NOTIFY_PRIORITY})."LOG_TO_FILE "---[${MOD_TITLE_9} COMPLETED]---"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_MSG "INFO" "Notification sent successfully (Priority: ${NOTIFY_PRIORITY})."fielseLOG_TO_FILE "---[MODULE 9: ${MOD_TITLE_9}]---"LOG_TO_FILE "MODULE [DISABLED]"if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenPRINT_SECTION "${MOD_TITLE_9} SKIPPED"fifi# =============================================================================# 8. FINALIZATION & STATUS REPORT# =============================================================================# Determine Final Status Text based on OVERALL_STATUS# Mapping: Internal Status Value → Human-readable Messages and Symbols.# States: SUCCESS, WARNING, ERROR, DEFAULT (Catch-all Fallback).case "$OVERALL_STATUS" inSUCCESS)FINAL_MESSAGE="All Routine Steps completed successfully."FINAL_SYMBOL="[SUCCESS]";;WARNING)FINAL_MESSAGE="All Routine Steps completed. One or more Warnings occurred."FINAL_SYMBOL="[WARNING]";;ERROR)FINAL_MESSAGE="All Routine Steps completed. One or more Errors occurred."FINAL_SYMBOL="[ERROR]";;*)FINAL_MESSAGE="All Routine Steps completed."FINAL_SYMBOL="[INFO]";;esac# Final CLI Banner# Display Condition: SHOW_CLI_OUTPUT=TRUE required.# Timing: After all Modules complete.if [ "$SHOW_CLI_OUTPUT" = "TRUE" ]; thenecho ""echo "=============================================================="# Visual differentiation based on OVERALL_STATUSif [[ "$OVERALL_STATUS" == "ERROR" ]]; thenecho " *** ERROR - SCRIPT ABORTED ***"echo "=============================================================="echo "FINAL STATUS: ${FINAL_SYMBOL} ${FINAL_MESSAGE}"echo "──────────────────────────────────────────────────────────────"echo "WARNING: Manual intervention required immediately!"elif [[ "$OVERALL_STATUS" == "WARNING" ]]; thenecho " *** COMPLETED WITH WARNINGS ***"echo "=============================================================="echo "FINAL STATUS: ${FINAL_SYMBOL} ${FINAL_MESSAGE}"echo "──────────────────────────────────────────────────────────────"echo "NOTE: Review Warnings in Log File below."elseecho " *** BACKUP, ANALYSIS & MAINTENANCE COMPLETED ***"echo "=============================================================="echo "FINAL STATUS: ${FINAL_SYMBOL} ${FINAL_MESSAGE}"echo "──────────────────────────────────────────────────────────────"fi# Add Log Path hint if Warnings or Errors occurredif [[ "$OVERALL_STATUS" "WARNING" ]] || [[ "$OVERALL_STATUS" "ERROR" ]]; thenecho "→ For Details, check Session Log: $LOG_FILE_PATH"echo "→ Or view Archive Logs: $ARCHIVE_LOG_DIR"fiecho "=============================================================="fi# Final Logging# Action: Completion Event Recorded to Log File.# Visual Display Covered by Final Banner Above.LOG_TO_FILE "FINAL STATUS: ${FINAL_SYMBOL} ${FINAL_MESSAGE}"# Final Archive Log Sync# Action: Local Session Log Copied to Archive Location.# Purpose: Permanent Record Established in Reports Directory.# NOTE: Cleanup details already logged in Module 8; Session Log preserves full history.LOG_TO_FILE "Final Archive Log Sync completed."cp "$LOCAL_LOG_PATH" "$ARCHIVE_LOG_PATH"# Normal Script Termination# Exit Code 0: Successful Completion (Warnings Permitted).exit 0The backup zip files are being created at the location specified. I'm just not getting an email notification. There also isn't any "sent email" entry in the system log. Edited yesterday at 12:33 AM1 day by wgstarks
Monday at 08:57 PM1 day 1 hour ago, Revan335 said:PN is a Option when its not published for all.Not sure what this means?
19 hours ago19 hr 15 hours ago, wgstarks said:Not sure what this means?Private Notification on the Forum.
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.