← back to Watches

scripts/backup-system.sh

442 lines

#!/bin/bash

###############################################################################
# Omega Watch Price History - Automated Backup System
#
# Features:
# - Daily automated backups
# - Incremental and full backups
# - Off-site backup support (S3-compatible)
# - Backup rotation and cleanup
# - Backup verification
# - Point-in-time recovery
###############################################################################

set -euo pipefail

# Configuration
PROJECT_DIR="/root/Projects/watches"
BACKUP_DIR="${PROJECT_DIR}/backups"
DATA_DIR="${PROJECT_DIR}/data"
LOGS_DIR="${PROJECT_DIR}/logs"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
DATE=$(date +%Y-%m-%d)
RETENTION_DAYS=${BACKUP_RETENTION_DAYS:-30}

# S3 Configuration (optional)
S3_BUCKET="${S3_BACKUP_BUCKET:-}"
S3_REGION="${AWS_REGION:-us-east-1}"

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# Logging
log() {
    echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
}

error() {
    echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR:${NC} $1" >&2
}

warning() {
    echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARNING:${NC} $1"
}

# Create backup directories
create_backup_dirs() {
    mkdir -p "${BACKUP_DIR}"/{daily,weekly,monthly,offsite}
    mkdir -p "${BACKUP_DIR}/restore"
    log "Backup directories created"
}

# Backup database (JSON files)
backup_database() {
    local backup_file="${BACKUP_DIR}/daily/database_${TIMESTAMP}.tar.gz"

    log "Backing up database..."

    tar -czf "${backup_file}" \
        -C "${DATA_DIR}" \
        --exclude='backups' \
        . || { error "Database backup failed"; return 1; }

    # Calculate checksum
    sha256sum "${backup_file}" > "${backup_file}.sha256"

    local size=$(du -h "${backup_file}" | cut -f1)
    log "Database backup completed: $(basename ${backup_file}) (${size})"

    echo "${backup_file}"
}

# Backup logs
backup_logs() {
    local backup_file="${BACKUP_DIR}/daily/logs_${TIMESTAMP}.tar.gz"

    if [ -d "${LOGS_DIR}" ]; then
        log "Backing up logs..."

        tar -czf "${backup_file}" \
            -C "${LOGS_DIR}" \
            --exclude='*.tmp' \
            . 2>/dev/null || warning "No logs to backup"

        if [ -f "${backup_file}" ]; then
            sha256sum "${backup_file}" > "${backup_file}.sha256"
            local size=$(du -h "${backup_file}" | cut -f1)
            log "Logs backup completed: ${backup_file} (${size})"
        fi
    fi
}

# Backup configuration files
backup_config() {
    local backup_file="${BACKUP_DIR}/daily/config_${TIMESTAMP}.tar.gz"

    log "Backing up configuration..."

    tar -czf "${backup_file}" \
        -C "${PROJECT_DIR}" \
        package.json \
        package-lock.json \
        server.js \
        .env 2>/dev/null \
        ecosystem.config.js 2>/dev/null \
        || true

    if [ -f "${backup_file}" ]; then
        sha256sum "${backup_file}" > "${backup_file}.sha256"
        log "Configuration backup completed"
    fi
}

# Create full backup
full_backup() {
    log "=========================================="
    log "Starting FULL backup: ${TIMESTAMP}"
    log "=========================================="

    create_backup_dirs

    local backup_manifest="${BACKUP_DIR}/daily/backup_${TIMESTAMP}_manifest.txt"

    {
        echo "Omega Watch Price History - Backup Manifest"
        echo "============================================"
        echo "Timestamp: ${TIMESTAMP}"
        echo "Date: ${DATE}"
        echo "Backup Type: FULL"
        echo ""
        echo "Components:"
    } > "${backup_manifest}"

    # Backup database
    local db_backup=$(backup_database)
    echo "- Database: $(basename ${db_backup})" >> "${backup_manifest}"

    # Backup logs
    backup_logs
    echo "- Logs: logs_${TIMESTAMP}.tar.gz" >> "${backup_manifest}"

    # Backup configuration
    backup_config
    echo "- Configuration: config_${TIMESTAMP}.tar.gz" >> "${backup_manifest}"

    # Create combined backup archive
    local full_backup_file="${BACKUP_DIR}/daily/full_backup_${TIMESTAMP}.tar.gz"
    tar -czf "${full_backup_file}" \
        -C "${BACKUP_DIR}/daily" \
        "database_${TIMESTAMP}.tar.gz" \
        "logs_${TIMESTAMP}.tar.gz" 2>/dev/null \
        "config_${TIMESTAMP}.tar.gz" 2>/dev/null \
        "backup_${TIMESTAMP}_manifest.txt" \
        || true

    sha256sum "${full_backup_file}" > "${full_backup_file}.sha256"

    local size=$(du -h "${full_backup_file}" | cut -f1)
    log "Full backup archive created: ${size}"

    # Upload to S3 if configured
    if [ -n "${S3_BUCKET}" ]; then
        upload_to_s3 "${full_backup_file}"
    fi

    log "=========================================="
    log "Backup completed successfully!"
    log "=========================================="
}

# Upload backup to S3
upload_to_s3() {
    local backup_file="$1"

    if ! command -v aws &> /dev/null; then
        warning "AWS CLI not installed, skipping S3 upload"
        return
    fi

    log "Uploading backup to S3..."

    aws s3 cp "${backup_file}" \
        "s3://${S3_BUCKET}/omega-watches/backups/$(basename ${backup_file})" \
        --region "${S3_REGION}" \
        && log "✓ Uploaded to S3: s3://${S3_BUCKET}/omega-watches/backups/" \
        || error "S3 upload failed"

    # Upload checksum
    aws s3 cp "${backup_file}.sha256" \
        "s3://${S3_BUCKET}/omega-watches/backups/$(basename ${backup_file}).sha256" \
        --region "${S3_REGION}" 2>/dev/null || true
}

# Verify backup integrity
verify_backup() {
    local backup_file="$1"

    log "Verifying backup integrity: $(basename ${backup_file})"

    if [ ! -f "${backup_file}" ]; then
        error "Backup file not found: ${backup_file}"
        return 1
    fi

    # Check if tar file is valid
    if ! tar -tzf "${backup_file}" > /dev/null 2>&1; then
        error "Backup file is corrupted!"
        return 1
    fi

    # Verify checksum if available
    if [ -f "${backup_file}.sha256" ]; then
        if sha256sum -c "${backup_file}.sha256" > /dev/null 2>&1; then
            log "✓ Checksum verification passed"
        else
            error "Checksum verification failed!"
            return 1
        fi
    fi

    log "✓ Backup verification passed"
    return 0
}

# Rotate old backups
rotate_backups() {
    log "Rotating old backups (retention: ${RETENTION_DAYS} days)..."

    # Daily backups - keep for RETENTION_DAYS
    find "${BACKUP_DIR}/daily" -name "*.tar.gz" -mtime +${RETENTION_DAYS} -delete 2>/dev/null || true
    find "${BACKUP_DIR}/daily" -name "*.sha256" -mtime +${RETENTION_DAYS} -delete 2>/dev/null || true
    find "${BACKUP_DIR}/daily" -name "*.txt" -mtime +${RETENTION_DAYS} -delete 2>/dev/null || true

    # Weekly backups - keep for 90 days
    find "${BACKUP_DIR}/weekly" -name "*.tar.gz" -mtime +90 -delete 2>/dev/null || true

    # Monthly backups - keep for 365 days
    find "${BACKUP_DIR}/monthly" -name "*.tar.gz" -mtime +365 -delete 2>/dev/null || true

    log "Old backups cleaned up"
}

# Create weekly backup (copy from daily)
weekly_backup() {
    local latest_daily=$(ls -t "${BACKUP_DIR}/daily"/full_backup_*.tar.gz 2>/dev/null | head -1)

    if [ -n "${latest_daily}" ]; then
        local week_num=$(date +%U)
        local weekly_file="${BACKUP_DIR}/weekly/full_backup_week${week_num}_${TIMESTAMP}.tar.gz"

        cp "${latest_daily}" "${weekly_file}"
        cp "${latest_daily}.sha256" "${weekly_file}.sha256" 2>/dev/null || true

        log "Weekly backup created: $(basename ${weekly_file})"
    fi
}

# Create monthly backup (copy from daily)
monthly_backup() {
    local latest_daily=$(ls -t "${BACKUP_DIR}/daily"/full_backup_*.tar.gz 2>/dev/null | head -1)

    if [ -n "${latest_daily}" ]; then
        local month=$(date +%Y-%m)
        local monthly_file="${BACKUP_DIR}/monthly/full_backup_${month}.tar.gz"

        cp "${latest_daily}" "${monthly_file}"
        cp "${latest_daily}.sha256" "${monthly_file}.sha256" 2>/dev/null || true

        log "Monthly backup created: $(basename ${monthly_file})"
    fi
}

# Restore from backup
restore_backup() {
    local backup_file="$1"
    local restore_dir="${BACKUP_DIR}/restore/restore_${TIMESTAMP}"

    log "=========================================="
    log "Starting RESTORE from: $(basename ${backup_file})"
    log "=========================================="

    # Verify backup first
    if ! verify_backup "${backup_file}"; then
        error "Backup verification failed, aborting restore"
        return 1
    fi

    # Create restore directory
    mkdir -p "${restore_dir}"

    # Extract backup
    log "Extracting backup..."
    tar -xzf "${backup_file}" -C "${restore_dir}" || {
        error "Failed to extract backup"
        return 1
    }

    # Stop service before restore
    log "Stopping service..."
    pm2 stop omega-watches 2>/dev/null || true

    # Backup current state before restore (just in case)
    local current_backup="${DATA_DIR}_backup_before_restore_${TIMESTAMP}"
    mv "${DATA_DIR}" "${current_backup}" 2>/dev/null || true

    # Restore database
    if [ -f "${restore_dir}/database_"*.tar.gz ]; then
        log "Restoring database..."
        mkdir -p "${DATA_DIR}"
        tar -xzf "${restore_dir}"/database_*.tar.gz -C "${DATA_DIR}"
    fi

    # Restore configuration if needed
    if [ -f "${restore_dir}/config_"*.tar.gz ]; then
        log "Configuration backup found (manual restore required)"
    fi

    # Restart service
    log "Restarting service..."
    pm2 restart omega-watches || pm2 start "${PROJECT_DIR}/server.js" --name omega-watches

    log "=========================================="
    log "Restore completed successfully!"
    log "Previous state backed up to: ${current_backup}"
    log "=========================================="
}

# List available backups
list_backups() {
    echo ""
    echo "=========================================="
    echo "Available Backups"
    echo "=========================================="
    echo ""

    echo "DAILY BACKUPS:"
    ls -lh "${BACKUP_DIR}/daily"/full_backup_*.tar.gz 2>/dev/null | awk '{print "  " $9 " - " $5}' || echo "  None"
    echo ""

    echo "WEEKLY BACKUPS:"
    ls -lh "${BACKUP_DIR}/weekly"/*.tar.gz 2>/dev/null | awk '{print "  " $9 " - " $5}' || echo "  None"
    echo ""

    echo "MONTHLY BACKUPS:"
    ls -lh "${BACKUP_DIR}/monthly"/*.tar.gz 2>/dev/null | awk '{print "  " $9 " - " $5}' || echo "  None"
    echo ""
}

# Get backup statistics
backup_stats() {
    echo ""
    echo "=========================================="
    echo "Backup Statistics"
    echo "=========================================="
    echo ""

    local total_backups=$(find "${BACKUP_DIR}" -name "*.tar.gz" | wc -l)
    local total_size=$(du -sh "${BACKUP_DIR}" 2>/dev/null | cut -f1)
    local latest_backup=$(ls -t "${BACKUP_DIR}/daily"/full_backup_*.tar.gz 2>/dev/null | head -1)

    echo "Total Backups: ${total_backups}"
    echo "Total Size: ${total_size}"
    echo ""

    if [ -n "${latest_backup}" ]; then
        echo "Latest Backup:"
        echo "  File: $(basename ${latest_backup})"
        echo "  Size: $(du -h ${latest_backup} | cut -f1)"
        echo "  Date: $(stat -c %y ${latest_backup} | cut -d. -f1)"

        # Verify latest backup
        if verify_backup "${latest_backup}" 2>&1 | grep -q "passed"; then
            echo "  Status: ✓ Valid"
        else
            echo "  Status: ✗ Invalid"
        fi
    fi
    echo ""
}

# Main execution
case "${1:-backup}" in
    backup|full)
        full_backup
        rotate_backups
        ;;
    weekly)
        full_backup
        weekly_backup
        rotate_backups
        ;;
    monthly)
        full_backup
        monthly_backup
        rotate_backups
        ;;
    restore)
        if [ -z "${2:-}" ]; then
            error "Usage: $0 restore <backup_file>"
            list_backups
            exit 1
        fi
        restore_backup "$2"
        ;;
    verify)
        if [ -z "${2:-}" ]; then
            error "Usage: $0 verify <backup_file>"
            exit 1
        fi
        verify_backup "$2"
        ;;
    list)
        list_backups
        ;;
    stats)
        backup_stats
        ;;
    rotate)
        rotate_backups
        ;;
    *)
        echo "Omega Watch Price History - Backup System"
        echo ""
        echo "Usage: $0 {backup|weekly|monthly|restore|verify|list|stats|rotate}"
        echo ""
        echo "Commands:"
        echo "  backup    - Create full daily backup"
        echo "  weekly    - Create weekly backup"
        echo "  monthly   - Create monthly backup"
        echo "  restore   - Restore from backup file"
        echo "  verify    - Verify backup integrity"
        echo "  list      - List all available backups"
        echo "  stats     - Show backup statistics"
        echo "  rotate    - Clean up old backups"
        echo ""
        exit 1
        ;;
esac