a1a17e81a1
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.
443 lines
15 KiB
Bash
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 "$@"
|