Automatically verify backup completion, check file integrity, and alert on failures.

Overview

This script validates that backups completed successfully by checking for backup files, verifying they’re recent, ensuring non-zero file sizes, and optionally testing sample file restoration. Alerts are sent if any verification step fails.

Use Case: Automated backup verification, disaster recovery preparedness, compliance validation, and early detection of backup failures before data loss occurs.

Platform: Linux (Bash) and Windows (PowerShell) Requirements: Read access to backup location, optional write access for restore testing Execution Time: 30 seconds to 5 minutes depending on file count and restore tests

The Script (Bash)

Lang: bash
  1#!/bin/bash
  2
  3#############################################################################
  4# Backup Verification Script (Bash)
  5#
  6# Description: Verify backup completion, file integrity, and send alerts
  7# Author: glyph.sh
  8# Reference: https://glyph.sh/scripts/backup-verification/
  9#
 10# Features:
 11# - Check if backup completed successfully
 12# - Verify backup file exists and is recent
 13# - Check backup file size (not 0 bytes)
 14# - Test restore of sample files (optional)
 15# - Send alert if backup failed
 16# - Multiple notification methods (email, syslog, webhook)
 17#############################################################################
 18
 19# Configuration
 20BACKUP_BASE_PATH=${BACKUP_BASE_PATH:-"/backup"}
 21BACKUP_NAME=${BACKUP_NAME:-"daily"}
 22MAX_AGE_HOURS=${MAX_AGE_HOURS:-24}           # Maximum age in hours
 23MIN_SIZE_MB=${MIN_SIZE_MB:-10}               # Minimum backup size in MB
 24TEST_RESTORE=${TEST_RESTORE:-false}          # Test restore functionality
 25RESTORE_TEST_PATH=${RESTORE_TEST_PATH:-"/tmp/restore_test"}
 26ALERT_EMAIL=${ALERT_EMAIL:-"admin@example.com"}
 27SEND_EMAIL=${SEND_EMAIL:-false}
 28USE_SYSLOG=${USE_SYSLOG:-true}
 29WEBHOOK_URL=${WEBHOOK_URL:-""}
 30BACKUP_PATTERN=${BACKUP_PATTERN:-"*.tar.gz"} # File pattern to search
 31
 32# Colors for output
 33RED='\033[0;31m'
 34GREEN='\033[0;32m'
 35YELLOW='\033[1;33m'
 36BLUE='\033[0;34m'
 37NC='\033[0m' # No Color
 38
 39# Exit codes
 40EXIT_SUCCESS=0
 41EXIT_NO_BACKUP_FOUND=1
 42EXIT_BACKUP_TOO_OLD=2
 43EXIT_BACKUP_TOO_SMALL=3
 44EXIT_RESTORE_FAILED=4
 45EXIT_VERIFICATION_FAILED=5
 46
 47#############################################################################
 48# Functions
 49#############################################################################
 50
 51log_message() {
 52    local level="$1"
 53    local message="$2"
 54    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
 55
 56    # Log to syslog
 57    if [ "$USE_SYSLOG" = true ]; then
 58        logger -t "backup-verification" -p "user.${level}" "$message"
 59    fi
 60
 61    # Print to console
 62    case "$level" in
 63        "error")
 64            echo -e "${RED}[ERROR]${NC} [$timestamp] $message"
 65            ;;
 66        "warning")
 67            echo -e "${YELLOW}[WARNING]${NC} [$timestamp] $message"
 68            ;;
 69        "info")
 70            echo -e "${GREEN}[INFO]${NC} [$timestamp] $message"
 71            ;;
 72        *)
 73            echo "[$timestamp] $message"
 74            ;;
 75    esac
 76}
 77
 78send_alert() {
 79    local subject="$1"
 80    local message="$2"
 81    local severity="$3"  # success, warning, error
 82
 83    # Email alert
 84    if [ "$SEND_EMAIL" = true ] && command -v mail >/dev/null 2>&1; then
 85        echo -e "$message" | mail -s "$subject" "$ALERT_EMAIL"
 86        log_message "info" "Email alert sent to $ALERT_EMAIL"
 87    fi
 88
 89    # Webhook alert (Slack/Discord/Teams)
 90    if [ -n "$WEBHOOK_URL" ] && command -v curl >/dev/null 2>&1; then
 91        local emoji="✅"
 92        [ "$severity" = "warning" ] && emoji="⚠️"
 93        [ "$severity" = "error" ] && emoji="❌"
 94
 95        curl -X POST "$WEBHOOK_URL" \
 96            -H 'Content-Type: application/json' \
 97            -d "{\"text\":\"${emoji} ${subject}\n\n${message}\"}" \
 98            >/dev/null 2>&1
 99        log_message "info" "Webhook alert sent"
100    fi
101}
102
103find_latest_backup() {
104    local backup_path="$1"
105    local pattern="$2"
106
107    # Find the most recent backup file matching pattern
108    find "$backup_path" -name "$pattern" -type f -printf '%T@ %p\n' 2>/dev/null | \
109        sort -rn | \
110        head -1 | \
111        awk '{print $2}'
112}
113
114verify_backup_exists() {
115    log_message "info" "Checking for backup files in $BACKUP_BASE_PATH"
116
117    LATEST_BACKUP=$(find_latest_backup "$BACKUP_BASE_PATH" "$BACKUP_PATTERN")
118
119    if [ -z "$LATEST_BACKUP" ]; then
120        log_message "error" "No backup files found matching pattern: $BACKUP_PATTERN"
121        send_alert "[CRITICAL] Backup Verification Failed" \
122            "No backup files found in $BACKUP_BASE_PATH\nPattern: $BACKUP_PATTERN\nHostname: $(hostname)\nDate: $(date)" \
123            "error"
124        return $EXIT_NO_BACKUP_FOUND
125    fi
126
127    log_message "info" "Found backup: $LATEST_BACKUP"
128    return 0
129}
130
131verify_backup_age() {
132    local backup_file="$1"
133    local max_age="$2"
134
135    log_message "info" "Verifying backup age (max: ${max_age}h)"
136
137    # Get file modification time in seconds since epoch
138    local file_time=$(stat -c %Y "$backup_file" 2>/dev/null || stat -f %m "$backup_file" 2>/dev/null)
139    local current_time=$(date +%s)
140    local age_seconds=$((current_time - file_time))
141    local age_hours=$((age_seconds / 3600))
142    local max_age_seconds=$((max_age * 3600))
143
144    log_message "info" "Backup age: ${age_hours}h"
145
146    if [ "$age_seconds" -gt "$max_age_seconds" ]; then
147        log_message "error" "Backup is too old: ${age_hours}h (max: ${max_age}h)"
148        send_alert "[CRITICAL] Backup Too Old" \
149            "Backup file: $backup_file\nAge: ${age_hours} hours\nMaximum allowed: ${max_age} hours\nHostname: $(hostname)\nDate: $(date)" \
150            "error"
151        return $EXIT_BACKUP_TOO_OLD
152    fi
153
154    log_message "info" "Backup age verification passed"
155    return 0
156}
157
158verify_backup_size() {
159    local backup_file="$1"
160    local min_size_mb="$2"
161
162    log_message "info" "Verifying backup size (min: ${min_size_mb}MB)"
163
164    # Get file size in bytes
165    local size_bytes=$(stat -c %s "$backup_file" 2>/dev/null || stat -f %z "$backup_file" 2>/dev/null)
166    local size_mb=$((size_bytes / 1024 / 1024))
167
168    log_message "info" "Backup size: ${size_mb}MB"
169
170    if [ "$size_bytes" -eq 0 ]; then
171        log_message "error" "Backup file is empty (0 bytes)"
172        send_alert "[CRITICAL] Empty Backup File" \
173            "Backup file: $backup_file\nSize: 0 bytes\nHostname: $(hostname)\nDate: $(date)" \
174            "error"
175        return $EXIT_BACKUP_TOO_SMALL
176    fi
177
178    if [ "$size_mb" -lt "$min_size_mb" ]; then
179        log_message "error" "Backup file is too small: ${size_mb}MB (min: ${min_size_mb}MB)"
180        send_alert "[WARNING] Backup Size Below Threshold" \
181            "Backup file: $backup_file\nSize: ${size_mb}MB\nMinimum expected: ${min_size_mb}MB\nHostname: $(hostname)\nDate: $(date)" \
182            "warning"
183        return $EXIT_BACKUP_TOO_SMALL
184    fi
185
186    log_message "info" "Backup size verification passed"
187    return 0
188}
189
190verify_backup_integrity() {
191    local backup_file="$1"
192
193    log_message "info" "Verifying backup file integrity"
194
195    # Determine file type and verify
196    case "$backup_file" in
197        *.tar.gz|*.tgz)
198            if ! tar -tzf "$backup_file" >/dev/null 2>&1; then
199                log_message "error" "Backup archive is corrupted (tar verification failed)"
200                send_alert "[CRITICAL] Backup Corrupted" \
201                    "Backup file: $backup_file\nIntegrity check failed: tar verification failed\nHostname: $(hostname)\nDate: $(date)" \
202                    "error"
203                return $EXIT_VERIFICATION_FAILED
204            fi
205            ;;
206        *.tar)
207            if ! tar -tf "$backup_file" >/dev/null 2>&1; then
208                log_message "error" "Backup archive is corrupted (tar verification failed)"
209                send_alert "[CRITICAL] Backup Corrupted" \
210                    "Backup file: $backup_file\nIntegrity check failed: tar verification failed\nHostname: $(hostname)\nDate: $(date)" \
211                    "error"
212                return $EXIT_VERIFICATION_FAILED
213            fi
214            ;;
215        *.zip)
216            if command -v unzip >/dev/null 2>&1; then
217                if ! unzip -t "$backup_file" >/dev/null 2>&1; then
218                    log_message "error" "Backup archive is corrupted (zip verification failed)"
219                    send_alert "[CRITICAL] Backup Corrupted" \
220                        "Backup file: $backup_file\nIntegrity check failed: zip verification failed\nHostname: $(hostname)\nDate: $(date)" \
221                        "error"
222                    return $EXIT_VERIFICATION_FAILED
223                fi
224            else
225                log_message "warning" "unzip not available, skipping integrity check"
226            fi
227            ;;
228        *)
229            log_message "info" "File type not recognized for integrity check, skipping"
230            return 0
231            ;;
232    esac
233
234    log_message "info" "Backup integrity verification passed"
235    return 0
236}
237
238test_restore_sample() {
239    local backup_file="$1"
240    local restore_path="$2"
241
242    log_message "info" "Testing sample file restoration"
243
244    # Create restore test directory
245    mkdir -p "$restore_path" 2>/dev/null
246
247    if [ ! -d "$restore_path" ]; then
248        log_message "warning" "Cannot create restore test directory: $restore_path"
249        return 0  # Non-critical, continue
250    fi
251
252    # Extract a few files for testing
253    case "$backup_file" in
254        *.tar.gz|*.tgz)
255            if ! tar -xzf "$backup_file" -C "$restore_path" --strip-components=1 2>/dev/null; then
256                log_message "error" "Sample restore test failed (tar extraction failed)"
257                send_alert "[WARNING] Restore Test Failed" \
258                    "Backup file: $backup_file\nRestore test failed during extraction\nHostname: $(hostname)\nDate: $(date)" \
259                    "warning"
260                return $EXIT_RESTORE_FAILED
261            fi
262            ;;
263        *.tar)
264            if ! tar -xf "$backup_file" -C "$restore_path" --strip-components=1 2>/dev/null; then
265                log_message "error" "Sample restore test failed (tar extraction failed)"
266                send_alert "[WARNING] Restore Test Failed" \
267                    "Backup file: $backup_file\nRestore test failed during extraction\nHostname: $(hostname)\nDate: $(date)" \
268                    "warning"
269                return $EXIT_RESTORE_FAILED
270            fi
271            ;;
272        *.zip)
273            if command -v unzip >/dev/null 2>&1; then
274                if ! unzip -q "$backup_file" -d "$restore_path" 2>/dev/null; then
275                    log_message "error" "Sample restore test failed (zip extraction failed)"
276                    send_alert "[WARNING] Restore Test Failed" \
277                        "Backup file: $backup_file\nRestore test failed during extraction\nHostname: $(hostname)\nDate: $(date)" \
278                        "warning"
279                    return $EXIT_RESTORE_FAILED
280                fi
281            fi
282            ;;
283    esac
284
285    # Verify files were restored
286    local restored_count=$(find "$restore_path" -type f | wc -l)
287    if [ "$restored_count" -eq 0 ]; then
288        log_message "error" "No files were restored during test"
289        send_alert "[WARNING] Restore Test Failed" \
290            "Backup file: $backup_file\nNo files extracted during restore test\nHostname: $(hostname)\nDate: $(date)" \
291            "warning"
292        # Clean up
293        rm -rf "$restore_path"
294        return $EXIT_RESTORE_FAILED
295    fi
296
297    log_message "info" "Restore test passed: $restored_count file(s) extracted"
298
299    # Clean up test restore
300    rm -rf "$restore_path"
301
302    return 0
303}
304
305print_summary() {
306    local status="$1"
307    local backup_file="$2"
308
309    echo ""
310    echo -e "${BLUE}========================================${NC}"
311    echo -e "${BLUE} Backup Verification Summary${NC}"
312    echo -e "${BLUE}========================================${NC}"
313    echo ""
314    echo "Hostname: $(hostname)"
315    echo "Date: $(date)"
316    echo "Backup Path: $BACKUP_BASE_PATH"
317    echo "Latest Backup: $backup_file"
318    echo ""
319
320    if [ "$status" = "success" ]; then
321        echo -e "${GREEN}✓ All verification checks passed${NC}"
322        echo -e "${GREEN}✓ Backup is valid and ready for restore${NC}"
323    else
324        echo -e "${RED}✗ Verification failed - check logs above${NC}"
325        echo -e "${RED}✗ Backup may not be usable for restore${NC}"
326    fi
327    echo ""
328}
329
330show_help() {
331    cat << EOF
332Usage: $0 [OPTIONS]
333
334Backup Verification Script
335
336OPTIONS:
337    -p, --path PATH          Backup base path (default: /backup)
338    -n, --name NAME          Backup name/identifier
339    -a, --max-age HOURS      Maximum backup age in hours (default: 24)
340    -s, --min-size MB        Minimum backup size in MB (default: 10)
341    -t, --test-restore       Enable restore test
342    -r, --restore-path PATH  Restore test path (default: /tmp/restore_test)
343    -e, --email ADDRESS      Email for alerts
344    -m, --mail               Enable email alerts
345    -w, --webhook URL        Webhook URL for alerts
346    --pattern PATTERN        File pattern to search (default: *.tar.gz)
347    -h, --help               Display this help message
348
349EXAMPLES:
350    # Basic verification
351    $0 --path /backup
352
353    # With email alerts
354    $0 --path /backup --email admin@example.com --mail
355
356    # With restore test
357    $0 --path /backup --test-restore
358
359    # Custom thresholds
360    $0 --path /backup --max-age 12 --min-size 100
361
362ENVIRONMENT VARIABLES:
363    BACKUP_BASE_PATH     Backup directory path
364    MAX_AGE_HOURS        Maximum backup age in hours
365    MIN_SIZE_MB          Minimum backup size in MB
366    TEST_RESTORE         Enable restore test (true/false)
367    ALERT_EMAIL          Email address for alerts
368    SEND_EMAIL           Enable email alerts (true/false)
369    WEBHOOK_URL          Webhook URL for alerts
370
371EOF
372    exit 0
373}
374
375#############################################################################
376# Main Script
377#############################################################################
378
379# Parse command line arguments
380while [[ $# -gt 0 ]]; do
381    case $1 in
382        -p|--path)
383            BACKUP_BASE_PATH="$2"
384            shift 2
385            ;;
386        -n|--name)
387            BACKUP_NAME="$2"
388            shift 2
389            ;;
390        -a|--max-age)
391            MAX_AGE_HOURS="$2"
392            shift 2
393            ;;
394        -s|--min-size)
395            MIN_SIZE_MB="$2"
396            shift 2
397            ;;
398        -t|--test-restore)
399            TEST_RESTORE=true
400            shift
401            ;;
402        -r|--restore-path)
403            RESTORE_TEST_PATH="$2"
404            shift 2
405            ;;
406        -e|--email)
407            ALERT_EMAIL="$2"
408            SEND_EMAIL=true
409            shift 2
410            ;;
411        -m|--mail)
412            SEND_EMAIL=true
413            shift
414            ;;
415        -w|--webhook)
416            WEBHOOK_URL="$2"
417            shift 2
418            ;;
419        --pattern)
420            BACKUP_PATTERN="$2"
421            shift 2
422            ;;
423        -h|--help)
424            show_help
425            ;;
426        *)
427            echo "Unknown option: $1"
428            echo "Use -h or --help for usage information"
429            exit 1
430            ;;
431    esac
432done
433
434# Main execution
435echo -e "${BLUE}========================================${NC}"
436echo -e "${BLUE} Backup Verification${NC}"
437echo -e "${BLUE}========================================${NC}"
438echo ""
439
440VERIFICATION_STATUS="success"
441EXIT_CODE=$EXIT_SUCCESS
442
443# Step 1: Check backup exists
444if ! verify_backup_exists; then
445    VERIFICATION_STATUS="failed"
446    EXIT_CODE=$EXIT_NO_BACKUP_FOUND
447    print_summary "$VERIFICATION_STATUS" "N/A"
448    exit $EXIT_CODE
449fi
450
451# Step 2: Verify backup age
452if ! verify_backup_age "$LATEST_BACKUP" "$MAX_AGE_HOURS"; then
453    VERIFICATION_STATUS="failed"
454    EXIT_CODE=$EXIT_BACKUP_TOO_OLD
455fi
456
457# Step 3: Verify backup size
458if ! verify_backup_size "$LATEST_BACKUP" "$MIN_SIZE_MB"; then
459    VERIFICATION_STATUS="failed"
460    EXIT_CODE=$EXIT_BACKUP_TOO_SMALL
461fi
462
463# Step 4: Verify backup integrity
464if ! verify_backup_integrity "$LATEST_BACKUP"; then
465    VERIFICATION_STATUS="failed"
466    EXIT_CODE=$EXIT_VERIFICATION_FAILED
467fi
468
469# Step 5: Optional restore test
470if [ "$TEST_RESTORE" = true ]; then
471    if ! test_restore_sample "$LATEST_BACKUP" "$RESTORE_TEST_PATH"; then
472        VERIFICATION_STATUS="failed"
473        EXIT_CODE=$EXIT_RESTORE_FAILED
474    fi
475fi
476
477# Print summary
478print_summary "$VERIFICATION_STATUS" "$LATEST_BACKUP"
479
480# Send success notification if all checks passed
481if [ "$VERIFICATION_STATUS" = "success" ]; then
482    send_alert "[SUCCESS] Backup Verification Passed" \
483        "Backup file: $LATEST_BACKUP\nAll verification checks passed\nHostname: $(hostname)\nDate: $(date)" \
484        "success"
485fi
486
487log_message "info" "Backup verification completed with status: $VERIFICATION_STATUS"
488
489exit $EXIT_CODE

The Script (PowerShell)

Lang: powershell
  1<#
  2.SYNOPSIS
  3    Backup Verification Script for Windows
  4
  5.DESCRIPTION
  6    Verifies backup completion, file integrity, and sends alerts on failures.
  7
  8    Features:
  9    - Check if backup completed successfully
 10    - Verify backup file exists and is recent
 11    - Check backup file size (not 0 bytes)
 12    - Test restore of sample files (optional)
 13    - Send alert if backup failed
 14
 15.PARAMETER BackupPath
 16    Path to backup directory
 17
 18.PARAMETER MaxAgeHours
 19    Maximum backup age in hours (default: 24)
 20
 21.PARAMETER MinSizeMB
 22    Minimum backup size in MB (default: 10)
 23
 24.PARAMETER TestRestore
 25    Enable restore test
 26
 27.PARAMETER RestoreTestPath
 28    Path for restore testing
 29
 30.PARAMETER EmailTo
 31    Email address for alerts
 32
 33.PARAMETER SmtpServer
 34    SMTP server for email alerts
 35
 36.PARAMETER WebhookUrl
 37    Webhook URL for alerts
 38
 39.PARAMETER BackupPattern
 40    File pattern to search (default: *.zip)
 41
 42.EXAMPLE
 43    .\Backup-Verification.ps1 -BackupPath "D:\Backups"
 44
 45.EXAMPLE
 46    .\Backup-Verification.ps1 -BackupPath "D:\Backups" -TestRestore -EmailTo "admin@example.com"
 47
 48.NOTES
 49    Author: glyph.sh
 50    Reference: https://glyph.sh/scripts/backup-verification/
 51#>
 52
 53[CmdletBinding()]
 54param(
 55    [Parameter(Mandatory=$false)]
 56    [string]$BackupPath = "C:\Backups",
 57
 58    [Parameter(Mandatory=$false)]
 59    [int]$MaxAgeHours = 24,
 60
 61    [Parameter(Mandatory=$false)]
 62    [int]$MinSizeMB = 10,
 63
 64    [Parameter(Mandatory=$false)]
 65    [switch]$TestRestore,
 66
 67    [Parameter(Mandatory=$false)]
 68    [string]$RestoreTestPath = "$env:TEMP\restore_test",
 69
 70    [Parameter(Mandatory=$false)]
 71    [string]$EmailTo,
 72
 73    [Parameter(Mandatory=$false)]
 74    [string]$SmtpServer,
 75
 76    [Parameter(Mandatory=$false)]
 77    [string]$WebhookUrl,
 78
 79    [Parameter(Mandatory=$false)]
 80    [string]$BackupPattern = "*.zip"
 81)
 82
 83# Exit codes
 84$ExitSuccess = 0
 85$ExitNoBackupFound = 1
 86$ExitBackupTooOld = 2
 87$ExitBackupTooSmall = 3
 88$ExitRestoreFailed = 4
 89$ExitVerificationFailed = 5
 90
 91# Initialize status
 92$VerificationStatus = "success"
 93$ExitCode = $ExitSuccess
 94$LatestBackup = $null
 95
 96#############################################################################
 97# Functions
 98#############################################################################
 99
100function Write-Log {
101    param(
102        [Parameter(Mandatory=$true)]
103        [ValidateSet('Info', 'Warning', 'Error')]
104        [string]$Level,
105
106        [Parameter(Mandatory=$true)]
107        [string]$Message
108    )
109
110    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
111    $color = switch ($Level) {
112        'Info'    { 'Green' }
113        'Warning' { 'Yellow' }
114        'Error'   { 'Red' }
115    }
116
117    Write-Host "[$Level] [$timestamp] $Message" -ForegroundColor $color
118
119    # Write to Windows Event Log
120    try {
121        $eventSource = "BackupVerification"
122        if (-not [System.Diagnostics.EventLog]::SourceExists($eventSource)) {
123            New-EventLog -LogName Application -Source $eventSource
124        }
125
126        $eventType = switch ($Level) {
127            'Info'    { 'Information' }
128            'Warning' { 'Warning' }
129            'Error'   { 'Error' }
130        }
131
132        Write-EventLog -LogName Application -Source $eventSource -EntryType $eventType -EventId 1000 -Message $Message
133    } catch {
134        # Silently continue if event log write fails
135    }
136}
137
138function Send-Alert {
139    param(
140        [Parameter(Mandatory=$true)]
141        [string]$Subject,
142
143        [Parameter(Mandatory=$true)]
144        [string]$Message,
145
146        [Parameter(Mandatory=$true)]
147        [ValidateSet('success', 'warning', 'error')]
148        [string]$Severity
149    )
150
151    # Email alert
152    if ($EmailTo -and $SmtpServer) {
153        try {
154            Send-MailMessage -To $EmailTo `
155                -From "backup-verification@$env:COMPUTERNAME" `
156                -Subject $Subject `
157                -Body $Message `
158                -SmtpServer $SmtpServer `
159                -ErrorAction Stop
160            Write-Log -Level Info -Message "Email alert sent to $EmailTo"
161        } catch {
162            Write-Log -Level Warning -Message "Failed to send email: $_"
163        }
164    }
165
166    # Webhook alert
167    if ($WebhookUrl) {
168        try {
169            $emoji = switch ($Severity) {
170                'success' { '✅' }
171                'warning' { '⚠️' }
172                'error'   { '❌' }
173            }
174
175            $body = @{
176                text = "$emoji $Subject`n`n$Message"
177            } | ConvertTo-Json
178
179            Invoke-RestMethod -Uri $WebhookUrl -Method Post -Body $body -ContentType 'application/json' -ErrorAction Stop
180            Write-Log -Level Info -Message "Webhook alert sent"
181        } catch {
182            Write-Log -Level Warning -Message "Failed to send webhook: $_"
183        }
184    }
185}
186
187function Find-LatestBackup {
188    param(
189        [Parameter(Mandatory=$true)]
190        [string]$Path,
191
192        [Parameter(Mandatory=$true)]
193        [string]$Pattern
194    )
195
196    if (-not (Test-Path $Path)) {
197        Write-Log -Level Error -Message "Backup path does not exist: $Path"
198        return $null
199    }
200
201    $backupFiles = Get-ChildItem -Path $Path -Filter $Pattern -File -ErrorAction SilentlyContinue |
202        Sort-Object LastWriteTime -Descending |
203        Select-Object -First 1
204
205    return $backupFiles
206}
207
208function Test-BackupAge {
209    param(
210        [Parameter(Mandatory=$true)]
211        [System.IO.FileInfo]$BackupFile,
212
213        [Parameter(Mandatory=$true)]
214        [int]$MaxAgeHours
215    )
216
217    Write-Log -Level Info -Message "Verifying backup age (max: ${MaxAgeHours}h)"
218
219    $fileAge = (Get-Date) - $BackupFile.LastWriteTime
220    $ageHours = [math]::Round($fileAge.TotalHours, 1)
221
222    Write-Log -Level Info -Message "Backup age: ${ageHours}h"
223
224    if ($fileAge.TotalHours -gt $MaxAgeHours) {
225        Write-Log -Level Error -Message "Backup is too old: ${ageHours}h (max: ${MaxAgeHours}h)"
226        Send-Alert -Subject "[CRITICAL] Backup Too Old" `
227            -Message "Backup file: $($BackupFile.FullName)`nAge: $ageHours hours`nMaximum allowed: $MaxAgeHours hours`nHostname: $env:COMPUTERNAME`nDate: $(Get-Date)" `
228            -Severity "error"
229        return $false
230    }
231
232    Write-Log -Level Info -Message "Backup age verification passed"
233    return $true
234}
235
236function Test-BackupSize {
237    param(
238        [Parameter(Mandatory=$true)]
239        [System.IO.FileInfo]$BackupFile,
240
241        [Parameter(Mandatory=$true)]
242        [int]$MinSizeMB
243    )
244
245    Write-Log -Level Info -Message "Verifying backup size (min: ${MinSizeMB}MB)"
246
247    $sizeMB = [math]::Round($BackupFile.Length / 1MB, 2)
248
249    Write-Log -Level Info -Message "Backup size: ${sizeMB}MB"
250
251    if ($BackupFile.Length -eq 0) {
252        Write-Log -Level Error -Message "Backup file is empty (0 bytes)"
253        Send-Alert -Subject "[CRITICAL] Empty Backup File" `
254            -Message "Backup file: $($BackupFile.FullName)`nSize: 0 bytes`nHostname: $env:COMPUTERNAME`nDate: $(Get-Date)" `
255            -Severity "error"
256        return $false
257    }
258
259    if ($sizeMB -lt $MinSizeMB) {
260        Write-Log -Level Error -Message "Backup file is too small: ${sizeMB}MB (min: ${MinSizeMB}MB)"
261        Send-Alert -Subject "[WARNING] Backup Size Below Threshold" `
262            -Message "Backup file: $($BackupFile.FullName)`nSize: ${sizeMB}MB`nMinimum expected: ${MinSizeMB}MB`nHostname: $env:COMPUTERNAME`nDate: $(Get-Date)" `
263            -Severity "warning"
264        return $false
265    }
266
267    Write-Log -Level Info -Message "Backup size verification passed"
268    return $true
269}
270
271function Test-BackupIntegrity {
272    param(
273        [Parameter(Mandatory=$true)]
274        [System.IO.FileInfo]$BackupFile
275    )
276
277    Write-Log -Level Info -Message "Verifying backup file integrity"
278
279    # Test ZIP file integrity
280    if ($BackupFile.Extension -eq '.zip') {
281        try {
282            Add-Type -AssemblyName System.IO.Compression.FileSystem
283            $zip = [System.IO.Compression.ZipFile]::OpenRead($BackupFile.FullName)
284            $entryCount = $zip.Entries.Count
285            $zip.Dispose()
286
287            if ($entryCount -eq 0) {
288                Write-Log -Level Error -Message "Backup archive is empty (0 entries)"
289                Send-Alert -Subject "[CRITICAL] Backup Corrupted" `
290                    -Message "Backup file: $($BackupFile.FullName)`nIntegrity check failed: archive contains no entries`nHostname: $env:COMPUTERNAME`nDate: $(Get-Date)" `
291                    -Severity "error"
292                return $false
293            }
294
295            Write-Log -Level Info -Message "Backup integrity verification passed ($entryCount entries)"
296            return $true
297        } catch {
298            Write-Log -Level Error -Message "Backup archive is corrupted: $_"
299            Send-Alert -Subject "[CRITICAL] Backup Corrupted" `
300                -Message "Backup file: $($BackupFile.FullName)`nIntegrity check failed: $_`nHostname: $env:COMPUTERNAME`nDate: $(Get-Date)" `
301                -Severity "error"
302            return $false
303        }
304    }
305
306    Write-Log -Level Info -Message "File type not recognized for integrity check, skipping"
307    return $true
308}
309
310function Test-RestoreSample {
311    param(
312        [Parameter(Mandatory=$true)]
313        [System.IO.FileInfo]$BackupFile,
314
315        [Parameter(Mandatory=$true)]
316        [string]$RestorePath
317    )
318
319    Write-Log -Level Info -Message "Testing sample file restoration"
320
321    # Create restore test directory
322    if (Test-Path $RestorePath) {
323        Remove-Item -Path $RestorePath -Recurse -Force -ErrorAction SilentlyContinue
324    }
325    New-Item -Path $RestorePath -ItemType Directory -Force | Out-Null
326
327    try {
328        if ($BackupFile.Extension -eq '.zip') {
329            Add-Type -AssemblyName System.IO.Compression.FileSystem
330
331            # Extract first 5 files as a sample
332            $zip = [System.IO.Compression.ZipFile]::OpenRead($BackupFile.FullName)
333            $sampleEntries = $zip.Entries | Select-Object -First 5
334
335            foreach ($entry in $sampleEntries) {
336                if ($entry.FullName -notlike "*/") {  # Skip directories
337                    $destinationPath = Join-Path $RestorePath $entry.Name
338                    [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $destinationPath, $true)
339                }
340            }
341
342            $zip.Dispose()
343        } else {
344            Write-Log -Level Warning -Message "Restore test not supported for this file type"
345            return $true
346        }
347
348        # Verify files were restored
349        $restoredCount = (Get-ChildItem -Path $RestorePath -File -Recurse).Count
350
351        if ($restoredCount -eq 0) {
352            Write-Log -Level Error -Message "No files were restored during test"
353            Send-Alert -Subject "[WARNING] Restore Test Failed" `
354                -Message "Backup file: $($BackupFile.FullName)`nNo files extracted during restore test`nHostname: $env:COMPUTERNAME`nDate: $(Get-Date)" `
355                -Severity "warning"
356            return $false
357        }
358
359        Write-Log -Level Info -Message "Restore test passed: $restoredCount file(s) extracted"
360        return $true
361    } catch {
362        Write-Log -Level Error -Message "Restore test failed: $_"
363        Send-Alert -Subject "[WARNING] Restore Test Failed" `
364            -Message "Backup file: $($BackupFile.FullName)`nRestore test failed: $_`nHostname: $env:COMPUTERNAME`nDate: $(Get-Date)" `
365            -Severity "warning"
366        return $false
367    } finally {
368        # Clean up
369        if (Test-Path $RestorePath) {
370            Remove-Item -Path $RestorePath -Recurse -Force -ErrorAction SilentlyContinue
371        }
372    }
373}
374
375function Show-Summary {
376    param(
377        [Parameter(Mandatory=$true)]
378        [string]$Status,
379
380        [Parameter(Mandatory=$false)]
381        [System.IO.FileInfo]$BackupFile
382    )
383
384    Write-Host "`n========================================" -ForegroundColor Cyan
385    Write-Host " Backup Verification Summary" -ForegroundColor Cyan
386    Write-Host "========================================`n" -ForegroundColor Cyan
387
388    Write-Host "Hostname: $env:COMPUTERNAME"
389    Write-Host "Date: $(Get-Date)"
390    Write-Host "Backup Path: $BackupPath"
391    if ($BackupFile) {
392        Write-Host "Latest Backup: $($BackupFile.FullName)"
393    } else {
394        Write-Host "Latest Backup: N/A"
395    }
396    Write-Host ""
397
398    if ($Status -eq "success") {
399        Write-Host "✓ All verification checks passed" -ForegroundColor Green
400        Write-Host "✓ Backup is valid and ready for restore" -ForegroundColor Green
401    } else {
402        Write-Host "✗ Verification failed - check logs above" -ForegroundColor Red
403        Write-Host "✗ Backup may not be usable for restore" -ForegroundColor Red
404    }
405    Write-Host ""
406}
407
408#############################################################################
409# Main Script
410#############################################################################
411
412Write-Host "========================================" -ForegroundColor Cyan
413Write-Host " Backup Verification" -ForegroundColor Cyan
414Write-Host "========================================`n" -ForegroundColor Cyan
415
416# Step 1: Find latest backup
417Write-Log -Level Info -Message "Checking for backup files in $BackupPath"
418$LatestBackup = Find-LatestBackup -Path $BackupPath -Pattern $BackupPattern
419
420if (-not $LatestBackup) {
421    Write-Log -Level Error -Message "No backup files found matching pattern: $BackupPattern"
422    Send-Alert -Subject "[CRITICAL] Backup Verification Failed" `
423        -Message "No backup files found in $BackupPath`nPattern: $BackupPattern`nHostname: $env:COMPUTERNAME`nDate: $(Get-Date)" `
424        -Severity "error"
425    $VerificationStatus = "failed"
426    $ExitCode = $ExitNoBackupFound
427    Show-Summary -Status $VerificationStatus -BackupFile $null
428    exit $ExitCode
429}
430
431Write-Log -Level Info -Message "Found backup: $($LatestBackup.FullName)"
432
433# Step 2: Verify backup age
434if (-not (Test-BackupAge -BackupFile $LatestBackup -MaxAgeHours $MaxAgeHours)) {
435    $VerificationStatus = "failed"
436    $ExitCode = $ExitBackupTooOld
437}
438
439# Step 3: Verify backup size
440if (-not (Test-BackupSize -BackupFile $LatestBackup -MinSizeMB $MinSizeMB)) {
441    $VerificationStatus = "failed"
442    $ExitCode = $ExitBackupTooSmall
443}
444
445# Step 4: Verify backup integrity
446if (-not (Test-BackupIntegrity -BackupFile $LatestBackup)) {
447    $VerificationStatus = "failed"
448    $ExitCode = $ExitVerificationFailed
449}
450
451# Step 5: Optional restore test
452if ($TestRestore) {
453    if (-not (Test-RestoreSample -BackupFile $LatestBackup -RestorePath $RestoreTestPath)) {
454        $VerificationStatus = "failed"
455        $ExitCode = $ExitRestoreFailed
456    }
457}
458
459# Show summary
460Show-Summary -Status $VerificationStatus -BackupFile $LatestBackup
461
462# Send success notification
463if ($VerificationStatus -eq "success") {
464    Send-Alert -Subject "[SUCCESS] Backup Verification Passed" `
465        -Message "Backup file: $($LatestBackup.FullName)`nAll verification checks passed`nHostname: $env:COMPUTERNAME`nDate: $(Get-Date)" `
466        -Severity "success"
467}
468
469Write-Log -Level Info -Message "Backup verification completed with status: $VerificationStatus"
470
471exit $ExitCode

Usage

Bash Usage

Basic Verification

Lang: bash
1chmod +x backup-verification.sh
2./backup-verification.sh --path /backup

With Email Alerts

Lang: bash
1./backup-verification.sh \
2    --path /backup \
3    --email admin@example.com \
4    --mail

With Restore Test

Lang: bash
1./backup-verification.sh \
2    --path /backup \
3    --test-restore \
4    --restore-path /tmp/restore_test

Custom Thresholds

Lang: bash
1./backup-verification.sh \
2    --path /backup \
3    --max-age 12 \
4    --min-size 100 \
5    --pattern "*.tar.gz"

With Webhook (Slack/Discord)

Lang: bash
1./backup-verification.sh \
2    --path /backup \
3    --webhook "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"

PowerShell Usage

Basic Verification

Lang: powershell
1.\Backup-Verification.ps1 -BackupPath "D:\Backups"

With Email Alerts

Lang: powershell
1.\Backup-Verification.ps1 `
2    -BackupPath "D:\Backups" `
3    -EmailTo "admin@example.com" `
4    -SmtpServer "smtp.example.com"

With Restore Test

Lang: powershell
1.\Backup-Verification.ps1 `
2    -BackupPath "D:\Backups" `
3    -TestRestore `
4    -RestoreTestPath "C:\Temp\restore_test"

Custom Thresholds

Lang: powershell
1.\Backup-Verification.ps1 `
2    -BackupPath "D:\Backups" `
3    -MaxAgeHours 12 `
4    -MinSizeMB 100 `
5    -BackupPattern "*.zip"

What It Does

  1. Finds Latest Backup: Locates the most recent backup file matching the specified pattern
  2. Verifies Age: Ensures backup was created within the allowed time window
  3. Checks Size: Validates backup is not empty and meets minimum size requirements
  4. Tests Integrity: Verifies archive can be read and is not corrupted
  5. Optional Restore Test: Extracts sample files to verify backup is restorable
  6. Sends Alerts: Notifies administrators via email, webhook, or syslog on failures

Automated Verification

Linux Cron Job

Lang: bash
1# Edit crontab
2crontab -e
3
4# Verify backups daily at 6 AM
50 6 * * * /usr/local/bin/backup-verification.sh --path /backup --email admin@example.com --mail
6
7# Verify with restore test weekly
80 6 * * 0 /usr/local/bin/backup-verification.sh --path /backup --test-restore --email admin@example.com --mail

Windows Task Scheduler

Lang: powershell
 1# Create scheduled task for daily verification
 2$action = New-ScheduledTaskAction -Execute "PowerShell.exe" `
 3    -Argument "-NoProfile -ExecutionPolicy Bypass -File C:\Scripts\Backup-Verification.ps1 -BackupPath D:\Backups -EmailTo admin@example.com -SmtpServer smtp.example.com"
 4
 5$trigger = New-ScheduledTaskTrigger -Daily -At 6:00AM
 6
 7$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
 8
 9Register-ScheduledTask -TaskName "Backup Verification" `
10    -Action $action `
11    -Trigger $trigger `
12    -Principal $principal `
13    -Description "Daily backup verification"

Verification Checks Explained

Age Check

Ensures backups are running on schedule. A backup older than expected indicates:

  • Backup job failed to run
  • Backup schedule needs updating
  • Backup system is offline

Size Check

Validates backup contains data. Zero-byte or unexpectedly small files indicate:

  • Backup process crashed mid-execution
  • Source data is missing or inaccessible
  • Backup compression failed

Integrity Check

Confirms backup file is not corrupted and can be read:

  • Tests archive can be opened
  • Validates file structure
  • Prevents restore failures

Restore Test

Most comprehensive check - actually extracts files:

  • Confirms data can be recovered
  • Tests extraction process
  • Validates backup usability

Common Issues Prevented

  • Discovering backup failures before data loss occurs
  • Identifying corrupted backups before they’re needed
  • Detecting storage space issues affecting backups
  • Finding scheduling problems early
  • Validating backup processes are working correctly
  • Meeting compliance requirements for backup verification

Integration Examples

Slack Webhook

Lang: bash
1# Get webhook URL from Slack (Incoming Webhooks app)
2WEBHOOK="https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXX"
3
4./backup-verification.sh \
5    --path /backup \
6    --webhook "$WEBHOOK"

Email with SMTP

Lang: bash
1# Configure system mail first
2# Edit /etc/postfix/main.cf or use msmtp
3
4./backup-verification.sh \
5    --path /backup \
6    --email admin@example.com \
7    --mail

Monitoring Integration

Lang: bash
1# Export metrics for Prometheus/Grafana
2./backup-verification.sh --path /backup > /var/lib/node_exporter/backup_status.prom
3
4# Or send to monitoring system
5if ./backup-verification.sh --path /backup; then
6    curl -X POST "http://monitoring.example.com/api/metrics" \
7        -d '{"backup_status": "success", "timestamp": "'$(date -u +%s)'"}'
8fi

Exit Codes

CodeMeaning
0Success - all checks passed
1No backup found
2Backup too old
3Backup too small or empty
4Restore test failed
5Integrity verification failed

Use exit codes for automation and monitoring:

Lang: bash
 1#!/bin/bash
 2./backup-verification.sh --path /backup
 3EXIT_CODE=$?
 4
 5if [ $EXIT_CODE -eq 0 ]; then
 6    echo "Backup verified successfully"
 7elif [ $EXIT_CODE -eq 1 ]; then
 8    echo "CRITICAL: No backup found!"
 9    # Trigger emergency backup
10else
11    echo "WARNING: Backup verification failed with code $EXIT_CODE"
12fi

See Also

Download

Bash Version

Lang: bash
1curl -O https://glyph.sh/scripts/backup-verification.sh
2chmod +x backup-verification.sh
3./backup-verification.sh --help

PowerShell Version

Lang: powershell
1Invoke-WebRequest -Uri "https://glyph.sh/scripts/Backup-Verification.ps1" -OutFile "Backup-Verification.ps1"
2Get-Help .\Backup-Verification.ps1 -Full