457 lines
15 KiB
Bash
Executable File
457 lines
15 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# Laravel Timebank Storage Restore Script
|
|
# Restores storage directory from compressed backup
|
|
# Usage: ./restore-storage.sh [backup_file] [options]
|
|
# Options: --confirm, --list-backups, --latest, --merge
|
|
|
|
set -e # Exit on any error
|
|
|
|
# Configuration
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
STORAGE_DIR="$PROJECT_ROOT/storage"
|
|
BACKUP_ROOT_DIR="$PROJECT_ROOT/backups"
|
|
LOG_FILE="$BACKUP_ROOT_DIR/restore.log"
|
|
|
|
# Create log directory
|
|
mkdir -p "$BACKUP_ROOT_DIR/logs"
|
|
|
|
# Colors for output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# Logging function
|
|
log() {
|
|
local level="$1"
|
|
local message="$2"
|
|
local timestamp="[$(date '+%Y-%m-%d %H:%M:%S')]"
|
|
|
|
case "$level" in
|
|
"INFO")
|
|
echo -e "${timestamp} ${BLUE}[INFO]${NC} $message" | tee -a "$LOG_FILE"
|
|
;;
|
|
"SUCCESS")
|
|
echo -e "${timestamp} ${GREEN}[SUCCESS]${NC} $message" | tee -a "$LOG_FILE"
|
|
;;
|
|
"WARNING")
|
|
echo -e "${timestamp} ${YELLOW}[WARNING]${NC} $message" | tee -a "$LOG_FILE"
|
|
;;
|
|
"ERROR")
|
|
echo -e "${timestamp} ${RED}[ERROR]${NC} $message" | tee -a "$LOG_FILE"
|
|
;;
|
|
*)
|
|
echo "$timestamp $message" | tee -a "$LOG_FILE"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Function to show usage
|
|
show_usage() {
|
|
echo "Usage: $0 [backup_file] [options]"
|
|
echo ""
|
|
echo "Arguments:"
|
|
echo " backup_file - Path to backup file (relative to backup directory or absolute)"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " --confirm - Skip confirmation prompt"
|
|
echo " --list-backups - List available backups"
|
|
echo " --latest - Restore from latest backup"
|
|
echo " --merge - Merge with existing storage (don't delete existing files)"
|
|
echo " --help - Show this help message"
|
|
echo ""
|
|
echo "Examples:"
|
|
echo " $0 --list-backups # List available backups"
|
|
echo " $0 --latest # Restore latest backup"
|
|
echo " $0 --latest --merge # Merge latest backup with existing"
|
|
echo " $0 storage/daily/daily_20240101_120000.tar.gz # Restore specific backup"
|
|
echo ""
|
|
exit 0
|
|
}
|
|
|
|
# Function to list available backups
|
|
list_backups() {
|
|
log "INFO" "Available storage backups:"
|
|
echo ""
|
|
|
|
if [ ! -d "$BACKUP_ROOT_DIR/storage" ]; then
|
|
log "WARNING" "No storage backup directory found"
|
|
return 0
|
|
fi
|
|
|
|
local backup_found=false
|
|
|
|
for backup_type in daily weekly monthly; do
|
|
local backup_dir="$BACKUP_ROOT_DIR/storage/$backup_type"
|
|
if [ -d "$backup_dir" ]; then
|
|
local backups=($(find "$backup_dir" -name "*.tar.gz" -type f | sort -r))
|
|
if [ ${#backups[@]} -gt 0 ]; then
|
|
echo -e "${BLUE}$backup_type backups:${NC}"
|
|
for backup in "${backups[@]}"; do
|
|
local size=$(du -h "$backup" | cut -f1)
|
|
local date_created=$(date -r "$backup" '+%Y-%m-%d %H:%M:%S')
|
|
echo " $(basename "$backup") ($size, $date_created)"
|
|
done
|
|
echo ""
|
|
backup_found=true
|
|
fi
|
|
fi
|
|
done
|
|
|
|
if [ "$backup_found" = false ]; then
|
|
log "WARNING" "No storage backups found"
|
|
fi
|
|
}
|
|
|
|
# Function to find latest backup
|
|
get_latest_backup() {
|
|
local latest_backup=""
|
|
local latest_time=0
|
|
|
|
if [ -d "$BACKUP_ROOT_DIR/storage" ]; then
|
|
while IFS= read -r -d '' backup; do
|
|
local backup_time=$(stat -c %Y "$backup")
|
|
if [ "$backup_time" -gt "$latest_time" ]; then
|
|
latest_time=$backup_time
|
|
latest_backup=$backup
|
|
fi
|
|
done < <(find "$BACKUP_ROOT_DIR/storage" -name "*.tar.gz" -type f -print0)
|
|
fi
|
|
|
|
echo "$latest_backup"
|
|
}
|
|
|
|
# Function to confirm restore
|
|
confirm_restore() {
|
|
local backup_file="$1"
|
|
local merge_mode="$2"
|
|
|
|
echo ""
|
|
if [ "$merge_mode" = true ]; then
|
|
echo -e "${YELLOW}WARNING: This will MERGE the backup with existing storage files!${NC}"
|
|
echo -e "Mode: ${GREEN}Merge${NC} (existing files will be preserved unless overwritten)"
|
|
else
|
|
echo -e "${RED}WARNING: This will REPLACE the entire storage directory!${NC}"
|
|
echo -e "Mode: ${RED}Full Replace${NC} (all existing files will be deleted)"
|
|
fi
|
|
|
|
echo -e "Storage directory: ${BLUE}$STORAGE_DIR${NC}"
|
|
echo -e "Backup file: ${BLUE}$(basename "$backup_file")${NC}"
|
|
echo -e "Backup size: $(du -h "$backup_file" | cut -f1)"
|
|
echo -e "Backup date: $(date -r "$backup_file" '+%Y-%m-%d %H:%M:%S')"
|
|
echo ""
|
|
|
|
read -p "Are you sure you want to continue? [y/N]: " -n 1 -r
|
|
echo ""
|
|
|
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
log "INFO" "Restore cancelled by user"
|
|
exit 0
|
|
fi
|
|
}
|
|
|
|
# Function to create storage backup before restore
|
|
create_pre_restore_backup() {
|
|
log "INFO" "Creating pre-restore backup of current storage"
|
|
|
|
if [ ! -d "$STORAGE_DIR" ]; then
|
|
log "WARNING" "Storage directory doesn't exist, skipping pre-restore backup"
|
|
return 0
|
|
fi
|
|
|
|
local pre_restore_dir="$BACKUP_ROOT_DIR/storage/pre-restore"
|
|
mkdir -p "$pre_restore_dir"
|
|
|
|
local timestamp=$(date '+%Y%m%d_%H%M%S')
|
|
local backup_file="$pre_restore_dir/storage_pre_restore_${timestamp}.tar.gz"
|
|
|
|
log "INFO" "Creating compressed archive of current storage..."
|
|
if tar -czf "$backup_file" -C "$PROJECT_ROOT" "storage" 2>/dev/null; then
|
|
if [ -f "$backup_file" ] && [ -s "$backup_file" ]; then
|
|
local backup_size=$(du -h "$backup_file" | cut -f1)
|
|
log "SUCCESS" "Pre-restore backup created: $(basename "$backup_file") ($backup_size)"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
log "WARNING" "Pre-restore backup failed"
|
|
return 1
|
|
}
|
|
|
|
# Function to restore storage
|
|
restore_storage() {
|
|
local backup_file="$1"
|
|
local merge_mode="$2"
|
|
|
|
log "INFO" "Starting storage restore from: $(basename "$backup_file")"
|
|
|
|
# Verify backup file exists and is readable
|
|
if [ ! -f "$backup_file" ] || [ ! -r "$backup_file" ]; then
|
|
log "ERROR" "Backup file not found or not readable: $backup_file"
|
|
return 1
|
|
fi
|
|
|
|
# Test if backup file is valid tar.gz
|
|
if ! tar -tzf "$backup_file" >/dev/null 2>&1; then
|
|
log "ERROR" "Backup file is corrupted or not a valid tar.gz file"
|
|
return 1
|
|
fi
|
|
|
|
# Create temporary extraction directory
|
|
local temp_dir="/tmp/timebank_restore_$$"
|
|
mkdir -p "$temp_dir"
|
|
|
|
# Extract backup to temporary directory
|
|
log "INFO" "Extracting backup archive..."
|
|
if tar -xzf "$backup_file" -C "$temp_dir" 2>/dev/null; then
|
|
log "SUCCESS" "Backup extracted successfully"
|
|
else
|
|
log "ERROR" "Failed to extract backup archive"
|
|
rm -rf "$temp_dir"
|
|
return 1
|
|
fi
|
|
|
|
# Find the extracted storage directory
|
|
local extracted_storage=""
|
|
|
|
# Check for different possible structures in the archive
|
|
log "INFO" "Checking extracted structure in: $temp_dir"
|
|
log "INFO" "Contents: $(ls -la "$temp_dir" | head -10)"
|
|
|
|
if [ -d "$temp_dir/storage" ]; then
|
|
log "INFO" "Found direct storage directory"
|
|
extracted_storage="$temp_dir/storage"
|
|
elif [ -d "$temp_dir/daily_"* ] || [ -d "$temp_dir/weekly_"* ] || [ -d "$temp_dir/monthly_"* ]; then
|
|
log "INFO" "Found timestamped directory structure"
|
|
# Handle snapshot-based backups - look for storage directory inside timestamped folder
|
|
local snapshot_dir=$(find "$temp_dir" -maxdepth 1 -type d \( -name "daily_*" -o -name "weekly_*" -o -name "monthly_*" \) | head -n 1)
|
|
log "INFO" "Snapshot directory found: $snapshot_dir"
|
|
if [ -n "$snapshot_dir" ] && [ -d "$snapshot_dir/storage" ]; then
|
|
log "INFO" "Found storage subdirectory in snapshot"
|
|
extracted_storage="$snapshot_dir/storage"
|
|
elif [ -n "$snapshot_dir" ]; then
|
|
log "INFO" "Using snapshot directory contents directly"
|
|
# If no storage subdirectory, use the snapshot dir directly (it may contain the files)
|
|
extracted_storage="$snapshot_dir"
|
|
fi
|
|
elif [ -d "$temp_dir/timebank_storage_backup_"* ]; then
|
|
log "INFO" "Found full backup structure"
|
|
# Handle full backup structure
|
|
local full_backup_dir=$(find "$temp_dir" -maxdepth 1 -type d -name "timebank_storage_backup_*" | head -n 1)
|
|
if [ -n "$full_backup_dir" ]; then
|
|
extracted_storage="$full_backup_dir"
|
|
fi
|
|
else
|
|
log "WARNING" "No recognized backup structure found"
|
|
fi
|
|
|
|
if [ -z "$extracted_storage" ] || [ ! -d "$extracted_storage" ]; then
|
|
log "ERROR" "Could not find storage directory in backup archive"
|
|
rm -rf "$temp_dir"
|
|
return 1
|
|
fi
|
|
|
|
log "INFO" "Found extracted storage at: $extracted_storage"
|
|
log "INFO" "Contents of extracted storage: $(ls -la "$extracted_storage" | head -5)"
|
|
|
|
# Restore storage based on mode
|
|
if [ "$merge_mode" = true ]; then
|
|
log "INFO" "Performing merge restore (existing files preserved)"
|
|
|
|
# Create storage directory if it doesn't exist
|
|
mkdir -p "$STORAGE_DIR"
|
|
|
|
# Copy files, preserving existing ones
|
|
if rsync -av --ignore-existing "$extracted_storage/" "$STORAGE_DIR/" 2>&1 | tee -a "$LOG_FILE"; then
|
|
log "SUCCESS" "Storage merge completed"
|
|
else
|
|
log "ERROR" "Storage merge failed"
|
|
rm -rf "$temp_dir"
|
|
return 1
|
|
fi
|
|
|
|
else
|
|
log "INFO" "Performing full restore (replacing existing storage)"
|
|
|
|
# Create storage directory (rsync will handle the rest)
|
|
mkdir -p "$STORAGE_DIR"
|
|
|
|
# Copy extracted storage contents to final location
|
|
if rsync -av --delete "$extracted_storage/" "$STORAGE_DIR/" 2>&1 | tee -a "$LOG_FILE"; then
|
|
log "SUCCESS" "Storage restore completed"
|
|
else
|
|
log "ERROR" "Storage restore failed"
|
|
rm -rf "$temp_dir"
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
# Set correct permissions
|
|
log "INFO" "Setting storage permissions..."
|
|
chmod -R 755 "$STORAGE_DIR"
|
|
find "$STORAGE_DIR" -type f -exec chmod 644 {} \;
|
|
|
|
# Create required Laravel storage directories if they don't exist
|
|
mkdir -p "$STORAGE_DIR"/{app/public,framework/{cache,sessions,testing,views},logs}
|
|
|
|
# Clean up temporary directory
|
|
rm -rf "$temp_dir"
|
|
|
|
log "SUCCESS" "Storage restore process completed"
|
|
return 0
|
|
}
|
|
|
|
# Parse command line arguments
|
|
BACKUP_FILE=""
|
|
CONFIRM=false
|
|
LIST_BACKUPS=false
|
|
LATEST=false
|
|
MERGE=false
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--confirm)
|
|
CONFIRM=true
|
|
shift
|
|
;;
|
|
--list-backups)
|
|
LIST_BACKUPS=true
|
|
shift
|
|
;;
|
|
--latest)
|
|
LATEST=true
|
|
shift
|
|
;;
|
|
--merge)
|
|
MERGE=true
|
|
shift
|
|
;;
|
|
--help)
|
|
show_usage
|
|
;;
|
|
-*)
|
|
log "ERROR" "Unknown option: $1"
|
|
show_usage
|
|
;;
|
|
*)
|
|
if [ -z "$BACKUP_FILE" ]; then
|
|
BACKUP_FILE="$1"
|
|
else
|
|
log "ERROR" "Multiple backup files specified"
|
|
show_usage
|
|
fi
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Handle list backups option
|
|
if [ "$LIST_BACKUPS" = true ]; then
|
|
list_backups
|
|
exit 0
|
|
fi
|
|
|
|
# Handle latest backup option
|
|
if [ "$LATEST" = true ]; then
|
|
BACKUP_FILE=$(get_latest_backup)
|
|
if [ -z "$BACKUP_FILE" ]; then
|
|
log "ERROR" "No storage backups found"
|
|
exit 1
|
|
fi
|
|
log "INFO" "Using latest backup: $(basename "$BACKUP_FILE")"
|
|
fi
|
|
|
|
# Validate backup file argument
|
|
if [ -z "$BACKUP_FILE" ]; then
|
|
log "ERROR" "No backup file specified"
|
|
show_usage
|
|
fi
|
|
|
|
# Resolve backup file path
|
|
if [[ "$BACKUP_FILE" != /* ]]; then
|
|
# Relative path - check if it exists relative to backup directory
|
|
if [ -f "$BACKUP_ROOT_DIR/$BACKUP_FILE" ]; then
|
|
BACKUP_FILE="$BACKUP_ROOT_DIR/$BACKUP_FILE"
|
|
elif [ -f "$BACKUP_ROOT_DIR/storage/daily/$BACKUP_FILE" ]; then
|
|
BACKUP_FILE="$BACKUP_ROOT_DIR/storage/daily/$BACKUP_FILE"
|
|
elif [ -f "$BACKUP_ROOT_DIR/storage/weekly/$BACKUP_FILE" ]; then
|
|
BACKUP_FILE="$BACKUP_ROOT_DIR/storage/weekly/$BACKUP_FILE"
|
|
elif [ -f "$BACKUP_ROOT_DIR/storage/monthly/$BACKUP_FILE" ]; then
|
|
BACKUP_FILE="$BACKUP_ROOT_DIR/storage/monthly/$BACKUP_FILE"
|
|
elif [ -f "$BACKUP_FILE" ]; then
|
|
# Exists relative to current directory
|
|
BACKUP_FILE="$(realpath "$BACKUP_FILE")"
|
|
else
|
|
log "ERROR" "Storage backup file not found: $BACKUP_FILE"
|
|
log "INFO" "Checked locations:"
|
|
log "INFO" " $BACKUP_ROOT_DIR/$BACKUP_FILE"
|
|
log "INFO" " $BACKUP_ROOT_DIR/storage/daily/$BACKUP_FILE"
|
|
log "INFO" " $BACKUP_ROOT_DIR/storage/weekly/$BACKUP_FILE"
|
|
log "INFO" " $BACKUP_ROOT_DIR/storage/monthly/$BACKUP_FILE"
|
|
log "INFO" " ./$BACKUP_FILE"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Main execution
|
|
main() {
|
|
log "INFO" "============================================"
|
|
log "INFO" "Starting storage restore process"
|
|
log "INFO" "Time: $(date)"
|
|
log "INFO" "============================================"
|
|
|
|
local start_time=$(date +%s)
|
|
|
|
# Confirm restore unless --confirm was specified
|
|
if [ "$CONFIRM" = false ]; then
|
|
confirm_restore "$BACKUP_FILE" "$MERGE"
|
|
fi
|
|
|
|
# Create pre-restore backup
|
|
if ! create_pre_restore_backup; then
|
|
log "WARNING" "Pre-restore backup failed, but continuing with restore"
|
|
fi
|
|
|
|
# Perform restore
|
|
if restore_storage "$BACKUP_FILE" "$MERGE"; then
|
|
local end_time=$(date +%s)
|
|
local execution_time=$((end_time - start_time))
|
|
local execution_time_formatted=$(date -d@$execution_time -u +%H:%M:%S)
|
|
|
|
log "SUCCESS" "Storage restore completed successfully in $execution_time_formatted"
|
|
log "INFO" "Restored from: $(basename "$BACKUP_FILE")"
|
|
|
|
# Send notification
|
|
if command -v mail >/dev/null 2>&1; then
|
|
echo "Storage restore completed successfully at $(date). Restored from: $(basename "$BACKUP_FILE")" | mail -s "Timebank Storage Restore Success" "${BACKUP_NOTIFY_EMAIL:-$USER@localhost}" 2>/dev/null || true
|
|
fi
|
|
|
|
# Recommend running Laravel commands
|
|
echo ""
|
|
log "INFO" "Recommended post-restore steps:"
|
|
log "INFO" " 1. php artisan storage:link"
|
|
log "INFO" " 2. php artisan config:clear"
|
|
log "INFO" " 3. php artisan cache:clear"
|
|
log "INFO" " 4. Check file permissions and ownership"
|
|
log "INFO" " 5. Test file uploads and media functionality"
|
|
|
|
# Check if storage link exists
|
|
if [ ! -L "$PROJECT_ROOT/public/storage" ]; then
|
|
log "WARNING" "Storage symlink missing. Run: php artisan storage:link"
|
|
fi
|
|
|
|
else
|
|
log "ERROR" "Storage restore failed"
|
|
exit 1
|
|
fi
|
|
|
|
log "INFO" "============================================"
|
|
log "INFO" "Restore process finished"
|
|
log "INFO" "============================================"
|
|
}
|
|
|
|
# Run main function
|
|
main |