Complete implementation of multi-server Oracle database support: Backend: - Multi-pool Oracle with lazy loading per server - Email-to-server cache for automatic server discovery - JWT tokens include server_id claim - /auth/check-identity and /auth/check-email endpoints - /auth/my-servers endpoint for listing user's accessible servers - Server switch with password re-authentication Frontend: - New ServerSelector component for header dropdown - Multi-step login flow (identity → server → password) - Server switching from header with password modal - Mobile drawer menu with server selection - Dark mode support for all new components - URL bookmark support with ?server= query param Scripts: - Unified start.sh replacing start-prod.sh/start-test.sh - Unified ssh-tunnel.sh with multi-server support - Updated status.sh for new architecture Tests: - E2E tests for multi-server and single-server login flows - Backend unit tests for all new endpoints - Oracle multi-pool integration tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
390 lines
12 KiB
Bash
390 lines
12 KiB
Bash
#!/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 <backup_name>|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 "$@" |