Files
roa2web-service-auto/scripts/backup.sh
Claude Agent b137e80b71 feat: multi-Oracle server support with runtime switching
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>
2026-01-26 22:39:06 +00:00

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 "$@"