#!/bin/bash # ROA2WEB Database and Application Backup Script # Creates backups of Oracle database, Docker volumes, and configuration set -e # Configuration SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" BACKUP_DIR="$PROJECT_DIR/backups" LOG_FILE="$PROJECT_DIR/backup.log" RETENTION_DAYS=30 MAX_BACKUPS=10 # 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 shift local message="$*" local timestamp=$(date '+%Y-%m-%d %H:%M:%S') echo -e "[$timestamp] [$level] $message" | tee -a "$LOG_FILE" } # Error handling error_exit() { log "ERROR" "$1" exit 1 } # Success message success() { log "SUCCESS" "$1" } # Info message info() { log "INFO" "$1" } # Warning message warning() { log "WARNING" "$1" } # Load environment variables load_env() { if [[ -f "$PROJECT_DIR/.env" ]]; then set -a source "$PROJECT_DIR/.env" set +a elif [[ -f "$PROJECT_DIR/.env.production" ]]; then set -a source "$PROJECT_DIR/.env.production" set +a else error_exit "No environment file found" fi } # Create backup directory structure create_backup_structure() { local backup_name=$1 local backup_path="$BACKUP_DIR/$backup_name" mkdir -p "$backup_path"/{database,volumes,config,logs} echo "$backup_path" } # Backup Oracle database backup_database() { local backup_path=$1 info "Starting Oracle database backup..." # Check if SSH tunnel is required if [[ "$ORACLE_HOST" == "localhost" && -f "$PROJECT_DIR/ssh-tunnel.sh" ]]; then info "Ensuring SSH tunnel is running..." "$PROJECT_DIR/ssh-tunnel.sh" status || "$PROJECT_DIR/ssh-tunnel.sh" start fi # Create database backup using Oracle export local db_backup_file="$backup_path/database/roa_backup_$(date +%Y%m%d_%H%M%S).dmp" local log_file="$backup_path/database/export.log" # Export specific schemas (adjust as needed) if command -v expdp &> /dev/null; then info "Using Oracle Data Pump for backup..." expdp \ userid="$ORACLE_USER/$ORACLE_PASSWORD@$ORACLE_HOST:$ORACLE_PORT/$ORACLE_SID" \ schemas="$ORACLE_USER" \ dumpfile="$(basename "$db_backup_file")" \ logfile="$(basename "$log_file")" \ directory=DATA_PUMP_DIR \ compression=ALL &> /dev/null || warning "Data Pump backup failed, trying alternative method" fi # Alternative: SQL backup for essential data if [[ ! -f "$db_backup_file" ]]; then info "Creating SQL backup of essential tables..." cat > "$backup_path/database/backup.sql" << EOF -- ROA2WEB Database Backup $(date) -- Essential tables backup -- Users table CREATE TABLE users_backup AS SELECT * FROM users; -- Companies table CREATE TABLE companies_backup AS SELECT * FROM companies; -- Invoices table CREATE TABLE invoices_backup AS SELECT * FROM invoices; -- Payments table CREATE TABLE payments_backup AS SELECT * FROM payments; -- Authentication tokens (structure only for security) CREATE TABLE auth_tokens_backup AS SELECT user_id, created_at, expires_at FROM auth_tokens WHERE 1=0; COMMIT; EOF # Execute SQL backup if sqlplus is available if command -v sqlplus &> /dev/null; then sqlplus -s "$ORACLE_USER/$ORACLE_PASSWORD@$ORACLE_HOST:$ORACLE_PORT/$ORACLE_SID" @"$backup_path/database/backup.sql" > "$backup_path/database/backup_output.log" 2>&1 || warning "SQL backup failed" fi fi success "Database backup completed" } # Backup Docker volumes backup_volumes() { local backup_path=$1 info "Backing up Docker volumes..." # Backup nginx logs if docker volume ls | grep -q "roa2web_nginx-logs"; then info "Backing up nginx logs..." docker run --rm \ -v roa2web_nginx-logs:/data:ro \ -v "$backup_path/volumes":/backup \ alpine tar czf /backup/nginx-logs.tar.gz -C /data . 2>/dev/null || warning "Nginx logs backup failed" fi # Backup SSL certificates if docker volume ls | grep -q "roa2web_ssl-certs"; then info "Backing up SSL certificates..." docker run --rm \ -v roa2web_ssl-certs:/data:ro \ -v "$backup_path/volumes":/backup \ alpine tar czf /backup/ssl-certs.tar.gz -C /data . 2>/dev/null || warning "SSL certs backup failed" fi # Backup Redis data if docker volume ls | grep -q "roa2web_redis-data"; then info "Backing up Redis data..." docker run --rm \ -v roa2web_redis-data:/data:ro \ -v "$backup_path/volumes":/backup \ alpine tar czf /backup/redis-data.tar.gz -C /data . 2>/dev/null || warning "Redis data backup failed" fi # Backup backend logs if docker volume ls | grep -q "roa2web_backend-logs"; then info "Backing up backend logs..." docker run --rm \ -v roa2web_backend-logs:/data:ro \ -v "$backup_path/volumes":/backup \ alpine tar czf /backup/backend-logs.tar.gz -C /data . 2>/dev/null || warning "Backend logs backup failed" fi success "Docker volumes backup completed" } # Backup configuration files backup_config() { local backup_path=$1 info "Backing up configuration files..." # Copy environment files cp -r "$PROJECT_DIR"/.env* "$backup_path/config/" 2>/dev/null || true # Copy Docker Compose files cp "$PROJECT_DIR"/docker-compose*.yml "$backup_path/config/" # Copy nginx configuration if [[ -d "$PROJECT_DIR/nginx/conf" ]]; then cp -r "$PROJECT_DIR/nginx/conf" "$backup_path/config/" fi # Copy SSL configuration (if exists) if [[ -d "$PROJECT_DIR/nginx/ssl" ]]; then cp -r "$PROJECT_DIR/nginx/ssl" "$backup_path/config/" 2>/dev/null || true fi # Copy deployment scripts if [[ -d "$PROJECT_DIR/scripts" ]]; then cp -r "$PROJECT_DIR/scripts" "$backup_path/config/" fi # Create backup metadata cat > "$backup_path/backup_info.txt" << EOF ROA2WEB Backup Information ========================= Backup Date: $(date) Backup Type: Full Backup Environment: ${ENVIRONMENT:-unknown} Oracle Host: ${ORACLE_HOST:-unknown} Oracle User: ${ORACLE_USER:-unknown} Git Commit: $(git rev-parse HEAD 2>/dev/null || echo "unknown") Git Branch: $(git branch --show-current 2>/dev/null || echo "unknown") Docker Images: $(docker images --format "table {{.Repository}}:{{.Tag}}\t{{.CreatedAt}}\t{{.Size}}" | grep roa2web || echo "No ROA2WEB images found") EOF success "Configuration backup completed" } # Clean old backups cleanup_old_backups() { info "Cleaning up old backups..." # Remove backups older than retention period find "$BACKUP_DIR" -name "backup_*" -type d -mtime +$RETENTION_DAYS -exec rm -rf {} \; 2>/dev/null || true # Keep only the latest MAX_BACKUPS local backup_count=$(find "$BACKUP_DIR" -name "backup_*" -type d | wc -l) if [[ $backup_count -gt $MAX_BACKUPS ]]; then local excess=$((backup_count - MAX_BACKUPS)) find "$BACKUP_DIR" -name "backup_*" -type d -printf '%T+ %p\n' | sort | head -n $excess | cut -d' ' -f2- | xargs rm -rf info "Removed $excess old backups" fi success "Backup cleanup completed" } # List available backups list_backups() { info "Available backups:" echo "" if [[ ! -d "$BACKUP_DIR" ]] || [[ -z "$(ls -A "$BACKUP_DIR" 2>/dev/null)" ]]; then warning "No backups found" return fi find "$BACKUP_DIR" -name "backup_*" -type d -printf '%T+ %p\n' | sort -r | while read -r line; do local date_part=$(echo "$line" | cut -d' ' -f1 | cut -d+ -f1) local backup_name=$(basename "$(echo "$line" | cut -d' ' -f2)") local backup_path="$(echo "$line" | cut -d' ' -f2)" echo "📦 $backup_name ($date_part)" if [[ -f "$backup_path/backup_info.txt" ]]; then grep -E "(Backup Date|Environment|Git)" "$backup_path/backup_info.txt" | sed 's/^/ /' fi echo "" done } # Restore from backup restore_backup() { local backup_name=$1 if [[ -z "$backup_name" ]]; then error_exit "Backup name is required for restore operation" fi local backup_path="$BACKUP_DIR/$backup_name" if [[ ! -d "$backup_path" ]]; then error_exit "Backup not found: $backup_name" fi warning "Restoring from backup: $backup_name" warning "This will overwrite current data. Press Ctrl+C to cancel or wait 10 seconds to continue..." sleep 10 info "Starting restore process..." # Stop services docker-compose -f "$PROJECT_DIR/docker-compose.yml" -f "$PROJECT_DIR/docker-compose.production.yml" down 2>/dev/null || true # Restore configuration if [[ -d "$backup_path/config" ]]; then info "Restoring configuration files..." cp -r "$backup_path/config"/.env* "$PROJECT_DIR/" 2>/dev/null || true cp -r "$backup_path/config"/docker-compose*.yml "$PROJECT_DIR/" if [[ -d "$backup_path/config/conf" ]]; then mkdir -p "$PROJECT_DIR/nginx" cp -r "$backup_path/config/conf" "$PROJECT_DIR/nginx/" fi fi # Restore volumes if [[ -d "$backup_path/volumes" ]]; then info "Restoring Docker volumes..." for volume_backup in "$backup_path/volumes"/*.tar.gz; do if [[ -f "$volume_backup" ]]; then local volume_name=$(basename "$volume_backup" .tar.gz) docker volume create "roa2web_$volume_name" 2>/dev/null || true docker run --rm \ -v "roa2web_$volume_name":/data \ -v "$backup_path/volumes":/backup \ alpine tar xzf "/backup/$(basename "$volume_backup")" -C /data info "Restored volume: $volume_name" fi done fi success "Restore completed. Please restart services manually." } # Main function main() { local action=${1:-backup} case $action in "backup"|"full") info "=== ROA2WEB Full Backup ===" load_env local backup_name="backup_$(date +%Y%m%d_%H%M%S)" local backup_path=$(create_backup_structure "$backup_name") mkdir -p "$BACKUP_DIR" backup_database "$backup_path" backup_volumes "$backup_path" backup_config "$backup_path" cleanup_old_backups success "Full backup completed: $backup_name" ;; "database"|"db") info "=== ROA2WEB Database Backup ===" load_env local backup_name="db_backup_$(date +%Y%m%d_%H%M%S)" local backup_path=$(create_backup_structure "$backup_name") backup_database "$backup_path" success "Database backup completed: $backup_name" ;; "volumes") info "=== ROA2WEB Volumes Backup ===" local backup_name="volumes_backup_$(date +%Y%m%d_%H%M%S)" local backup_path=$(create_backup_structure "$backup_name") backup_volumes "$backup_path" success "Volumes backup completed: $backup_name" ;; "list") list_backups ;; "restore") restore_backup "$2" ;; "cleanup") cleanup_old_backups ;; *) echo "Usage: $0 {backup|database|volumes|list|restore |cleanup}" echo "" echo "Commands:" echo " backup|full - Create full backup (database + volumes + config)" echo " database|db - Backup only Oracle database" echo " volumes - Backup only Docker volumes" echo " list - List available backups" echo " restore - Restore from backup" echo " cleanup - Clean up old backups" exit 1 ;; esac } # Run main function main "$@"