#!/bin/bash # Laravel Timebank Storage Backup Script # Backs up storage directory with incremental rsync and compression # Usage: ./backup-storage.sh [backup_type] # backup_type: daily (default), weekly, monthly, full 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/backup.log" # Create backup directories mkdir -p "$BACKUP_ROOT_DIR"/{storage/{daily,weekly,monthly,snapshots},logs} # Logging function log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" } # Check if storage directory exists if [ ! -d "$STORAGE_DIR" ]; then log "ERROR: Storage directory not found: $STORAGE_DIR" exit 1 fi # Set backup type BACKUP_TYPE="${1:-daily}" TIMESTAMP=$(date '+%Y%m%d_%H%M%S') BACKUP_DIR="$BACKUP_ROOT_DIR/storage/$BACKUP_TYPE" SNAPSHOT_DIR="$BACKUP_ROOT_DIR/storage/snapshots" # Create directories mkdir -p "$BACKUP_DIR" "$SNAPSHOT_DIR" log "Starting $BACKUP_TYPE storage backup" # Rsync exclude patterns for Laravel storage RSYNC_EXCLUDES=" --exclude=framework/cache/* --exclude=framework/sessions/* --exclude=framework/testing/* --exclude=framework/views/* --exclude=logs/*.log --exclude=debugbar/* --exclude=app/backup/* --exclude=*/livewire-tmp/* --exclude=*.tmp --exclude=.DS_Store --exclude=Thumbs.db " # Function for incremental backup using rsync incremental_backup() { local target_dir="$1" local snapshot_name="$2" log "Performing incremental backup to $target_dir" # Create current snapshot directory local current_snapshot="$SNAPSHOT_DIR/$snapshot_name" mkdir -p "$current_snapshot" # Rsync with hard links to previous snapshot for space efficiency local link_dest_option="" local latest_snapshot=$(find "$SNAPSHOT_DIR" -maxdepth 1 -type d -name "${BACKUP_TYPE}_*" | sort -r | head -n 1) if [ -n "$latest_snapshot" ] && [ "$latest_snapshot" != "$current_snapshot" ]; then link_dest_option="--link-dest=$latest_snapshot" fi rsync -av \ --delete \ $RSYNC_EXCLUDES \ $link_dest_option \ "$STORAGE_DIR/" \ "$current_snapshot/" \ 2>&1 | tee -a "$LOG_FILE" # Create compressed archive for long-term storage local archive_name="${snapshot_name}.tar.gz" local archive_path="$target_dir/$archive_name" log "Creating compressed archive: $archive_name" tar -czf "$archive_path" -C "$SNAPSHOT_DIR" "$snapshot_name" 2>&1 | tee -a "$LOG_FILE" # Verify archive if [ -f "$archive_path" ] && [ -s "$archive_path" ]; then local archive_size=$(du -h "$archive_path" | cut -f1) log "Archive created successfully: $archive_path ($archive_size)" # Keep snapshots for recent backups only (last 3) if [ "$BACKUP_TYPE" = "daily" ]; then # Clean up old daily snapshots (keep last 3) find "$SNAPSHOT_DIR" -maxdepth 1 -type d -name "daily_*" | sort -r | tail -n +4 | xargs -r rm -rf fi return 0 else log "ERROR: Archive creation failed or archive is empty" return 1 fi } # Function for full backup (copy entire directory) full_backup() { local target_dir="$1" local backup_name="storage_full_${TIMESTAMP}" local backup_path="$target_dir/${backup_name}.tar.gz" log "Performing full storage backup" # Create temporary directory for clean backup local temp_dir="/tmp/timebank_storage_backup_$$" mkdir -p "$temp_dir" # Copy storage with rsync (excluding unwanted files) rsync -av $RSYNC_EXCLUDES "$STORAGE_DIR/" "$temp_dir/" 2>&1 | tee -a "$LOG_FILE" # Create compressed archive log "Creating compressed archive: $backup_name.tar.gz" tar -czf "$backup_path" -C "/tmp" "timebank_storage_backup_$$" 2>&1 | tee -a "$LOG_FILE" # Cleanup temporary directory rm -rf "$temp_dir" # Verify archive if [ -f "$backup_path" ] && [ -s "$backup_path" ]; then local archive_size=$(du -h "$backup_path" | cut -f1) log "Full backup completed: $backup_path ($archive_size)" return 0 else log "ERROR: Full backup failed" return 1 fi } # Perform backup based on type case "$BACKUP_TYPE" in daily) incremental_backup "$BACKUP_DIR" "daily_${TIMESTAMP}" ;; weekly) incremental_backup "$BACKUP_DIR" "weekly_${TIMESTAMP}" ;; monthly) incremental_backup "$BACKUP_DIR" "monthly_${TIMESTAMP}" ;; full) full_backup "$BACKUP_DIR" ;; *) log "ERROR: Invalid backup type: $BACKUP_TYPE (valid: daily, weekly, monthly, full)" exit 1 ;; esac # Cleanup function cleanup_old_backups() { local backup_dir="$1" local keep_count="$2" local pattern="$3" log "Cleaning up old $BACKUP_TYPE storage backups (keeping $keep_count most recent)" find "$backup_dir" -name "$pattern" -type f | sort -r | tail -n +$((keep_count + 1)) | while read -r old_backup; do if [ -f "$old_backup" ]; then rm "$old_backup" log "Removed old backup: $(basename "$old_backup")" fi done } # Apply retention policy case "$BACKUP_TYPE" in daily) cleanup_old_backups "$BACKUP_DIR" 7 "daily_*.tar.gz" ;; weekly) cleanup_old_backups "$BACKUP_DIR" 4 "weekly_*.tar.gz" ;; monthly) cleanup_old_backups "$BACKUP_DIR" 12 "monthly_*.tar.gz" ;; full) cleanup_old_backups "$BACKUP_DIR" 2 "storage_full_*.tar.gz" ;; esac # Generate storage usage report log "Storage backup summary:" if [ -d "$BACKUP_ROOT_DIR/storage" ]; then find "$BACKUP_ROOT_DIR/storage" -name "*.tar.gz" -exec basename {} \; | sort | while read -r backup; do backup_path="$BACKUP_ROOT_DIR/storage"/*/"$backup" if [ -f $backup_path ]; then size=$(du -h $backup_path | cut -f1) log " $backup ($size)" fi done total_size=$(du -sh "$BACKUP_ROOT_DIR/storage" | cut -f1) log "Total storage backups size: $total_size" fi log "$BACKUP_TYPE storage backup completed successfully" # Send notification if command -v mail >/dev/null 2>&1; then echo "Storage backup completed successfully at $(date)" | mail -s "Timebank Storage Backup Success" "${BACKUP_NOTIFY_EMAIL:-$USER@localhost}" 2>/dev/null || true fi