#!/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