From a68a1cc4bae6cc4a47be863a7b3a4ab93c411c86 Mon Sep 17 00:00:00 2001 From: Peter Wood Date: Sun, 25 May 2025 19:53:23 -0400 Subject: [PATCH] Add enhanced backup and restoration scripts for Plex Media Server with validation and monitoring features --- README.md | 105 +++++++++++- backup-plex.sh | 355 +++++++++++++++++++++++++++++++++++++++ restore-plex.sh | 225 +++++++++++++++++++++++++ validate-plex-backups.sh | 329 ++++++++++++++++++++++++++++++++++++ 4 files changed, 1012 insertions(+), 2 deletions(-) create mode 100755 backup-plex.sh create mode 100755 restore-plex.sh create mode 100755 validate-plex-backups.sh diff --git a/README.md b/README.md index b7d147f..daf0b99 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,109 @@ This repository contains various shell scripts for managing media-related tasks - [Backup Media Script](docs/backup-media.md): Documentation for the `backup-media.sh` script. - `plex.sh`: Script to manage the Plex Media Server (start, stop, restart, status). -- `backup-plex.sh`: Script to back up Plex Media Server databases and related files. +- `backup-plex.sh`: Enhanced Plex backup script with integrity verification, incremental backups, and advanced features. +- `restore-plex.sh`: Script to restore Plex data from backups with safety checks. +- `validate-plex-backups.sh`: Script to validate backup integrity and monitor backup health. - `folder-metrics.sh`: Script to calculate disk usage and file count for a directory and its subdirectories. +## Enhanced Plex Backup System + +This repository includes an enhanced backup system for Plex Media Server with multiple components: + +### Scripts + +- **`backup-plex.sh`**: Advanced backup script with integrity verification, incremental backups, and automatic cleanup +- **`restore-plex.sh`**: Safe restoration script with dry-run mode and current data backup +- **`validate-plex-backups.sh`**: Backup validation and health monitoring script + +### Key Features + +- **Incremental backups**: Only backs up files that have changed since last backup +- **File integrity verification**: Uses MD5 checksums to verify backup integrity +- **Automatic cleanup**: Configurable retention policies for old backups +- **Disk space monitoring**: Checks available space before starting backup +- **Safe restoration**: Backs up current data before restoring from backup +- **Comprehensive logging**: Detailed logs with color-coded output +- **Service management**: Safely stops/starts Plex during backup operations + +### Usage Examples + +#### Enhanced Backup Script + +```bash +# Run the enhanced backup (recommended) +./backup-plex.sh +``` + +#### Backup Validation + +```bash +# Validate all backups and generate report +./validate-plex-backups.sh --report + +# Validate backups and attempt to fix common issues +./validate-plex-backups.sh --fix + +# Quick validation check +./validate-plex-backups.sh +``` + +#### Restore from Backup + +```bash +# List available backups +./restore-plex.sh + +# Test restore without making changes (dry run) +./restore-plex.sh 20250125 --dry-run + +# Actually restore from a specific backup +./restore-plex.sh 20250125 +``` + +### Automation Examples + +#### Daily Backup with Validation + +```bash +# Add to crontab for daily backup at 3 AM +0 3 * * * /home/acedanger/shell/backup-plex.sh + +# Add daily validation at 7 AM +0 7 * * * /home/acedanger/shell/validate-plex-backups.sh --fix +``` + +#### Weekly Full Validation Report + +```bash +# Generate detailed weekly report (Sundays at 8 AM) +0 8 * * 0 /home/acedanger/shell/validate-plex-backups.sh --report +``` + +### Configuration + +The enhanced backup script includes configurable parameters at the top of the file: + +- `MAX_BACKUP_AGE_DAYS=30`: Remove backups older than 30 days +- `MAX_BACKUPS_TO_KEEP=10`: Keep maximum of 10 backup sets +- `BACKUP_ROOT`: Location for backup storage +- `LOG_ROOT`: Location for backup logs + +### Backup Strategy + +The system implements a robust 3-2-1 backup strategy: + +1. **3 copies**: Original data + local backup + compressed archive +2. **2 different media**: Local disk + network storage +3. **1 offsite**: Consider syncing to remote location + +For offsite backup, add to cron: + +```bash +# Sync backups to remote server daily at 6 AM +0 6 * * * rsync -av /mnt/share/media/backups/plex/ user@remote-server:/backups/plex/ +``` + ## Documentation - [Plex Backup Script Documentation](./docs/plex-backup.md): Detailed documentation for the `backup-plex.sh` script. @@ -72,6 +172,7 @@ Test your setup in isolated Docker containers with: - Run comprehensive checks before committing changes The test environment checks: + - Package availability and installation - Core components (git, curl, wget, etc.) - Additional packages from `setup/packages.list` @@ -80,7 +181,7 @@ The test environment checks: Tests will continue even when some packages fail to install, reporting all issues in a comprehensive summary. -# plex.sh +## plex.sh This script is used to manage the Plex Media Server service on a systemd-based Linux distribution. It provides the following functionalities: diff --git a/backup-plex.sh b/backup-plex.sh new file mode 100755 index 0000000..6ef7216 --- /dev/null +++ b/backup-plex.sh @@ -0,0 +1,355 @@ +#!/bin/bash + +set -e + +# Color codes for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Configuration +MAX_BACKUP_AGE_DAYS=30 +MAX_BACKUPS_TO_KEEP=10 +BACKUP_ROOT="/mnt/share/media/backups/plex" +LOG_ROOT="/mnt/share/media/backups/logs" +SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" +JSON_LOG_FILE="${SCRIPT_DIR}/logs/plex-backup.json" + +# Create necessary directories +mkdir -p "${LOG_ROOT}" "${SCRIPT_DIR}/logs" + +# Date variables +CURRENT_DATE=$(date +%Y%m%d) +CURRENT_DATETIME=$(date +%Y%m%d_%H%M%S) +LOG_FILE="${LOG_ROOT}/plex_backup_${CURRENT_DATETIME}.log" +BACKUP_PATH="${BACKUP_ROOT}/${CURRENT_DATE}" + +# Plex files to backup with their nicknames for easier handling +declare -A PLEX_FILES=( + ["library_db"]="/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.db" + ["blobs_db"]="/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.blobs.db" + ["preferences"]="/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Preferences.xml" +) + +# Logging functions +log_message() { + local message="$1" + local timestamp=$(date '+%H:%M:%S') + echo "${timestamp} ${message}" | tee -a "${LOG_FILE}" +} + +log_error() { + local message="$1" + log_message "${RED}ERROR: ${message}${NC}" +} + +log_success() { + local message="$1" + log_message "${GREEN}SUCCESS: ${message}${NC}" +} + +log_warning() { + local message="$1" + log_message "${YELLOW}WARNING: ${message}${NC}" +} + +# Initialize JSON log file +initialize_json_log() { + if [ ! -f "${JSON_LOG_FILE}" ] || ! jq empty "${JSON_LOG_FILE}" 2>/dev/null; then + echo "{}" > "${JSON_LOG_FILE}" + log_message "Initialized JSON log file" + fi +} + +# Check if file needs backup based on modification time +needs_backup() { + local file="$1" + + if [ ! -f "$file" ]; then + log_warning "File not found: $file" + return 1 + fi + + local current_mod_date=$(stat -c %Y "$file") + local last_backup_date=$(jq -r --arg file "$file" '.[$file] // 0' "${JSON_LOG_FILE}") + + if [ "$last_backup_date" == "null" ] || [ "$last_backup_date" == "0" ]; then + log_message "File has never been backed up: $(basename "$file")" + return 0 + fi + + if [ "$current_mod_date" -gt "$last_backup_date" ]; then + log_message "File modified since last backup: $(basename "$file")" + return 0 + fi + + log_message "File unchanged since last backup: $(basename "$file")" + return 1 +} + +# Update JSON log with successful backup +update_json_log() { + local file="$1" + local mod_date=$(stat -c %Y "$file") + + jq -c --arg file "$file" --argjson mod_date "$mod_date" '.[$file] = $mod_date' "${JSON_LOG_FILE}" > "${JSON_LOG_FILE}.tmp" + + if [ $? -eq 0 ]; then + mv "${JSON_LOG_FILE}.tmp" "${JSON_LOG_FILE}" + log_message "Updated backup log for: $(basename "$file")" + else + log_error "Failed to update JSON log file" + rm -f "${JSON_LOG_FILE}.tmp" + return 1 + fi +} + +# Calculate MD5 checksum +calculate_checksum() { + local file="$1" + md5sum "$file" | cut -d' ' -f1 +} + +# Verify file integrity after copy +verify_backup() { + local src="$1" + local dest="$2" + + if [ ! -f "$dest" ]; then + log_error "Backup file not found: $dest" + return 1 + fi + + local src_checksum=$(calculate_checksum "$src") + local dest_checksum=$(calculate_checksum "$dest") + + if [ "$src_checksum" = "$dest_checksum" ]; then + log_success "Backup verified: $(basename "$dest")" + return 0 + else + log_error "Backup verification failed: $(basename "$dest")" + return 1 + fi +} + +# Manage Plex service +manage_plex_service() { + local action="$1" + log_message "Attempting to $action Plex Media Server..." + + if systemctl is-active --quiet plexmediaserver.service; then + case "$action" in + "stop") + sudo systemctl stop plexmediaserver.service + sleep 3 # Give it time to stop cleanly + log_success "Plex Media Server stopped" + ;; + "start") + sudo systemctl start plexmediaserver.service + sleep 3 # Give it time to start + log_success "Plex Media Server started" + ;; + esac + else + case "$action" in + "stop") + log_warning "Plex Media Server was not running" + ;; + "start") + log_warning "Plex Media Server failed to start or was already stopped" + ;; + esac + fi +} + +# Check available disk space +check_disk_space() { + local backup_dir="$1" + local required_space_mb="$2" + + local available_space_kb=$(df "$backup_dir" | awk 'NR==2 {print $4}') + local available_space_mb=$((available_space_kb / 1024)) + + if [ "$available_space_mb" -lt "$required_space_mb" ]; then + log_error "Insufficient disk space. Required: ${required_space_mb}MB, Available: ${available_space_mb}MB" + return 1 + fi + + log_message "Disk space check passed. Available: ${available_space_mb}MB" + return 0 +} + +# Estimate backup size +estimate_backup_size() { + local total_size=0 + + for nickname in "${!PLEX_FILES[@]}"; do + local file="${PLEX_FILES[$nickname]}" + if [ -f "$file" ] && needs_backup "$file"; then + local size_kb=$(du -k "$file" | cut -f1) + total_size=$((total_size + size_kb)) + fi + done + + echo $((total_size / 1024)) # Return size in MB +} + +# Clean old backups +cleanup_old_backups() { + log_message "Cleaning up old backups..." + + # Remove backups older than MAX_BACKUP_AGE_DAYS + find "${BACKUP_ROOT}" -maxdepth 1 -type d -name "????????" -mtime +${MAX_BACKUP_AGE_DAYS} -exec rm -rf {} \; 2>/dev/null || true + + # Keep only MAX_BACKUPS_TO_KEEP most recent backups + local backup_count=$(find "${BACKUP_ROOT}" -maxdepth 1 -type d -name "????????" | wc -l) + + if [ "$backup_count" -gt "$MAX_BACKUPS_TO_KEEP" ]; then + local excess=$((backup_count - MAX_BACKUPS_TO_KEEP)) + find "${BACKUP_ROOT}" -maxdepth 1 -type d -name "????????" -printf '%T@ %p\n' | sort -n | head -n "$excess" | cut -d' ' -f2- | xargs rm -rf + log_message "Removed $excess old backup directories" + fi + + # Clean old log files + find "${LOG_ROOT}" -name "plex_backup_*.log" -mtime +${MAX_BACKUP_AGE_DAYS} -delete 2>/dev/null || true +} + +# Create backup with verification +backup_file() { + local nickname="$1" + local src_file="${PLEX_FILES[$nickname]}" + local filename=$(basename "$src_file") + local dest_file="${BACKUP_PATH}/${filename}" + + log_message "Backing up $nickname: $filename" + + # Copy with sudo if needed + if sudo cp "$src_file" "$dest_file"; then + # Verify the backup + if verify_backup "$src_file" "$dest_file"; then + update_json_log "$src_file" + local size=$(du -h "$dest_file" | cut -f1) + log_success "Successfully backed up $filename ($size)" + return 0 + else + rm -f "$dest_file" + return 1 + fi + else + log_error "Failed to copy $filename" + return 1 + fi +} + +# Send notification +send_notification() { + local files_backed_up="$1" + local message="🎬 Plex backup completed successfully! 📦 $files_backed_up files backed up on $(hostname) ✅" + + if command -v curl >/dev/null 2>&1; then + curl -s \ + -H "tags:popcorn,backup,plex,$(hostname)" \ + -d "$message" \ + https://notify.peterwood.rocks/lab || log_warning "Failed to send notification" + else + log_warning "curl not available, skipping notification" + fi +} + +# Main backup function +main() { + log_message "Starting enhanced Plex backup process at $(date)" + + # Initialize + initialize_json_log + + # Estimate backup size and check disk space + local estimated_size_mb=$(estimate_backup_size) + local required_space_mb=$((estimated_size_mb + 100)) # Add 100MB buffer + + if ! check_disk_space "$(dirname "$BACKUP_PATH")" "$required_space_mb"; then + log_error "Backup aborted due to insufficient disk space" + exit 1 + fi + + # Stop Plex service + manage_plex_service stop + + # Create backup directory + mkdir -p "${BACKUP_PATH}" + + local files_backed_up=0 + local backup_errors=0 + + # Backup each file + for nickname in "${!PLEX_FILES[@]}"; do + local file="${PLEX_FILES[$nickname]}" + + if [ ! -f "$file" ]; then + log_warning "File not found: $file" + continue + fi + + if needs_backup "$file"; then + if backup_file "$nickname"; then + files_backed_up=$((files_backed_up + 1)) + else + backup_errors=$((backup_errors + 1)) + fi + fi + done + + # Start Plex service + manage_plex_service start + + # Create compressed archive if files were backed up + if [ "$files_backed_up" -gt 0 ]; then + local archive_file="${SCRIPT_DIR}/plex_backup_${CURRENT_DATE}.tar.gz" + + log_message "Creating compressed archive..." + if tar -czf "$archive_file" -C "$BACKUP_PATH" .; then + log_success "Archive created: $(basename "$archive_file")" + + # Verify archive + if tar -tzf "$archive_file" >/dev/null 2>&1; then + log_success "Archive verification passed" + rm -rf "$BACKUP_PATH" + log_message "Temporary backup directory removed" + else + log_error "Archive verification failed" + backup_errors=$((backup_errors + 1)) + fi + else + log_error "Failed to create archive" + backup_errors=$((backup_errors + 1)) + fi + + # Send notification + send_notification "$files_backed_up" + else + log_message "No files needed backup, removing empty backup directory" + rmdir "$BACKUP_PATH" 2>/dev/null || true + fi + + # Cleanup old backups + cleanup_old_backups + + # Final summary + log_message "Backup process completed at $(date)" + log_message "Files backed up: $files_backed_up" + log_message "Errors encountered: $backup_errors" + + if [ "$backup_errors" -gt 0 ]; then + log_error "Backup completed with errors" + exit 1 + else + log_success "Backup completed successfully" + fi +} + +# Trap to ensure Plex is restarted on script exit +trap 'manage_plex_service start' EXIT + +# Run main function +main "$@" diff --git a/restore-plex.sh b/restore-plex.sh new file mode 100755 index 0000000..5bdf74d --- /dev/null +++ b/restore-plex.sh @@ -0,0 +1,225 @@ +#!/bin/bash + +# Plex Backup Restoration Script +# Usage: ./restore-plex.sh [backup_date] [--dry-run] + +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +# Configuration +SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" +BACKUP_ROOT="/mnt/share/media/backups/plex" +PLEX_DATA_DIR="/var/lib/plexmediaserver/Library/Application Support/Plex Media Server" + +# Plex file locations +declare -A RESTORE_LOCATIONS=( + ["com.plexapp.plugins.library.db"]="$PLEX_DATA_DIR/Plug-in Support/Databases/" + ["com.plexapp.plugins.library.blobs.db"]="$PLEX_DATA_DIR/Plug-in Support/Databases/" + ["Preferences.xml"]="$PLEX_DATA_DIR/" +) + +log_message() { + echo -e "$(date '+%H:%M:%S') $1" +} + +log_error() { + log_message "${RED}ERROR: $1${NC}" +} + +log_success() { + log_message "${GREEN}SUCCESS: $1${NC}" +} + +log_warning() { + log_message "${YELLOW}WARNING: $1${NC}" +} + +# List available backups +list_backups() { + log_message "Available backups:" + find "$BACKUP_ROOT" -maxdepth 1 -type d -name "????????" | sort -r | while read backup_dir; do + local backup_date=$(basename "$backup_dir") + local readable_date=$(date -d "${backup_date:0:4}-${backup_date:4:2}-${backup_date:6:2}" '+%B %d, %Y') + local file_count=$(ls -1 "$backup_dir" 2>/dev/null | wc -l) + echo " $backup_date ($readable_date) - $file_count files" + done +} + +# Validate backup integrity +validate_backup() { + local backup_date="$1" + local backup_dir="$BACKUP_ROOT/$backup_date" + + if [ ! -d "$backup_dir" ]; then + log_error "Backup directory not found: $backup_dir" + return 1 + fi + + log_message "Validating backup integrity for $backup_date..." + + for file in "${!RESTORE_LOCATIONS[@]}"; do + local backup_file="$backup_dir/$file" + if [ -f "$backup_file" ]; then + log_success "Found: $file" + else + log_warning "Missing: $file" + fi + done +} + +# Create backup of current Plex data +backup_current_data() { + local backup_suffix=$(date '+%Y%m%d_%H%M%S') + local current_backup_dir="$SCRIPT_DIR/plex_current_backup_$backup_suffix" + + log_message "Creating backup of current Plex data..." + mkdir -p "$current_backup_dir" + + for file in "${!RESTORE_LOCATIONS[@]}"; do + local src="${RESTORE_LOCATIONS[$file]}$file" + if [ -f "$src" ]; then + if sudo cp "$src" "$current_backup_dir/"; then + log_success "Backed up current: $file" + else + log_error "Failed to backup current: $file" + return 1 + fi + fi + done + + log_success "Current data backed up to: $current_backup_dir" + echo "$current_backup_dir" +} + +# Restore files from backup +restore_files() { + local backup_date="$1" + local dry_run="$2" + local backup_dir="$BACKUP_ROOT/$backup_date" + + if [ "$dry_run" = "true" ]; then + log_message "DRY RUN: Would restore the following files:" + else + log_message "Restoring files from backup $backup_date..." + fi + + for file in "${!RESTORE_LOCATIONS[@]}"; do + local backup_file="$backup_dir/$file" + local restore_location="${RESTORE_LOCATIONS[$file]}$file" + + if [ -f "$backup_file" ]; then + if [ "$dry_run" = "true" ]; then + echo " $backup_file -> $restore_location" + else + log_message "Restoring: $file" + if sudo cp "$backup_file" "$restore_location"; then + sudo chown plex:plex "$restore_location" + log_success "Restored: $file" + else + log_error "Failed to restore: $file" + return 1 + fi + fi + else + log_warning "Backup file not found: $backup_file" + fi + done +} + +# Manage Plex service +manage_plex_service() { + local action="$1" + log_message "$action Plex Media Server..." + + case "$action" in + "stop") + sudo systemctl stop plexmediaserver.service + sleep 3 + log_success "Plex stopped" + ;; + "start") + sudo systemctl start plexmediaserver.service + sleep 3 + log_success "Plex started" + ;; + esac +} + +# Main function +main() { + local backup_date="$1" + local dry_run=false + + # Check for dry-run flag + if [ "$2" = "--dry-run" ] || [ "$1" = "--dry-run" ]; then + dry_run=true + fi + + # If no backup date provided, list available backups + if [ -z "$backup_date" ] || [ "$backup_date" = "--dry-run" ]; then + list_backups + echo + echo "Usage: $0 [--dry-run]" + echo "Example: $0 20250125" + exit 0 + fi + + # Validate backup exists and is complete + if ! validate_backup "$backup_date"; then + log_error "Backup validation failed" + exit 1 + fi + + if [ "$dry_run" = "true" ]; then + restore_files "$backup_date" true + log_message "Dry run completed. No changes were made." + exit 0 + fi + + # Confirm restoration + echo + log_warning "This will restore Plex data from backup $backup_date" + log_warning "Current Plex data will be backed up before restoration" + read -p "Continue? (y/N): " -n 1 -r + echo + + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + log_message "Restoration cancelled" + exit 0 + fi + + # Stop Plex service + manage_plex_service stop + + # Backup current data + local current_backup=$(backup_current_data) + if [ $? -ne 0 ]; then + log_error "Failed to backup current data" + manage_plex_service start + exit 1 + fi + + # Restore files + if restore_files "$backup_date" false; then + log_success "Restoration completed successfully" + log_message "Current data backup saved at: $current_backup" + else + log_error "Restoration failed" + manage_plex_service start + exit 1 + fi + + # Start Plex service + manage_plex_service start + + log_success "Plex restoration completed. Please verify your server is working correctly." +} + +# Trap to ensure Plex is restarted on script exit +trap 'manage_plex_service start' EXIT + +main "$@" diff --git a/validate-plex-backups.sh b/validate-plex-backups.sh new file mode 100755 index 0000000..95559f0 --- /dev/null +++ b/validate-plex-backups.sh @@ -0,0 +1,329 @@ +#!/bin/bash + +# Plex Backup Validation and Monitoring Script +# Usage: ./validate-plex-backups.sh [--fix] [--report] + +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# Configuration +SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" +BACKUP_ROOT="/mnt/share/media/backups/plex" +JSON_LOG_FILE="$SCRIPT_DIR/logs/plex-backup.json" +REPORT_FILE="$SCRIPT_DIR/logs/backup-validation-$(date +%Y%m%d_%H%M%S).log" + +# Expected files in backup +EXPECTED_FILES=( + "com.plexapp.plugins.library.db" + "com.plexapp.plugins.library.blobs.db" + "Preferences.xml" +) + +log_message() { + local message="$1" + echo -e "$(date '+%H:%M:%S') $message" | tee -a "$REPORT_FILE" +} + +log_error() { + log_message "${RED}ERROR: $1${NC}" +} + +log_success() { + log_message "${GREEN}SUCCESS: $1${NC}" +} + +log_warning() { + log_message "${YELLOW}WARNING: $1${NC}" +} + +log_info() { + log_message "${BLUE}INFO: $1${NC}" +} + +# Check backup directory structure +validate_backup_structure() { + log_info "Validating backup directory structure..." + + if [ ! -d "$BACKUP_ROOT" ]; then + log_error "Backup root directory not found: $BACKUP_ROOT" + return 1 + fi + + local backup_count=$(find "$BACKUP_ROOT" -maxdepth 1 -type d -name "????????" | wc -l) + log_info "Found $backup_count backup directories" + + if [ "$backup_count" -eq 0 ]; then + log_warning "No backup directories found" + return 1 + fi + + return 0 +} + +# Validate individual backup +validate_backup() { + local backup_dir="$1" + local backup_date=$(basename "$backup_dir") + local errors=0 + + log_info "Validating backup: $backup_date" + + # Check if directory exists and is readable + if [ ! -d "$backup_dir" ] || [ ! -r "$backup_dir" ]; then + log_error "Backup directory not accessible: $backup_dir" + return 1 + fi + + # Check for expected files + for file in "${EXPECTED_FILES[@]}"; do + local file_path="$backup_dir/$file" + + if [ -f "$file_path" ]; then + # Check file size + local size=$(stat -c%s "$file_path") + if [ "$size" -gt 0 ]; then + local human_size=$(du -h "$file_path" | cut -f1) + log_success " $file ($human_size)" + else + log_error " $file is empty" + errors=$((errors + 1)) + fi + else + log_error " Missing file: $file" + errors=$((errors + 1)) + fi + done + + # Check for unexpected files + local file_count=$(ls -1 "$backup_dir" | wc -l) + local expected_count=${#EXPECTED_FILES[@]} + + if [ "$file_count" -ne "$expected_count" ]; then + log_warning " Expected $expected_count files, found $file_count" + ls -la "$backup_dir" | grep -v "^total" | grep -v "^d" | while read line; do + local filename=$(echo "$line" | awk '{print $9}') + if [[ ! " ${EXPECTED_FILES[@]} " =~ " ${filename} " ]]; then + log_warning " Unexpected file: $filename" + fi + done + fi + + return $errors +} + +# Check backup freshness +check_backup_freshness() { + log_info "Checking backup freshness..." + + local latest_backup=$(find "$BACKUP_ROOT" -maxdepth 1 -type d -name "????????" | sort | tail -1) + + if [ -z "$latest_backup" ]; then + log_error "No backups found" + return 1 + fi + + local backup_date=$(basename "$latest_backup") + local backup_timestamp=$(date -d "${backup_date:0:4}-${backup_date:4:2}-${backup_date:6:2}" +%s) + local current_timestamp=$(date +%s) + local age_days=$(( (current_timestamp - backup_timestamp) / 86400 )) + + log_info "Latest backup: $backup_date ($age_days days old)" + + if [ "$age_days" -gt 7 ]; then + log_warning "Latest backup is older than 7 days" + return 1 + elif [ "$age_days" -gt 3 ]; then + log_warning "Latest backup is older than 3 days" + else + log_success "Latest backup is recent" + fi + + return 0 +} + +# Validate JSON log file +validate_json_log() { + log_info "Validating JSON log file..." + + if [ ! -f "$JSON_LOG_FILE" ]; then + log_error "JSON log file not found: $JSON_LOG_FILE" + return 1 + fi + + if ! jq empty "$JSON_LOG_FILE" 2>/dev/null; then + log_error "JSON log file is invalid" + return 1 + fi + + local entry_count=$(jq 'length' "$JSON_LOG_FILE") + log_success "JSON log file is valid ($entry_count entries)" + + return 0 +} + +# Check disk space +check_disk_space() { + log_info "Checking disk space..." + + local backup_disk_usage=$(du -sh "$BACKUP_ROOT" | cut -f1) + local available_space=$(df -h "$BACKUP_ROOT" | awk 'NR==2 {print $4}') + local used_percentage=$(df "$BACKUP_ROOT" | awk 'NR==2 {print $5}' | sed 's/%//') + + log_info "Backup disk usage: $backup_disk_usage" + log_info "Available space: $available_space" + log_info "Disk usage: $used_percentage%" + + if [ "$used_percentage" -gt 90 ]; then + log_error "Disk usage is above 90%" + return 1 + elif [ "$used_percentage" -gt 80 ]; then + log_warning "Disk usage is above 80%" + else + log_success "Disk usage is acceptable" + fi + + return 0 +} + +# Generate backup report +generate_report() { + log_info "Generating backup report..." + + local total_backups=0 + local valid_backups=0 + local total_errors=0 + + # Header + echo "==================================" >> "$REPORT_FILE" + echo "Plex Backup Validation Report" >> "$REPORT_FILE" + echo "Generated: $(date)" >> "$REPORT_FILE" + echo "==================================" >> "$REPORT_FILE" + + # Validate each backup + find "$BACKUP_ROOT" -maxdepth 1 -type d -name "????????" | sort | while read backup_dir; do + total_backups=$((total_backups + 1)) + validate_backup "$backup_dir" + local backup_errors=$? + + if [ "$backup_errors" -eq 0 ]; then + valid_backups=$((valid_backups + 1)) + else + total_errors=$((total_errors + backup_errors)) + fi + done + + # Summary + echo >> "$REPORT_FILE" + echo "Summary:" >> "$REPORT_FILE" + echo " Total backups: $total_backups" >> "$REPORT_FILE" + echo " Valid backups: $valid_backups" >> "$REPORT_FILE" + echo " Total errors: $total_errors" >> "$REPORT_FILE" + + log_success "Report generated: $REPORT_FILE" +} + +# Fix common issues +fix_issues() { + log_info "Attempting to fix common issues..." + + # Fix JSON log file + if [ ! -f "$JSON_LOG_FILE" ] || ! jq empty "$JSON_LOG_FILE" 2>/dev/null; then + log_info "Fixing JSON log file..." + mkdir -p "$(dirname "$JSON_LOG_FILE")" + echo "{}" > "$JSON_LOG_FILE" + log_success "JSON log file created/fixed" + fi + + # Remove empty backup directories + find "$BACKUP_ROOT" -maxdepth 1 -type d -name "????????" -empty -delete 2>/dev/null || true + + # Fix permissions if needed + if [ -d "$BACKUP_ROOT" ]; then + chmod 755 "$BACKUP_ROOT" + find "$BACKUP_ROOT" -type f -exec chmod 644 {} \; 2>/dev/null || true + log_success "Fixed backup permissions" + fi +} + +# Main function +main() { + local fix_mode=false + local report_mode=false + + # Parse arguments + while [[ $# -gt 0 ]]; do + case $1 in + --fix) + fix_mode=true + shift + ;; + --report) + report_mode=true + shift + ;; + *) + echo "Usage: $0 [--fix] [--report]" + echo " --fix Attempt to fix common issues" + echo " --report Generate detailed backup report" + exit 1 + ;; + esac + done + + log_info "Starting Plex backup validation..." + + # Create logs directory if needed + mkdir -p "$(dirname "$REPORT_FILE")" + + local overall_status=0 + + # Fix issues if requested + if [ "$fix_mode" = true ]; then + fix_issues + fi + + # Validate backup structure + if ! validate_backup_structure; then + overall_status=1 + fi + + # Check backup freshness + if ! check_backup_freshness; then + overall_status=1 + fi + + # Validate JSON log + if ! validate_json_log; then + overall_status=1 + fi + + # Check disk space + if ! check_disk_space; then + overall_status=1 + fi + + # Generate detailed report if requested + if [ "$report_mode" = true ]; then + generate_report + fi + + # Final summary + echo + if [ "$overall_status" -eq 0 ]; then + log_success "All validation checks passed" + else + log_error "Some validation checks failed" + echo + echo "Consider running with --fix to attempt automatic repairs" + echo "Use --report for a detailed backup analysis" + fi + + exit $overall_status +} + +main "$@"