mirror of
https://github.com/acedanger/shell.git
synced 2025-12-06 05:40:11 -08:00
Add enhanced backup and restoration scripts for Plex Media Server with validation and monitoring features
This commit is contained in:
105
README.md
105
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:
|
||||
|
||||
|
||||
355
backup-plex.sh
Executable file
355
backup-plex.sh
Executable file
@@ -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 "$@"
|
||||
225
restore-plex.sh
Executable file
225
restore-plex.sh
Executable file
@@ -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 <backup_date> [--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 "$@"
|
||||
329
validate-plex-backups.sh
Executable file
329
validate-plex-backups.sh
Executable file
@@ -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 "$@"
|
||||
Reference in New Issue
Block a user