Files
chiefgeek a1a17e81a1 Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist,
same-site referer bypass, Blackbox-Exporter allowed bot, and all
new exporters, cheat sheets, and automation scripts.
2026-05-25 03:31:08 +02:00

443 lines
15 KiB
Bash

#!/usr/bin/env bash
#########################################################################################
#### backup-verify.sh — Verify backup files exist, are recent, and aren't zero-size ####
#### Checks file existence, size, and modification time against thresholds ####
#### ####
#### Author: Phil Connor ####
#### Contact: contact@mylinux.work ####
#### License: MIT ####
#### Version 1.00 ####
#### ####
#### Usage: ####
#### ./backup-verify.sh /var/backups/*.tar.gz ####
#### ./backup-verify.sh --config backup-paths.conf ####
#### ####
#### See --help for all options. ####
#########################################################################################
set -euo pipefail
# ── Defaults ──────────────────────────────────────────────────────────
BACKUP_MAX_AGE="${BACKUP_MAX_AGE:-24h}"
BACKUP_MIN_SIZE="${BACKUP_MIN_SIZE:-1}"
VERBOSE="${VERBOSE:-false}"
COLOR="${COLOR:-auto}"
CONFIG_FILE=""
PATH_FILE=""
# ── State ─────────────────────────────────────────────────────────────
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
ENTRIES=()
COUNT_TOTAL=0
COUNT_OK=0
COUNT_WARNING=0
COUNT_CRITICAL=0
# ── Colors ────────────────────────────────────────────────────────────
setup_colors() {
if [[ "$COLOR" == "never" ]]; then
RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" RESET=""
return
fi
if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
RESET='\033[0m'
else
RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" RESET=""
fi
}
# ── Logging ───────────────────────────────────────────────────────────
log() { echo -e "${CYAN}[INFO]${RESET} $*"; }
warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${DIM}[DEBUG]${RESET} $*"; fi; }
# ── Helpers ───────────────────────────────────────────────────────────
section_header() {
echo ""
echo -e " ${BOLD}${CYAN}── $1 ──${RESET}"
echo ""
}
field() {
printf " ${BOLD}%-22s${RESET} %s\n" "$1" "$2"
}
field_color() {
printf " ${BOLD}%-22s${RESET} %b\n" "$1" "$2"
}
human_bytes() {
local bytes="$1"
if [[ "$bytes" -ge 1073741824 ]]; then
awk "BEGIN { printf \"%.1f GiB\", $bytes / 1073741824 }"
elif [[ "$bytes" -ge 1048576 ]]; then
awk "BEGIN { printf \"%.1f MiB\", $bytes / 1048576 }"
elif [[ "$bytes" -ge 1024 ]]; then
awk "BEGIN { printf \"%.1f KiB\", $bytes / 1024 }"
else
echo "${bytes} B"
fi
}
human_age() {
local seconds="$1"
local days=$((seconds / 86400))
local hours=$(( (seconds % 86400) / 3600 ))
local mins=$(( (seconds % 3600) / 60 ))
if [[ "$days" -gt 0 ]]; then
echo "${days}d ${hours}h"
elif [[ "$hours" -gt 0 ]]; then
echo "${hours}h ${mins}m"
else
echo "${mins}m"
fi
}
# Convert age string (24h, 7d, 2w) to seconds
parse_age_to_seconds() {
local age="$1"
local num="${age%[hdw]*}"
local unit="${age##*[0-9]}"
case "$unit" in
h) echo $((num * 3600)) ;;
d) echo $((num * 86400)) ;;
w) echo $((num * 604800)) ;;
*) echo $((num * 3600)) ;;
esac
}
# Convert size string (1, 1K, 1M, 1G) to bytes
parse_size_to_bytes() {
local size="$1"
# Pure number
if [[ "$size" =~ ^[0-9]+$ ]]; then
echo "$size"
return
fi
local num="${size%[KkMmGg]*}"
local unit="${size##*[0-9]}"
case "${unit^^}" in
K) echo $((num * 1024)) ;;
M) echo $((num * 1048576)) ;;
G) echo $((num * 1073741824)) ;;
*) echo "$num" ;;
esac
}
# ══════════════════════════════════════════════════════════════════════
# VERIFICATION
# ══════════════════════════════════════════════════════════════════════
verify_file() {
local file="$1"
local max_age_str="$2"
local min_size_str="$3"
local max_age_secs min_size_bytes
max_age_secs=$(parse_age_to_seconds "$max_age_str")
min_size_bytes=$(parse_size_to_bytes "$min_size_str")
COUNT_TOTAL=$((COUNT_TOTAL + 1))
verbose "Checking: ${file} (max-age=${max_age_str}, min-size=${min_size_str})"
# Check existence
if [[ ! -e "$file" ]]; then
printf " %b%-50s %10s %10s %s%b\n" "$RED" "$file" "--" "--" "MISSING" "$RESET"
COUNT_CRITICAL=$((COUNT_CRITICAL + 1))
return
fi
# Get file info
local file_size file_mtime now_epoch age_secs
file_size=$(stat -c%s "$file" 2>/dev/null || echo "0")
file_mtime=$(stat -c%Y "$file" 2>/dev/null || echo "0")
now_epoch=$(date +%s)
age_secs=$((now_epoch - file_mtime))
local size_str age_str
size_str=$(human_bytes "$file_size")
age_str=$(human_age "$age_secs")
# Check zero-size
if [[ "$file_size" -eq 0 ]]; then
printf " %b%-50s %10s %10s %s%b\n" "$RED" "$file" "$size_str" "$age_str" "EMPTY" "$RESET"
COUNT_CRITICAL=$((COUNT_CRITICAL + 1))
return
fi
# Check minimum size
if [[ "$file_size" -lt "$min_size_bytes" ]]; then
printf " %b%-50s %10s %10s %s%b\n" "$YELLOW" "$file" "$size_str" "$age_str" "SMALL" "$RESET"
COUNT_WARNING=$((COUNT_WARNING + 1))
return
fi
# Check age
if [[ "$age_secs" -gt "$max_age_secs" ]]; then
printf " %b%-50s %10s %10s %s%b\n" "$YELLOW" "$file" "$size_str" "$age_str" "STALE" "$RESET"
COUNT_WARNING=$((COUNT_WARNING + 1))
return
fi
# All good
printf " %b%-50s %10s %10s %s%b\n" "$GREEN" "$file" "$size_str" "$age_str" "OK" "$RESET"
COUNT_OK=$((COUNT_OK + 1))
}
verify_glob() {
local pattern="$1"
local max_age_str="$2"
local min_size_str="$3"
local found=false
# Use compgen to safely expand globs
local files
files=$(compgen -G "$pattern" 2>/dev/null || true)
if [[ -z "$files" ]]; then
COUNT_TOTAL=$((COUNT_TOTAL + 1))
COUNT_CRITICAL=$((COUNT_CRITICAL + 1))
printf " %b%-50s %10s %10s %s%b\n" "$RED" "$pattern" "--" "--" "MISSING" "$RESET"
return
fi
while IFS= read -r file; do
found=true
verify_file "$file" "$max_age_str" "$min_size_str"
done <<< "$files"
if [[ "$found" == "false" ]]; then
COUNT_TOTAL=$((COUNT_TOTAL + 1))
COUNT_CRITICAL=$((COUNT_CRITICAL + 1))
printf " %b%-50s %10s %10s %s%b\n" "$RED" "$pattern" "--" "--" "MISSING" "$RESET"
fi
}
# ══════════════════════════════════════════════════════════════════════
# INPUT PARSING
# ══════════════════════════════════════════════════════════════════════
add_entry() {
local path="$1"
local max_age="${2:-$BACKUP_MAX_AGE}"
local min_size="${3:-$BACKUP_MIN_SIZE}"
ENTRIES+=("${path}|${max_age}|${min_size}")
}
load_config_file() {
local file="$1"
if [[ ! -f "$file" ]]; then
err "Config file not found: $file"
exit 1
fi
while IFS= read -r line; do
line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
[[ -z "$line" || "$line" == \#* ]] && continue
local path max_age
path=$(echo "$line" | awk '{print $1}')
max_age=$(echo "$line" | awk '{print $2}')
if [[ -z "$max_age" ]]; then
max_age="$BACKUP_MAX_AGE"
fi
add_entry "$path" "$max_age"
done < "$file"
}
load_paths_from_file() {
local file="$1"
if [[ ! -f "$file" ]]; then
err "File not found: $file"
exit 1
fi
while IFS= read -r line; do
line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
[[ -z "$line" || "$line" == \#* ]] && continue
add_entry "$line"
done < "$file"
}
load_paths_from_stdin() {
while IFS= read -r line; do
line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
[[ -z "$line" || "$line" == \#* ]] && continue
add_entry "$line"
done
}
# ══════════════════════════════════════════════════════════════════════
# USAGE
# ══════════════════════════════════════════════════════════════════════
usage() {
cat <<EOF
${SCRIPT_NAME} — Verify backup files exist, are recent, and aren't zero-size
USAGE:
${SCRIPT_NAME} [OPTIONS] [PATH/GLOB ...]
${SCRIPT_NAME} --config backup-paths.conf
${SCRIPT_NAME} --file paths.txt
OPTIONS:
--config FILE Config file with paths and optional max-age per line
Format: /path/to/backup/*.tar.gz 24h
--file FILE Read paths from file (one per line)
--max-age AGE Maximum file age (default: 24h)
Accepts: h (hours), d (days), w (weeks)
--min-size SIZE Minimum expected file size (default: 1)
Accepts: K, M, G suffixes
--verbose Enable debug output
--no-color Disable colored output
--help Show this help
ENVIRONMENT VARIABLES:
BACKUP_MAX_AGE Maximum file age (default: 24h)
BACKUP_MIN_SIZE Minimum file size (default: 1)
CONFIG FILE FORMAT:
# One path/glob per line, optional max-age
/var/backups/db/*.sql.gz 12h
/var/backups/files/*.tar.gz 24h
/opt/backups/weekly/*.tar 7d
EXAMPLES:
# Check specific backup files
./backup-verify.sh /var/backups/db-latest.sql.gz
# Check with glob patterns
./backup-verify.sh /var/backups/*.tar.gz
# Use config file with per-path thresholds
./backup-verify.sh --config /etc/backup-verify.conf
# Custom thresholds
./backup-verify.sh --max-age 48h --min-size 1M /var/backups/*.tar.gz
EOF
}
# ══════════════════════════════════════════════════════════════════════
# ARGUMENT PARSING
# ══════════════════════════════════════════════════════════════════════
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--config)
CONFIG_FILE="$2"; shift 2 ;;
--file)
PATH_FILE="$2"; shift 2 ;;
--max-age)
BACKUP_MAX_AGE="$2"; shift 2 ;;
--min-size)
BACKUP_MIN_SIZE="$2"; shift 2 ;;
--verbose)
VERBOSE="true"; shift ;;
--no-color)
COLOR="never"; shift ;;
--help|-h)
setup_colors
usage
exit 0 ;;
-*)
err "Unknown option: $1"
echo "Run ${SCRIPT_NAME} --help for usage" >&2
exit 1 ;;
*)
add_entry "$1"; shift ;;
esac
done
}
# ══════════════════════════════════════════════════════════════════════
# MAIN
# ══════════════════════════════════════════════════════════════════════
main() {
parse_args "$@"
setup_colors
# Load from config file
if [[ -n "$CONFIG_FILE" ]]; then
load_config_file "$CONFIG_FILE"
fi
# Load from path file
if [[ -n "$PATH_FILE" ]]; then
load_paths_from_file "$PATH_FILE"
fi
# Load from stdin if no entries yet and stdin is not a terminal
if [[ ${#ENTRIES[@]} -eq 0 ]] && ! [[ -t 0 ]]; then
load_paths_from_stdin
fi
if [[ ${#ENTRIES[@]} -eq 0 ]]; then
err "No backup paths specified"
echo "Run ${SCRIPT_NAME} --help for usage" >&2
exit 1
fi
echo ""
echo -e "${BOLD}Backup Verification — $(hostname -f 2>/dev/null || hostname)${RESET}"
echo -e "${DIM}$(date '+%Y-%m-%d %H:%M:%S %Z')${RESET}"
echo -e "${DIM}Defaults: max-age=${BACKUP_MAX_AGE}, min-size=${BACKUP_MIN_SIZE}${RESET}"
section_header "Backup Status"
printf " ${BOLD}%-50s %10s %10s %s${RESET}\n" "FILE" "SIZE" "AGE" "STATUS"
printf " %s\n" "$(printf '%.0s─' {1..85})"
for entry in "${ENTRIES[@]}"; do
local path max_age min_size
path=$(echo "$entry" | cut -d'|' -f1)
max_age=$(echo "$entry" | cut -d'|' -f2)
min_size=$(echo "$entry" | cut -d'|' -f3)
# Check if path contains glob characters
if [[ "$path" == *\** || "$path" == *\?* || "$path" == *\[* ]]; then
verify_glob "$path" "$max_age" "$min_size"
else
verify_file "$path" "$max_age" "$min_size"
fi
done
section_header "Summary"
field "Total checked:" "$COUNT_TOTAL"
field_color "OK:" "${GREEN}${COUNT_OK}${RESET}"
if [[ "$COUNT_WARNING" -gt 0 ]]; then
field_color "Warnings:" "${YELLOW}${COUNT_WARNING}${RESET}"
else
field "Warnings:" "$COUNT_WARNING"
fi
if [[ "$COUNT_CRITICAL" -gt 0 ]]; then
field_color "Critical:" "${RED}${COUNT_CRITICAL}${RESET}"
else
field "Critical:" "$COUNT_CRITICAL"
fi
echo ""
# Exit with error code if any critical issues
if [[ "$COUNT_CRITICAL" -gt 0 ]]; then
return 2
elif [[ "$COUNT_WARNING" -gt 0 ]]; then
return 1
fi
}
main "$@"