#!/bin/bash ################################################################################ # MySQL/MariaDB File-Based Restore to SQL Dump ################################################################################ # Purpose: Convert restored MySQL/MariaDB data files to usable .sql dumps # Use Case: Restore InnoDB databases from file-based backups (Acronis, etc.) # # Features: # - Interactive guided workflow # - Validates MySQL data directory structure # - Starts second MySQL instance for safe extraction # - Creates SQL dumps from restored files # - Handles InnoDB system tablespace requirements # - Optional force-recovery mode for corrupted databases ################################################################################ # Path resolution (modules/backup/script.sh → ../../) SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" source "$SCRIPT_DIR/lib/common-functions.sh" source "$SCRIPT_DIR/lib/system-detect.sh" # Root check if [ "$EUID" -ne 0 ]; then print_error "This script must be run as root" exit 1 fi # Detect control panel for proper directory paths detect_control_panel >/dev/null 2>&1 || true ################################################################################ # GLOBAL VARIABLES ################################################################################ RESTORE_DIR="" DATABASE_NAME="" LIVE_DATADIR="" TEMP_DATADIR="" TICKET_NUMBER="" FORCE_RECOVERY="" MYSQL_VERSION="" MYSQL_VARIANT="" # mysql or mariadb SECOND_INSTANCE_RUNNING=0 # Track if second instance is running # Cleanup trap for interruption/exit cleanup_on_exit() { if [ "$SECOND_INSTANCE_RUNNING" -eq 1 ] && [ -n "$TEMP_DATADIR" ]; then echo "" print_warning "Script interrupted - cleaning up second MySQL instance..." if [ -S "$TEMP_DATADIR/socket.mysql" ]; then mysqladmin -h localhost -S "$TEMP_DATADIR/socket.mysql" shutdown 2>/dev/null || true sleep 1 print_success "Second instance shut down safely" fi fi } # Set trap for signals trap cleanup_on_exit EXIT INT TERM ################################################################################ # UTILITY FUNCTIONS ################################################################################ # Detect MySQL version and variant detect_mysql_version() { print_info "Detecting MySQL version and variant..." # Try to get version from running instance if systemctl is-active --quiet mysqld || systemctl is-active --quiet mariadb; then local version_output=$(mysql -V 2>/dev/null || mysqld --version 2>/dev/null | head -1) if echo "$version_output" | grep -qi "mariadb"; then MYSQL_VARIANT="mariadb" MYSQL_VERSION=$(echo "$version_output" | grep -oP '\d+\.\d+\.\d+' | head -1) echo " Detected: MariaDB $MYSQL_VERSION" else MYSQL_VARIANT="mysql" MYSQL_VERSION=$(echo "$version_output" | grep -oP '\d+\.\d+\.\d+' | head -1) echo " Detected: MySQL $MYSQL_VERSION" fi return 0 fi # Fallback: Check binaries if command -v mariadb --version &> /dev/null; then MYSQL_VARIANT="mariadb" MYSQL_VERSION=$(mariadb --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' | head -1) echo " Detected: MariaDB $MYSQL_VERSION" elif command -v mysqld &> /dev/null; then MYSQL_VARIANT="mysql" MYSQL_VERSION=$(mysqld --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' | head -1) echo " Detected: MySQL $MYSQL_VERSION" else print_warning "Could not detect MySQL variant/version" MYSQL_VARIANT="unknown" MYSQL_VERSION="unknown" return 1 fi return 0 } # Detect live MySQL data directory detect_mysql_datadir() { print_info "Detecting MySQL data directory..." # Method 1: Check if MySQL is running if systemctl is-active --quiet mysqld || systemctl is-active --quiet mariadb; then LIVE_DATADIR=$(mysql -NBe 'SELECT @@datadir;' 2>/dev/null) if [ -n "$LIVE_DATADIR" ]; then echo " Detected from running MySQL: $LIVE_DATADIR" return 0 fi fi # Method 2: Check configuration files local config_dir=$(grep -r "^datadir" /etc/my.cnf /etc/my.cnf.d/* 2>/dev/null | head -1 | cut -d'=' -f2 | tr -d ' ') if [ -n "$config_dir" ]; then LIVE_DATADIR="$config_dir" echo " Detected from config: $LIVE_DATADIR" return 0 fi # Method 3: Default location if [ -d "/var/lib/mysql" ]; then LIVE_DATADIR="/var/lib/mysql" echo " Using default: $LIVE_DATADIR" return 0 fi print_warning "Could not auto-detect MySQL data directory" return 1 } # Validate restored data directory structure validate_restore_structure() { local dir="$1" local missing_files=() print_info "Validating restored data structure..." # Check for InnoDB system tablespace if [ ! -f "$dir/ibdata1" ]; then missing_files+=("ibdata1") fi # Check for redo logs (version-specific) # IMPORTANT: MySQL 8.0.30+ changed redo log architecture # MySQL 8.0.30+: #innodb_redo directory with #ib_redoN files # MySQL 8.0.0-8.0.29: ib_logfile0/ib_logfile1 # MySQL 5.7 and MariaDB: ib_logfile0/ib_logfile1 local major_version=$(echo "$MYSQL_VERSION" | cut -d'.' -f1) local minor_version=$(echo "$MYSQL_VERSION" | cut -d'.' -f2) if [ "$MYSQL_VARIANT" = "mysql" ] && [ -n "$major_version" ] && [ "$major_version" -ge 8 ]; then # Check if MySQL 8.0.30+ (new redo log format) if [ -n "$minor_version" ] && [ "$major_version" -eq 8 ] && [ "$minor_version" -ge 0 ]; then # Try to detect 8.0.30+ by checking patch version or directory existence if [ -d "$dir/#innodb_redo" ]; then # MySQL 8.0.30+: #innodb_redo directory exists print_info "Detected MySQL 8.0.30+ redo log format (#innodb_redo)" elif [ -f "$dir/ib_logfile0" ]; then # MySQL 8.0.0-8.0.29: old format print_info "Detected MySQL 8.0.0-8.0.29 redo log format (ib_logfile)" else missing_files+=("ib_logfile0 OR #innodb_redo directory (MySQL 8.0)") fi else # MySQL 9.0+ or other major versions if [ ! -d "$dir/#innodb_redo" ]; then missing_files+=("#innodb_redo directory (MySQL 8.0.30+/9.0+)") fi fi else # MySQL 5.7 and MariaDB: Always use ib_logfile0/ib_logfile1 if [ ! -f "$dir/ib_logfile0" ]; then missing_files+=("ib_logfile0 (MySQL 5.7/MariaDB)") fi if [ ! -f "$dir/ib_logfile1" ]; then print_warning "ib_logfile1 not found (may be optional)" fi fi # Check for mysql system database if [ ! -d "$dir/mysql" ] && [ ! -f "$dir/mysql.ibd" ]; then missing_files+=("mysql directory or mysql.ibd") fi # Check for target database if [ -n "$DATABASE_NAME" ] && [ ! -d "$dir/$DATABASE_NAME" ]; then missing_files+=("$DATABASE_NAME directory") fi if [ ${#missing_files[@]} -gt 0 ]; then print_error "Missing required files/directories:" for file in "${missing_files[@]}"; do echo " - $file" done return 1 fi print_success "Data structure validation passed" return 0 } # Check error log for InnoDB startup issues check_innodb_errors() { local error_log="$1" local check_recent="${2:-no}" # "yes" = only check recent errors, "no" = full check if [ ! -f "$error_log" ]; then return 0 # No error log yet, assume OK fi local errors_found=0 local critical_errors=() # InnoDB critical error patterns local error_patterns=( "InnoDB: Corrupted" "InnoDB: Database page corruption" "InnoDB: Unable to open" "InnoDB: Cannot allocate memory" "InnoDB: Tablespace.*missing" "InnoDB: Redo log.*corrupt" "InnoDB:.*redo log.*incompatible" "InnoDB: Plugin initialization aborted" "\[ERROR\].*InnoDB" ) # If checking recent errors, only look at last 50 lines local log_content if [ "$check_recent" = "yes" ]; then log_content=$(tail -50 "$error_log" 2>/dev/null) else log_content=$(cat "$error_log" 2>/dev/null) fi # Check each pattern for pattern in "${error_patterns[@]}"; do if echo "$log_content" | grep -qE "$pattern"; then local error_line=$(echo "$log_content" | grep -E "$pattern" | tail -1) critical_errors+=("$error_line") errors_found=$((errors_found + 1)) fi done if [ -n "$errors_found" ] && [ "$errors_found" -gt 0 ]; then print_error "InnoDB errors detected in $error_log:" for err in "${critical_errors[@]}"; do echo " - ${err:0:120}..." done return 1 fi return 0 } # Show intelligent recovery options based on error type show_recovery_options() { local datadir="$1" local current_recovery="${2:-0}" print_banner "Recovery Options" # Analyze the error log to determine failure type local error_log="$datadir/mysql.err" local missing_files="" local corruption_detected="" local redo_incompatible="" local memory_issue="" if [ -f "$error_log" ]; then if grep -qE "Cannot open tablespace|Tablespace.*missing|Unable to open" "$error_log"; then missing_files="yes" fi if grep -qE "Corrupted|Database page corruption" "$error_log"; then corruption_detected="yes" fi if grep -qE "redo log.*incompatible|redo log.*different|redo log format" "$error_log"; then redo_incompatible="yes" fi if grep -qE "Cannot allocate memory|Out of memory" "$error_log"; then memory_issue="yes" fi fi # Provide targeted guidance based on error type if [ -n "$missing_files" ]; then print_error "DIAGNOSIS: Missing or unopenable tablespace files" echo "" # Parse error log to find EXACT missing files echo "Analyzing error log for missing files..." echo "" local missing_list=() local missing_count=0 # Extract tablespace names from various error patterns while IFS= read -r error_line; do # Pattern 1: "Cannot open tablespace 'db/table'" if echo "$error_line" | grep -qE "Cannot open|Unable to open|Tablespace.*missing"; then local tablespace=$(echo "$error_line" | grep -oE "'[^']+'" | tr -d "'" | head -1) if [ -n "$tablespace" ]; then missing_list+=("$tablespace") missing_count=$((missing_count + 1)) fi fi # Pattern 2: "Cannot find space id N in the tablespace memory cache" if echo "$error_line" | grep -qE "Cannot find space id.*in the tablespace"; then local space_id=$(echo "$error_line" | grep -oE "space id [0-9]+" | awk '{print $3}') if [ -n "$space_id" ]; then missing_list+=("space_id_$space_id") missing_count=$((missing_count + 1)) fi fi done < <(grep -iE "Cannot open|Unable to open|Tablespace.*missing|Cannot find space id" "$error_log" 2>/dev/null) if [ "$missing_count" -gt 0 ]; then print_warning "MISSING FILES DETECTED ($missing_count found):" echo "" # Remove duplicates and display local unique_missing=($(printf '%s\n' "${missing_list[@]}" | sort -u)) local file_num=1 for item in "${unique_missing[@]}"; do if [[ "$item" == *"/"* ]]; then # Format: database/table local db_name=$(echo "$item" | cut -d'/' -f1) local table_name=$(echo "$item" | cut -d'/' -f2) echo " $file_num) Table: $table_name (in database: $db_name)" echo " File needed: $datadir/$db_name/${table_name}.ibd" echo " Backup path: /var/lib/mysql/$db_name/${table_name}.ibd" elif [[ "$item" == space_id_* ]]; then echo " $file_num) Unknown tablespace (space ID: ${item#space_id_})" echo " Action: Restore entire database directory to ensure all files present" else echo " $file_num) $item" fi echo "" file_num=$((file_num + 1)) done else print_warning "Could not parse specific missing files from error log" echo "Showing raw error lines:" echo "" grep -iE "Cannot open|Unable to open|Tablespace.*missing" "$error_log" 2>/dev/null | head -10 echo "" fi echo "Common causes:" echo " - Not all database table files (.ibd) were restored" echo " - Table files are in wrong location" echo " - Permissions issue (not mysql:mysql)" echo "" print_warning "RECOMMENDED ACTIONS:" echo "" echo " Option 1: Restore Missing Files (RECOMMENDED)" echo " ────────────────────────────────────────────────" if [ "$missing_count" -gt 0 ]; then echo " 1. Restore the $missing_count file(s) listed above from your backup" echo "" echo " 2. Use Acronis/rsync/cp to copy .ibd files:" echo " Example commands:" for item in "${unique_missing[@]}"; do if [[ "$item" == *"/"* ]]; then local db_name=$(echo "$item" | cut -d'/' -f1) local table_name=$(echo "$item" | cut -d'/' -f2) echo " cp /backup/path/$db_name/${table_name}.ibd $datadir/$db_name/" fi done echo "" echo " 3. Fix ownership:" echo " chown mysql:mysql $datadir/$DATABASE_NAME/*.ibd" else echo " 1. Check error log manually:" echo " grep -i 'cannot open\\|missing' $error_log" echo "" echo " 2. Restore identified .ibd files from backup" fi echo "" echo " 4. Re-run this script (will detect newly added files)" echo "" echo " Option 2: Restore Entire Database Directory" echo " ────────────────────────────────────────────────" echo " If you're missing many files, easier to restore all:" echo "" echo " 1. Remove partial database directory:" echo " rm -rf $datadir/$DATABASE_NAME" echo "" echo " 2. Restore complete database directory from backup:" echo " cp -r /backup/path/$DATABASE_NAME $datadir/" echo "" echo " 3. Fix ownership:" echo " chown -R mysql:mysql $datadir/$DATABASE_NAME" echo "" echo " 4. Re-run this script" echo "" echo " Option 3: Start Completely Fresh" echo " ────────────────────────────────────────────────" echo " 1. Clear the entire restore directory:" echo " rm -rf $datadir/*" echo "" echo " 2. Restore ALL files from backup (complete set):" echo " - ibdata1" echo " - redo logs (ib_logfile* or #innodb_redo/)" echo " - mysql/ directory" echo " - All database directories" echo "" echo " 3. Re-run this script from the beginning" echo "" elif [ -n "$redo_incompatible" ]; then print_error "DIAGNOSIS: Redo log incompatibility" echo "" echo "Common causes:" echo " - Backup from different MySQL version" echo " - Mixed redo log formats (8.0.30 vs older)" echo " - Partial restore (old + new redo logs mixed)" echo "" print_warning "RECOMMENDED ACTIONS:" echo "" echo " Option 1: Start Fresh with Correct Redo Logs" echo " ────────────────────────────────────────────────" echo " 1. Remove current redo logs:" if [ -d "$datadir/#innodb_redo" ]; then echo " rm -rf $datadir/#innodb_redo" else echo " rm -f $datadir/ib_logfile*" fi echo "" echo " 2. Restore redo logs from SAME backup date:" echo " - Must match the ibdata1 file exactly" echo " - Check backup timestamp carefully" echo "" echo " 3. Re-run this script" echo "" echo " Option 2: Force Recovery (if redo logs are lost)" echo " ────────────────────────────────────────────────" echo " Some data loss may occur, but better than nothing" echo " Re-run script and select Force Recovery Level 6" echo "" elif [ -n "$corruption_detected" ]; then print_error "DIAGNOSIS: InnoDB corruption detected" echo "" print_warning "RECOMMENDED ACTIONS (IN ORDER):" echo "" if [ "$current_recovery" = "0" ] || [ -z "$current_recovery" ]; then echo " Option 1: Try Force Recovery Level 1" echo " ────────────────────────────────────────────────" echo " Re-run script → Step 4 → Select recovery mode 1" echo " (Ignores corrupt pages)" echo "" fi if [ "$current_recovery" = "1" ]; then echo " Option 2: Try Force Recovery Level 4" echo " ────────────────────────────────────────────────" echo " Re-run script → Step 4 → Select recovery mode 4" echo " (Prevents insert buffer merge)" echo "" fi if [ "${current_recovery:-0}" -ge 4 ]; then echo " Option 2: Try Force Recovery Level 6 (LAST RESORT)" echo " ────────────────────────────────────────────────" echo " Re-run script → Step 4 → Select recovery mode 6" echo " (Skips page checksums - maximum data recovery)" echo "" fi echo " Option 3: Start Fresh" echo " ────────────────────────────────────────────────" echo " 1. Corruption may be in the backup itself" echo " 2. Try restoring from an older backup date" echo " 3. Clear directory: rm -rf $datadir/*" echo " 4. Restore from different backup snapshot" echo "" elif [ -n "$memory_issue" ]; then print_error "DIAGNOSIS: Memory allocation failure" echo "" print_warning "RECOMMENDED ACTIONS:" echo "" echo " 1. Check available memory: free -h" echo " 2. Stop other MySQL instances: systemctl stop mysqld" echo " 3. Re-run this script" echo "" else # Generic troubleshooting print_warning "TROUBLESHOOTING STEPS:" echo "" echo " 1. Review Error Log" echo " ────────────────────────────────────────────────" echo " tail -100 $error_log | less" echo "" echo " 2. Verify File Structure" echo " ────────────────────────────────────────────────" echo " ls -laR $datadir/ | less" echo " Look for: ibdata1, redo logs, mysql/, database/" echo "" echo " 3. Check Ownership" echo " ────────────────────────────────────────────────" echo " stat -c '%U:%G' $datadir/ibdata1" echo " Should be: mysql:mysql" echo "" echo " 4. Try Force Recovery" echo " ────────────────────────────────────────────────" echo " Re-run script → Step 4 → Select recovery mode 1-6" echo "" echo " 5. Start Fresh" echo " ────────────────────────────────────────────────" echo " rm -rf $datadir/*" echo " Restore complete file set from backup" echo " Re-run script" echo "" fi # Always show the error log location echo "" print_info "Full error log location:" echo " $error_log" echo "" # Offer to show recent errors echo -n "View recent errors from log now? (y/n): " read -r view_errors if [ "$view_errors" = "y" ]; then echo "" echo "════════════════════════════════════════════════════════════════" echo "LAST 50 LINES OF ERROR LOG" echo "════════════════════════════════════════════════════════════════" tail -50 "$error_log" 2>/dev/null || echo "Error log not found" echo "════════════════════════════════════════════════════════════════" echo "" fi } # Check available disk space (CRITICAL SAFETY CHECK #3) check_disk_space() { local target_dir="$1" local estimated_size_mb="${2:-100}" # Minimum 100MB default print_info "Checking available disk space..." # Get available space in MB local available_mb=$(df -BM "$target_dir" | awk 'NR==2 {print $4}' | tr -d 'M') local available_gb=$(awk "BEGIN {printf \"%.2f\", $available_mb/1024}") # Require at least 2x the estimated size (safety margin) local required_mb=$((estimated_size_mb * 2)) local required_gb=$(awk "BEGIN {printf \"%.2f\", $required_mb/1024}") echo " Available space: ${available_gb}GB (${available_mb}MB)" echo " Recommended minimum: ${required_gb}GB" # Check if we have enough space if [ -n "$available_mb" ] && [ "$available_mb" -lt "$required_mb" ]; then print_error "Insufficient disk space!" echo "" echo " Available: ${available_gb}GB" echo " Required: ${required_gb}GB" echo " Shortage: $(awk "BEGIN {printf \"%.2f\", ($required_mb - $available_mb)/1024}")GB" echo "" echo "Free up space or use a different directory." return 1 fi print_success "Sufficient disk space available (${available_gb}GB free)" return 0 } # Validate force recovery level warnings (CRITICAL SAFETY CHECK #6) warn_force_recovery() { local level="${1:-0}" if [ -z "$level" ] || [ "$level" -eq 0 ]; then return 0 fi echo "" print_warning "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" print_warning " FORCE RECOVERY MODE ACTIVE: Level $level" print_warning "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" case $level in 1) echo "Level 1: Ignore Corrupt Pages" echo " - Data Loss: Minimal (only corrupt pages skipped)" echo " - Safety: Relatively safe" ;; 2) echo "Level 2: Prevent Background Operations" echo " - Data Loss: Minimal" echo " - Safety: Moderate" ;; 3) echo "Level 3: Prevent Transaction Rollbacks" echo " - Data Loss: Some uncommitted data may persist" echo " - Safety: Moderate risk" ;; 4) echo "Level 4: Prevent Insert Buffer Merge" echo " - Data Loss: Moderate (recent inserts may be lost)" echo " - Safety: Higher risk" ;; 5) echo "Level 5: Skip Log Redo" echo " - Data Loss: HIGH (recent transactions may be incomplete)" echo " - Safety: HIGH RISK" print_error " WARNING: May result in inconsistent data!" ;; 6) echo "Level 6: Skip Page Checksums (MAXIMUM RECOVERY)" echo " - Data Loss: VERY HIGH (corrupted data WILL be included)" echo " - Safety: VERY HIGH RISK" print_error " CRITICAL: Use only as LAST RESORT!" print_error " Exported data WILL contain corrupted/invalid records!" ;; esac echo "" if [ "$level" -ge 5 ]; then print_error "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" print_error " DANGEROUS RECOVERY LEVEL!" print_error "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "The resulting SQL dump may contain:" echo " ✗ Incomplete transactions" echo " ✗ Corrupted data" echo " ✗ Invalid foreign key relationships" echo " ✗ Inconsistent table states" echo "" print_warning "Type 'I UNDERSTAND THE RISKS' to continue:" echo -n "> " read -r risk_confirm if [ "$risk_confirm" != "I UNDERSTAND THE RISKS" ]; then print_error "Recovery cancelled for safety" echo "Consider using a lower recovery level or older backup." return 1 fi echo "" print_success "Risk acknowledgment confirmed" fi echo "" return 0 } # Start second MySQL instance start_second_instance() { local datadir="$1" local force_recovery="${2:-}" print_info "Starting second MySQL instance..." print_warning "This may take a few moments..." # CRITICAL SAFETY CHECK: Ensure we're not using live MySQL data directory if [ "$datadir" = "/var/lib/mysql" ] || [ "$datadir" = "$LIVE_DATADIR" ]; then print_error "CRITICAL SAFETY ERROR: Attempting to use LIVE MySQL data directory!" echo "" echo "Data directory specified: $datadir" echo "Live MySQL directory: $LIVE_DATADIR" echo "" print_error "This script must use a SEPARATE restore directory" print_error "NEVER run on the live MySQL data directory" echo "" echo "Expected restore directory format:" echo " /home/temp/restore*/mysql" echo " /root/restore*/mysql" echo " Any path OTHER than $LIVE_DATADIR" echo "" return 1 fi # Verify using custom socket (not live MySQL socket) if [ -S "/var/lib/mysql/mysql.sock" ] && [ "$datadir/socket.mysql" = "/var/lib/mysql/mysql.sock" ]; then print_error "CRITICAL: Attempting to use live MySQL socket!" return 1 fi # Display isolation confirmation echo "" print_success "Safety checks passed:" echo " Second instance datadir: $datadir" echo " Second instance socket: $datadir/socket.mysql" echo " Live MySQL datadir: $LIVE_DATADIR" echo " Live MySQL socket: /var/lib/mysql/mysql.sock (PROTECTED)" echo "" # Clear or backup old error log if [ -f "$datadir/mysql.err" ]; then print_info "Backing up old error log..." mv "$datadir/mysql.err" "$datadir/mysql.err.old" 2>/dev/null || true fi # Check if socket already exists (instance already running) if [ -S "$datadir/socket.mysql" ]; then print_warning "Socket file already exists. Attempting to shut down existing instance..." mysqladmin -h localhost -S "$datadir/socket.mysql" shutdown 2>/dev/null || true sleep 2 fi # Build mysqld command local mysqld_cmd="/usr/libexec/mysqld" if ! command -v "$mysqld_cmd" &> /dev/null; then mysqld_cmd="mysqld" fi # Start in background - build args array to avoid eval local mysqld_args=( "--datadir=$datadir" "--socket=$datadir/socket.mysql" "--pid-file=$datadir/mysql.pid" "--log-error=$datadir/mysql.err" "--skip-grant-tables" "--skip-networking" "--user=mysql" ) if [ -n "$force_recovery" ]; then mysqld_args+=("--innodb-force-recovery=$force_recovery") print_warning "Using InnoDB force recovery mode: $force_recovery" fi # Start in background "$mysqld_cmd" "${mysqld_args[@]}" & local pid=$! # Wait for instance to start (max 30 seconds) local count=0 while [ -n "$count" ] && [ "$count" -lt 30 ]; do if [ -S "$datadir/socket.mysql" ]; then print_success "Second MySQL instance started (PID: $pid)" # Give InnoDB a moment to initialize sleep 2 # Check for InnoDB errors in the error log echo "" print_info "Checking InnoDB startup status..." if ! check_innodb_errors "$datadir/mysql.err" "yes"; then print_error "InnoDB initialization encountered errors" echo "" print_warning "Attempting to shut down second instance..." mysqladmin -h localhost -S "$datadir/socket.mysql" shutdown 2>/dev/null || true echo "" print_info "Review full error log:" echo " tail -100 $datadir/mysql.err" echo "" print_info "Consider using force recovery mode (re-run script, option 1-6)" return 1 fi print_success "InnoDB initialized successfully - no critical errors detected" # Mark second instance as running for cleanup trap SECOND_INSTANCE_RUNNING=1 return 0 fi sleep 1 count=$((count + 1)) done # Check if process is still running if ! kill -0 $pid 2>/dev/null; then print_error "Second MySQL instance failed to start" echo "" # Check for InnoDB errors if ! check_innodb_errors "$datadir/mysql.err" "no"; then echo "" fi print_info "Full error log (last 50 lines):" if [ -f "$datadir/mysql.err" ]; then tail -50 "$datadir/mysql.err" fi return 1 fi print_warning "Instance started but socket not detected. Check error log:" echo " tail -50 $datadir/mysql.err" return 1 } # Stop second MySQL instance stop_second_instance() { local datadir="$1" if [ -S "$datadir/socket.mysql" ]; then print_info "Shutting down second MySQL instance..." mysqladmin -h localhost -S "$datadir/socket.mysql" shutdown 2>/dev/null || true sleep 2 print_success "Second instance shut down" # Mark as no longer running SECOND_INSTANCE_RUNNING=0 fi } # Validate SQL dump integrity validate_sql_dump() { local sql_file="$1" local dbname="$2" local datadir="$3" print_info "Validating SQL dump integrity..." local validation_errors=0 # Check 1: File exists and is not empty if [ ! -f "$sql_file" ]; then print_error "SQL dump file not found: $sql_file" return 1 fi local file_size=$(stat -c%s "$sql_file" 2>/dev/null || stat -f%z "$sql_file" 2>/dev/null) if [ -z "$file_size" ] || [ "$file_size" -lt 100 ]; then print_error "SQL dump file is too small ($file_size bytes) - likely incomplete" return 1 fi print_success " File size: $(du -h "$sql_file" | awk '{print $1}')" # Check 2: Completion marker if grep -q "Dump completed on" "$sql_file"; then local completion_date=$(grep "Dump completed on" "$sql_file" | tail -1) print_success " Dump completion marker found: ${completion_date:0:60}..." else print_error " Missing 'Dump completed' marker - dump may be incomplete" validation_errors=$((validation_errors + 1)) fi # Check 3: Database name in dump if grep -q "^-- Database: \`$dbname\`" "$sql_file" || grep -q "^USE \`$dbname\`" "$sql_file"; then print_success " Database name '$dbname' found in dump" else print_warning " Database name '$dbname' not explicitly found in dump (may be OK)" fi # Check 4: Count CREATE TABLE statements local table_count=$(grep -c "^CREATE TABLE" "$sql_file" || echo "0") if [ "$table_count" -gt 0 ]; then print_success " Found $table_count CREATE TABLE statements" else print_warning " No CREATE TABLE statements found - database may be empty or dump incomplete" fi # Check 5: Count INSERT statements (data) local insert_count=$(grep -c "^INSERT INTO" "$sql_file" || echo "0") if [ "$insert_count" -gt 0 ]; then print_success " Found $insert_count INSERT INTO statements" else print_warning " No INSERT statements found - database may be empty or tables have no data" fi # Check 6: SQL syntax spot check (no unclosed quotes in first 100 lines) local syntax_errors=$(head -100 "$sql_file" | grep -E "^\s*['\"].*[^'\"];?\s*$" | wc -l || echo "0") if [ "$syntax_errors" -eq 0 ]; then print_success " SQL syntax spot check passed" else print_warning " Potential SQL syntax issues detected (may be false positive)" fi # Check 7: Compare with source database (if second instance still running) if [ -S "$datadir/socket.mysql" ]; then print_info " Comparing dump with source database..." # Get table count from source local source_tables=$(mysql -h localhost -S "$datadir/socket.mysql" -NBe "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA='$dbname';" 2>/dev/null || echo "0") if [ -n "$source_tables" ] && [ "$source_tables" -gt 0 ]; then if [ "$table_count" -eq "$source_tables" ]; then print_success " Table count matches: $table_count tables" else print_error " Table count mismatch: Dump has $table_count, source has $source_tables" validation_errors=$((validation_errors + 1)) fi # Get approximate data size from source local source_size=$(mysql -h localhost -S "$datadir/socket.mysql" -NBe "SELECT ROUND(SUM(data_length + index_length)/1024/1024, 2) FROM information_schema.TABLES WHERE TABLE_SCHEMA='$dbname';" 2>/dev/null || echo "0") local dump_size_mb=$(awk "BEGIN {printf \"%.2f\", $file_size/1024/1024}") if [ -n "$source_size" ]; then print_info " Source database size: ${source_size}MB (data+indexes)" print_info " Dump file size: ${dump_size_mb}MB (uncompressed SQL)" # Dump is usually 1-3x the data size (reasonable range) local size_ratio=$(awk "BEGIN {if ($source_size > 0) printf \"%.1f\", $dump_size_mb/$source_size; else print 0}") if [ -n "$size_ratio" ]; then print_info " Size ratio: ${size_ratio}x (1-3x is normal for text SQL)" fi fi fi fi if [ -n "$validation_errors" ] && [ "$validation_errors" -gt 0 ]; then echo "" print_error "Validation completed with $validation_errors errors" return 1 fi echo "" print_success "SQL dump validation PASSED - dump appears clean and complete" return 0 } # Dump database from second instance dump_database() { local datadir="$1" local dbname="$2" local output_file="$3" print_info "Creating SQL dump of database: $dbname" print_warning "This may take some time for large databases..." # Check if database exists in second instance local db_check=$(mysql -h localhost -S "$datadir/socket.mysql" -NBe "SHOW DATABASES LIKE '$dbname';" 2>/dev/null) if [ -z "$db_check" ]; then print_error "Database '$dbname' not found in second instance" return 1 fi # Get table count before dump local table_count=$(mysql -h localhost -S "$datadir/socket.mysql" -NBe "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA='$dbname';" 2>/dev/null || echo "0") print_info "Database contains $table_count tables" # Perform dump echo "" if mysqldump -h localhost -S "$datadir/socket.mysql" --single-transaction "$dbname" > "$output_file" 2>/dev/null; then # Verify dump completed if grep -q "Dump completed on" "$output_file"; then local size=$(du -h "$output_file" | awk '{print $1}') print_success "Dump created: $output_file ($size)" # Validate the dump echo "" if ! validate_sql_dump "$output_file" "$dbname" "$datadir"; then print_warning "Dump created but validation found issues" echo "" echo -n "Continue anyway? (y/n): " read -r continue_choice if [ "$continue_choice" != "y" ]; then return 1 fi fi return 0 else print_error "Dump appears incomplete (missing completion marker)" return 1 fi else print_error "mysqldump failed" return 1 fi } ################################################################################ # INTERACTIVE WORKFLOW ################################################################################ show_intro() { clear print_banner "MySQL/MariaDB File-Based Restore" # Detect MySQL version first detect_mysql_version echo "" echo "This tool helps restore MySQL/MariaDB databases from file-based backups" echo "(such as Acronis) when InnoDB tables are involved." echo "" if [ "$MYSQL_VARIANT" != "unknown" ]; then echo "Detected Database: $MYSQL_VARIANT $MYSQL_VERSION" echo "" fi echo "Process Overview:" echo " 1. Detect live MySQL data directory (read-only check)" echo " 2. Validate restored data files" echo " 3. Start SECOND MySQL instance using restored files" echo " 4. Create SQL dump from second instance" echo " 5. Shutdown second instance and output .sql file" echo "" print_success "SAFETY GUARANTEES:" echo " ✓ Uses SEPARATE MySQL instance (isolated socket/pid/datadir)" echo " ✓ Your LIVE MySQL is NEVER touched, stopped, or modified" echo " ✓ Second instance uses --skip-networking (no port 3306 conflicts)" echo " ✓ Automatic shutdown of second instance on completion or failure" echo " ✓ Second instance only reads restored files, never touches live data" echo "" print_warning "PREREQUISITES:" echo " - Restored MySQL data files must already be on this server" # Version-specific file requirements (2025 updated) if [ "$MYSQL_VARIANT" = "mysql" ]; then local major_ver=$(echo "$MYSQL_VERSION" | cut -d'.' -f1) local patch_ver=$(echo "$MYSQL_VERSION" | cut -d'.' -f3) if [ -n "$major_ver" ] && [ "$major_ver" -ge 9 ]; then echo " - Files: ibdata1, #innodb_redo/, #innodb_temp/ (opt), mysql/, sys/ (opt), target DB/" elif [ -n "$major_ver" ] && [ "$major_ver" -eq 8 ] && [ -n "$patch_ver" ] && [ "$patch_ver" -ge 30 ]; then echo " - Files: ibdata1, #innodb_redo/, #innodb_temp/ (opt), mysql/, sys/ (opt), target DB/" elif [ -n "$major_ver" ] && [ "$major_ver" -ge 8 ]; then echo " - Files: ibdata1, ib_logfile0/1, #innodb_temp/ (opt), mysql/, sys/ (opt), target DB/" else echo " - Files: ibdata1, ib_logfile0/1, mysql/, sys/ (opt), target DB/" fi else echo " - Files: ibdata1, ib_logfile0/1, mysql/, sys/ (opt), target DB/" fi echo " - Files must be owned by mysql:mysql" echo " - Sufficient disk space for SQL dumps" echo "" } step1_detect_datadir() { print_banner "Step 1: Detect Live MySQL Data Directory" detect_mysql_datadir echo "" echo "Live MySQL Data Directory: $LIVE_DATADIR" echo "" echo -n "Is this correct? (y/n, or 0 to cancel): " read -r confirm if [ "$confirm" = "0" ]; then echo "Operation cancelled." press_enter exit 0 fi if [ "$confirm" != "y" ]; then echo "" echo -n "Enter MySQL data directory path (or 0 to cancel): " read -r custom_dir if [ -z "$custom_dir" ] || [ "$custom_dir" = "0" ]; then echo "Operation cancelled." press_enter exit 0 fi if [ ! -d "$custom_dir" ]; then print_error "Directory does not exist: $custom_dir" press_enter return 1 fi LIVE_DATADIR="$custom_dir" print_success "Updated data directory: $LIVE_DATADIR" fi echo "" press_enter } step2_set_restore_location() { print_banner "Step 2: Set Restored Data Location" echo "Let's set up the restore directory." echo "" # Use control panel-specific home base, fallback to /home local home_base="${SYS_USER_HOME_BASE:-/home}" # Offer to create a timestamped directory local suggested_dir="${home_base}/temp/restore$(date +%Y%m%d)/mysql" echo "Suggested directory: $suggested_dir" echo "" echo " 1) Use suggested directory (will create if needed)" echo " 2) Enter custom path" echo " 0) Cancel" echo "" echo -n "Select option: " read -r dir_choice case $dir_choice in 0) echo "Operation cancelled." press_enter exit 0 ;; 1) TEMP_DATADIR="$suggested_dir" ;; 2) echo "" echo -n "Enter path to restored data directory (or 0 to cancel): " read -r restore_path if [ -z "$restore_path" ] || [ "$restore_path" = "0" ]; then echo "Operation cancelled." press_enter exit 0 fi TEMP_DATADIR="$restore_path" ;; *) print_error "Invalid option" press_enter return 1 ;; esac # Create directory if it doesn't exist if [ ! -d "$TEMP_DATADIR" ]; then echo "" print_info "Creating directory: $TEMP_DATADIR" if mkdir -p "$TEMP_DATADIR"; then chown mysql:mysql "$TEMP_DATADIR" chmod 751 "$TEMP_DATADIR" # Also ensure parent temp directory has correct permissions local parent_temp="${home_base}/temp" if [ -d "$parent_temp" ]; then chmod 751 "$parent_temp" 2>/dev/null || true fi print_success "Directory created with mysql:mysql ownership" else print_error "Failed to create directory" press_enter return 1 fi fi # Show required files list echo "" print_banner "Required Files to Restore" echo "" echo "You need to restore the following files from your backup to:" echo " $TEMP_DATADIR" echo "" print_warning "REQUIRED FILES:" echo "" echo "1. InnoDB System Tablespace:" echo " 📁 $TEMP_DATADIR/ibdata1" echo "" # Version-specific redo log files (2025 updated) local major_version=$(echo "$MYSQL_VERSION" | cut -d'.' -f1) local minor_version=$(echo "$MYSQL_VERSION" | cut -d'.' -f2) local patch_version=$(echo "$MYSQL_VERSION" | cut -d'.' -f3) if [ "$MYSQL_VARIANT" = "mysql" ] && [ -n "$major_version" ] && [ "$major_version" -ge 8 ]; then # Detect if MySQL 8.0.30+ or MySQL 9.0+ local use_new_redo=0 if [ "$major_version" -ge 9 ]; then use_new_redo=1 elif [ "$major_version" -eq 8 ] && [ -n "$patch_version" ] && [ "$patch_version" -ge 30 ]; then use_new_redo=1 fi if [ "$use_new_redo" -eq 1 ]; then echo "2. InnoDB Redo Logs (MySQL 8.0.30+/9.0+):" echo " 📁 $TEMP_DATADIR/#innodb_redo/ (entire directory)" echo " Contains: #ib_redo0, #ib_redo1, ... #ib_redoN files" else echo "2. InnoDB Redo Logs (MySQL 8.0.0-8.0.29):" echo " 📁 $TEMP_DATADIR/ib_logfile0" echo " 📁 $TEMP_DATADIR/ib_logfile1" fi else echo "2. InnoDB Redo Logs (MySQL 5.7/MariaDB):" echo " 📁 $TEMP_DATADIR/ib_logfile0" echo " 📁 $TEMP_DATADIR/ib_logfile1" fi echo "" echo "3. InnoDB Temporary Tablespace (if exists):" echo " 📁 $TEMP_DATADIR/#innodb_temp/ (optional, contains temp_N.ibt files)" echo " 📁 $TEMP_DATADIR/ibtmp1 (optional, global temp tablespace)" echo "" echo "4. MySQL System Database:" echo " 📁 $TEMP_DATADIR/mysql/ (entire directory)" echo " OR" echo " 📁 $TEMP_DATADIR/mysql.ibd (single file, if using)" echo "" echo "5. Optional: System Schema (if exists in backup):" echo " 📁 $TEMP_DATADIR/sys/ (entire directory - recommended)" echo "" echo "6. Your Target Database(s):" echo " 📁 $TEMP_DATADIR// (entire directory)" echo " Example: $TEMP_DATADIR/myuser_wordpress/" echo "" print_info "NOTE: performance_schema is NOT needed (recreated automatically)" echo "" print_info "TIP: Use Acronis, rsync, or cp to restore these files" echo "" echo -n "Have you finished restoring all required files? (y/n, or 0 to cancel): " read -r files_ready if [ "$files_ready" = "0" ]; then echo "Operation cancelled." press_enter exit 0 fi if [ "$files_ready" != "y" ]; then echo "" print_warning "Please restore the files listed above, then re-run this script." press_enter return 1 fi # Validate structure echo "" if ! validate_restore_structure "$TEMP_DATADIR"; then echo "" print_error "Data structure validation failed" echo "" print_info "Required files:" echo " - ibdata1 (InnoDB system tablespace)" echo " - ib_logfile0 and ib_logfile1 (MySQL 5.7/MariaDB)" echo " OR #innodb_redo/ directory (MySQL 8.0+)" echo " - mysql/ directory (system database)" echo " - / directory (your target database)" echo "" press_enter return 1 fi # Check ownership echo "" print_info "Checking file ownership..." local owner=$(stat -c '%U:%G' "$TEMP_DATADIR/ibdata1" 2>/dev/null || echo "unknown") if [ "$owner" != "mysql:mysql" ]; then print_warning "Files are not owned by mysql:mysql (current: $owner)" echo "" echo -n "Fix ownership now? (y/n, or 0 to cancel): " read -r fix_ownership if [ "$fix_ownership" = "0" ]; then echo "Operation cancelled." press_enter exit 0 fi if [ "$fix_ownership" = "y" ]; then print_info "Running: chown -R mysql:mysql $TEMP_DATADIR" chown -R mysql:mysql "$TEMP_DATADIR" print_success "Ownership updated" fi else print_success "File ownership is correct (mysql:mysql)" fi echo "" press_enter } step3_select_database() { print_banner "Step 3: Select Database to Restore" echo "Available databases in restored data:" echo "" # List directories (exclude system databases and special files) local databases=() while IFS= read -r dir; do local dbname=$(basename "$dir") # Skip system databases and special directories if [[ "$dbname" != "mysql" ]] && [[ "$dbname" != "sys" ]] && \ [[ "$dbname" != "performance_schema" ]] && [[ "$dbname" != "information_schema" ]] && \ [[ "$dbname" != "#"* ]]; then databases+=("$dbname") fi done < <(find "$TEMP_DATADIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null) if [ ${#databases[@]} -eq 0 ]; then print_error "No user databases found in $TEMP_DATADIR" press_enter return 1 fi local i=1 for db in "${databases[@]}"; do echo " $i) $db" i=$((i + 1)) done echo "" echo " 0) Cancel" echo "" echo -n "Select database number (or enter name manually): " read -r selection if [ "$selection" = "0" ]; then echo "Operation cancelled." press_enter exit 0 fi # Check if numeric selection if [[ "$selection" =~ ^[0-9]+$ ]] && [ "$selection" -ge 1 ] && [ "$selection" -le "${#databases[@]}" ]; then DATABASE_NAME="${databases[$((selection - 1))]}" else # Manual entry DATABASE_NAME="$selection" fi # Validate database exists if [ ! -d "$TEMP_DATADIR/$DATABASE_NAME" ]; then print_error "Database directory not found: $TEMP_DATADIR/$DATABASE_NAME" press_enter return 1 fi print_success "Selected database: $DATABASE_NAME" echo "" press_enter } step4_configure_options() { print_banner "Step 4: Configure Restore Options" echo "Database: $DATABASE_NAME" echo "Data Directory: $TEMP_DATADIR" echo "" echo "Optional Settings:" echo "" # Ticket number (optional) echo -n "Ticket number (optional, press Enter to skip): " read -r ticket if [ -n "$ticket" ]; then TICKET_NUMBER="$ticket" fi # Force recovery mode echo "" echo "InnoDB Force Recovery Mode:" echo " 0) No force recovery (default)" echo " 1) Ignore corrupt pages" echo " 2) Prevent background operations" echo " 3) Prevent transaction rollbacks" echo " 4) Prevent insert buffer merge" echo " 5) Skip log redo" echo " 6) Skip page checksums" echo "" echo -n "Select recovery mode (0-6, or press Enter for 0): " read -r recovery_mode if [ -n "$recovery_mode" ] && [ "$recovery_mode" != "0" ]; then FORCE_RECOVERY="$recovery_mode" print_warning "Will use --innodb-force-recovery=$FORCE_RECOVERY" echo "" # Show force recovery warnings and get confirmation if ! warn_force_recovery "$FORCE_RECOVERY"; then echo "" print_info "Recovery mode cancelled. Returning to default (level 0)." FORCE_RECOVERY="" fi fi echo "" press_enter } step5_create_dump() { print_banner "Step 5: Create SQL Dump" echo "Summary:" echo " Database: $DATABASE_NAME" echo " Data Directory: $TEMP_DATADIR" if [ -n "$TICKET_NUMBER" ]; then echo " Ticket: $TICKET_NUMBER" fi if [ -n "$FORCE_RECOVERY" ]; then echo " Force Recovery: Level $FORCE_RECOVERY" fi echo "" echo "This will:" echo " 1. Start a second MySQL instance using the restored data" echo " 2. Create an SQL dump of the database" echo " 3. Save the dump to the current directory" echo "" print_warning "The second MySQL instance will run on a separate socket." print_warning "Your live MySQL instance will NOT be affected." echo "" echo -n "Proceed with dump creation? (y/n, or 0 to cancel): " read -r confirm if [ "$confirm" = "0" ] || [ "$confirm" != "y" ]; then echo "Operation cancelled." press_enter exit 0 fi echo "" echo "════════════════════════════════════════════════════════════════" echo "STARTING RESTORE PROCESS" echo "════════════════════════════════════════════════════════════════" echo "" # Check disk space before proceeding print_info "Checking available disk space..." if ! check_disk_space "$(pwd)" 500; then press_enter return 1 fi echo "" # Start second instance if ! start_second_instance "$TEMP_DATADIR" "$FORCE_RECOVERY"; then print_error "Failed to start second MySQL instance" echo "" # Provide intelligent recovery guidance show_recovery_options "$TEMP_DATADIR" "$FORCE_RECOVERY" press_enter return 1 fi echo "" # Generate output filename - save to parent directory of TEMP_DATADIR # e.g., if TEMP_DATADIR is /home/temp/restore20251210/mysql # then output goes to /home/temp/restore20251210/ local timestamp=$(date +%Y%m%d_%H%M%S) local output_dir="$(dirname "$TEMP_DATADIR")" local output_file="${output_dir}/${DATABASE_NAME}_restored_${timestamp}.sql" if [ -n "$TICKET_NUMBER" ]; then output_file="${output_dir}/${DATABASE_NAME}_ticket${TICKET_NUMBER}_${timestamp}.sql" fi print_info "SQL dump will be saved to: $output_file" echo "" # Create dump if ! dump_database "$TEMP_DATADIR" "$DATABASE_NAME" "$output_file"; then print_error "Failed to create dump" stop_second_instance "$TEMP_DATADIR" press_enter return 1 fi echo "" # Stop second instance stop_second_instance "$TEMP_DATADIR" echo "" echo "════════════════════════════════════════════════════════════════" print_success "RESTORE COMPLETE!" echo "════════════════════════════════════════════════════════════════" echo "" echo "SQL Dump Created: $output_file" echo "" echo "Next Steps:" echo " 1. Verify dump integrity:" echo " grep 'Dump completed on' '$output_file'" echo "" echo " 2. Import to live database:" echo " mysql $DATABASE_NAME < '$output_file'" echo "" echo " 3. Or create fresh database first:" echo " mysql -e 'DROP DATABASE IF EXISTS $DATABASE_NAME;'" echo " mysql -e 'CREATE DATABASE $DATABASE_NAME;'" echo " mysql $DATABASE_NAME < '$output_file'" echo "" press_enter } ################################################################################ # MAIN EXECUTION ################################################################################ main() { show_intro echo -n "Continue? (y/n, or 0 to cancel): " read -r start if [ "$start" = "0" ] || [ "$start" != "y" ]; then echo "Operation cancelled." press_enter exit 0 fi # Step 1: Detect live data directory while ! step1_detect_datadir; do echo "" echo -n "Retry? (y/n): " read -r retry if [ "$retry" != "y" ]; then exit 0 fi done # Step 2: Set restore location while ! step2_set_restore_location; do echo "" echo -n "Retry? (y/n): " read -r retry if [ "$retry" != "y" ]; then exit 0 fi done # Step 3: Select database while ! step3_select_database; do echo "" echo -n "Retry? (y/n): " read -r retry if [ "$retry" != "y" ]; then exit 0 fi done # Step 4: Configure options step4_configure_options # Step 5: Create dump step5_create_dump } # Run main function main