Add enhanced backup and restoration scripts for Plex Media Server with validation and monitoring features

This commit is contained in:
Peter Wood
2025-05-25 19:53:23 -04:00
parent aa854588a5
commit a68a1cc4ba
4 changed files with 1012 additions and 2 deletions

355
backup-plex.sh Executable file
View 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 "$@"