Sindbad~EG File Manager

Current Path : /var/www/html/sumar.com.py/wp-content/plugins/migrator-plugin-0.1.0/includes/
Upload File :
Current File : /var/www/html/sumar.com.py/wp-content/plugins/migrator-plugin-0.1.0/includes/class-exporter.php

<?php
/**
 * The class responsible for exporting the website.
 *
 * @package CustomMigrator
 */

/**
 * Exporter class for WordPress website migration with binary archive format.
 */
class Custom_Migrator_Exporter {

    /**
     * The filesystem handler.
     *
     * @var Custom_Migrator_Filesystem
     */
    private $filesystem;



    /**
     * The metadata handler.
     *
     * @var Custom_Migrator_Metadata
     */
    private $metadata;

    /**
     * The file extension for exported content.
     * 
     * @var string
     */
    private $file_extension = 'hstgr';

    /**
     * Paths to exclude from export.
     * 
     * @var array
     */
    private $exclusion_paths = [];



    /**
     * Batch processing constants - Optimized for responsiveness.
     */
    const BATCH_SIZE = 1000;            // Reduced from 10,000 to 1,000 for faster resume
    const MAX_EXECUTION_TIME = 10;      // 10 seconds like All-in-One WP Migration  
    const MEMORY_THRESHOLD = 0.9;       // More aggressive memory usage
    const CHUNK_SIZE = 512000;          // 512KB chunks like All-in-One

    /**
     * Initialize the class.
     */
    public function __construct() {
        $this->filesystem = new Custom_Migrator_Filesystem();
        // Note: Database export now uses unified Custom_Migrator_Database_Exporter
        $this->metadata = new Custom_Migrator_Metadata();

        // Define exclusion paths
        $this->set_exclusion_paths();
    }

    /**
     * Set paths to be excluded from export.
     */
    private function set_exclusion_paths() {
        // Use unified helper class for exclusion paths
        $this->exclusion_paths = Custom_Migrator_Helper::get_exclusion_paths();
    }

    /**
     * Run the export process with database export protection.
     */
    public function export() {
        $this->setup_execution_environment();
        
        $file_paths = $this->filesystem->get_export_file_paths();
        $hstgr_file = $file_paths['hstgr'];
        $meta_file = $file_paths['metadata'];
        $sql_file = $file_paths['sql'];

        $resume_info_file = $this->filesystem->get_export_dir() . '/export-resume-info.json';
        $is_resuming = file_exists($resume_info_file);

        if ($is_resuming) {
            $this->filesystem->write_status('resuming');
            $this->filesystem->log('Resuming export process');
        } else {
            $this->filesystem->write_status('exporting');
            $this->filesystem->log('Starting export process');
        }

        try {
            if (!$is_resuming) {
                $this->filesystem->log('Generating metadata...');
                
                // Use unified metadata generation with regular export configuration
                $metadata_options = array(
                    'file_format' => $this->file_extension,
                    'exporter_version' => CUSTOM_MIGRATOR_VERSION,
                    'export_type' => 'regular',
                    'export_method' => 'cron_based',
                );
                
                $result = $this->metadata->generate_and_save($meta_file, $metadata_options);
                if (!$result) {
                    throw new Exception('Metadata generation failed');
                }
            }

            // CRITICAL FIX: Only run database export on fresh start, never when resuming file export
            if (!$is_resuming) {
                // Fresh start: check if database export is needed
                if (!$this->is_database_export_complete($sql_file)) {
                    $this->safe_database_export($sql_file);
                } else {
                    $this->filesystem->log('Database export already complete, skipping');
                }
            } else {
                // Resuming: we're only resuming file export, never database export
                $this->filesystem->log('Resuming file export - database export already complete, skipping');
            }

            $this->filesystem->log('Exporting wp-content files...');
            $content_export_result = $this->export_wp_content_archive($hstgr_file);
            
            if ($content_export_result === 'paused') {
                return true;
            }
            
            $this->filesystem->log('wp-content files exported successfully');

            // CRITICAL: Validate all required files exist before marking export as done
            $missing_files = Custom_Migrator_Helper::validate_export_files($file_paths);
            if (!empty($missing_files)) {
                $error_msg = 'Export incomplete - missing files: ' . implode(', ', $missing_files);
                $this->filesystem->write_status('error: ' . $error_msg);
                $this->filesystem->log('❌ EXPORT VALIDATION FAILED: ' . $error_msg);
                throw new Exception($error_msg);
            }

            if (file_exists($resume_info_file)) {
                @unlink($resume_info_file);
            }

            $this->filesystem->write_status('done');
            $this->filesystem->log('Export completed successfully');
            
            return true;
        } catch (Exception $e) {
            $error_message = 'Export failed: ' . $e->getMessage();
            $this->filesystem->write_status('error: ' . $error_message);
            $this->filesystem->log($error_message);
            
            if (defined('WP_DEBUG') && WP_DEBUG) {
                $this->filesystem->log('Error trace: ' . $e->getTraceAsString());
            }
            
            return false;
        }
    }

    /**
     * Setup execution environment.
     */
    private function setup_execution_environment() {
        if (function_exists('set_time_limit') && !ini_get('safe_mode')) {
            @set_time_limit(0);
        }
        
        $current_limit = $this->get_memory_limit_bytes();
        $target_limit = max($current_limit, 1024 * 1024 * 1024);
        
        @ini_set('memory_limit', $this->format_bytes($target_limit));
        
        while (ob_get_level()) {
            ob_end_clean();
        }
        
        if (function_exists('gc_enable')) {
            gc_enable();
        }
        
        $this->filesystem->log('Execution environment setup: Memory limit = ' . ini_get('memory_limit'));
    }

    /**
     * Export WordPress content files using optimized single-file processing.
     * Process one file at a time with immediate timeout checks for better resource management.
     */
    private function export_wp_content_archive($hstgr_file) {
        $wp_content_dir = WP_CONTENT_DIR;
        $export_dir = $this->filesystem->get_export_dir();
        
        $content_list_file = $export_dir . '/content-list.csv';
        $resume_info_file = $export_dir . '/export-resume-info.json';
        
        // Load resume data with CSV offset tracking
        $resume_data = $this->load_resume_data($resume_info_file);
        $csv_offset = isset($resume_data['csv_offset']) ? $resume_data['csv_offset'] : 0;
        $archive_offset = isset($resume_data['archive_offset']) ? $resume_data['archive_offset'] : 0;
        $files_processed = isset($resume_data['files_processed']) ? $resume_data['files_processed'] : 0;
        $bytes_processed = isset($resume_data['bytes_processed']) ? $resume_data['bytes_processed'] : 0;
        
        $is_resuming = $csv_offset > 0 || $archive_offset > 0;
        
        // Calculate adaptive timeout based on restart patterns
        $timeout_info = $this->calculate_adaptive_timeout($resume_data);
        $adaptive_timeout = $timeout_info['timeout'];
        $cron_mode = $timeout_info['cron_mode'];
        
        // Log startup mode
        if ($is_resuming) {
            $this->filesystem->log("Resuming in {$timeout_info['mode_name']} - processed $files_processed files so far");
        }
        
        // Step 1: Enumerate files into CSV for efficient processing
        if (!$is_resuming && !file_exists($content_list_file)) {
            $this->filesystem->log('Phase 1: Enumerating files into CSV for optimized processing');
            $this->enumerate_content_files($content_list_file);
        }
        
        // Step 2: Process files from CSV (ONE FILE AT A TIME)
        $this->filesystem->log('Phase 2: Processing files one-by-one for optimal resource usage');
        
        // Open CSV file for reading
        $csv_handle = fopen($content_list_file, 'r');
        if (!$csv_handle) {
            throw new Exception('Cannot open content list file');
        }
        
        // Seek to CSV offset if resuming
        if ($csv_offset > 0) {
            fseek($csv_handle, $csv_offset);
        }
        
        // Open archive file
        $archive_mode = $archive_offset > 0 ? 'ab' : 'wb';
        $archive_handle = fopen($hstgr_file, $archive_mode);
        if (!$archive_handle) {
            fclose($csv_handle);
            throw new Exception('Cannot create or open archive file');
        }
        
        // CRITICAL FIX: Don't seek when using append mode!
        // Append mode ('ab') automatically positions at end of file
        // The fseek() was causing corruption by seeking to wrong position
        // if ($archive_offset > 0) {
        //     fseek($archive_handle, $archive_offset);
        // }
        
        // Start precise timing for optimal resource management
        $start = microtime(true);
        $completed = true;
        
        // Track files processed in this batch for accurate rate calculation
        $files_processed_this_batch = 0;
        
        // CRITICAL: Track failed files to prevent incomplete backups
        $files_failed = 0;
        $bytes_failed = 0;
        $total_files_attempted = 0;
        
        try {
            // Process files from CSV one at a time for hosting-friendly resource usage
            while (($file_data = fgetcsv($csv_handle, 0, ',', '"', '\\')) !== FALSE) {
                
                // Parse CSV data: [file_path, relative_path, size, mtime]
                if (count($file_data) < 4) continue;
                
                $file_path = $file_data[0];
                $relative_path = $file_data[1];
                $file_size = (int)$file_data[2];
                $file_mtime = (int)$file_data[3];
                
                // Skip if file no longer exists
                if (!file_exists($file_path) || !is_readable($file_path)) {
                    continue;
                }
                
                // Process the file (one at a time for optimal resource usage)
                $file_info = [
                    'path' => $file_path,
                    'relative' => $relative_path,
                    'size' => $file_size
                ];
                
                $total_files_attempted++;
                $result = $this->add_file_to_archive($archive_handle, $file_info);
                
                if ($result['success']) {
                    $files_processed++;
                    $files_processed_this_batch++;
                    $bytes_processed += $result['bytes'];
                } else {
                    // CRITICAL: Track failed files and their impact
                    $files_failed++;
                    $bytes_failed += $file_size;
                    
                    // FAIL FAST: If too many files are failing, stop the export immediately
                    if ($files_failed >= 100) {
                        $failure_rate = ($files_failed / $total_files_attempted) * 100;
                        $failed_size_mb = $bytes_failed / (1024 * 1024);
                        
                        $error_msg = sprintf(
                            'Export FAILED: %d files failed to archive (%.1f%% failure rate, %.2f MB lost). ' .
                            'Latest failure: %s. This backup would be incomplete and unreliable.',
                            $files_failed,
                            $failure_rate,
                            $failed_size_mb,
                            basename($file_path)
                        );
                        
                        $this->filesystem->write_status('error: ' . $error_msg);
                        $this->filesystem->log('❌ EXPORT TERMINATED DUE TO EXCESSIVE FAILURES: ' . $error_msg);
                        throw new Exception($error_msg);
                    }
                }
                
                // Progress logging every 1000 files
                if ($files_processed % 1000 === 0) {
                    $elapsed = microtime(true) - $start;
                    $batch_rate = $files_processed_this_batch / max($elapsed, 1);
                    $mode_desc = $cron_mode ? "Cron" : "HTTP";
                    $this->filesystem->log(sprintf(
                        "Processing: %d files (%.2f MB). Batch Rate: %.2f files/sec (%d files in %.1fs) [%s Mode - %ds timeout]",
                        $files_processed,
                        $bytes_processed / (1024 * 1024),
                        $batch_rate,
                        $files_processed_this_batch,
                        $elapsed,
                        $mode_desc,
                        $adaptive_timeout
                    ));
                }
                
                // LVE THROTTLING DETECTION: Check every 100 files for performance degradation
                if ($files_processed % 100 === 0) {
                    static $last_lve_check = null;
                    static $last_lve_files = 0;
                    
                    if ($last_lve_check !== null) {
                        $time_since_check = microtime(true) - $last_lve_check;
                        $files_since_check = $files_processed - $last_lve_files;
                        $recent_rate = $files_since_check / max($time_since_check, 1);
                        
                        // If processing rate drops below 5 files/sec, likely LVE throttling
                        if ($recent_rate < 5 && $files_since_check >= 100) {
                            $this->filesystem->log(sprintf(
                                "⚠️ LVE THROTTLING DETECTED: Rate=%.1f files/sec (last 100 files took %.1fs) - will pause early to avoid limits",
                                $recent_rate,
                                $time_since_check
                            ));
                            
                            // Force early timeout to avoid hitting harder LVE limits
                            $completed = false;
                            break;
                        }
                    }
                    
                    $last_lve_check = microtime(true);
                    $last_lve_files = $files_processed;
                }
                
                // Check adaptive timeout after each file
                if ((microtime(true) - $start) > $adaptive_timeout) {
                    $elapsed = microtime(true) - $start;
                    
                    $this->filesystem->log(sprintf(
                        "⏰ Batch complete: Processed %d files in %.1fs using %s (timeout=%ds)",
                        $files_processed_this_batch,
                        $elapsed,
                        $timeout_info['mode_name'],
                        $adaptive_timeout
                    ));
                    
                    // Save CSV and archive positions for precise resume
                    $current_csv_offset = ftell($csv_handle);
                    
                    // Archive offset is saved for logging but NOT used for seeking
                    // We use append mode ('ab') which automatically positions at end of file
                    $current_archive_offset = ftell($archive_handle);
                    
                    $this->save_resume_data($resume_info_file, [
                        'csv_offset' => $current_csv_offset,
                        'archive_offset' => $current_archive_offset,
                        'files_processed' => $files_processed,
                        'bytes_processed' => $bytes_processed,
                        'last_update' => time(),
                        'last_restart_time' => time(),
                        'restart_count' => (isset($resume_data['restart_count']) ? $resume_data['restart_count'] : 0) + 1,
                        'cron_mode' => $cron_mode
                    ]);
                    
                    $completed = false;
                    break; // Pause for optimal batch processing
                }
            }
            
        } catch (Exception $e) {
            fclose($csv_handle);
            fclose($archive_handle);
            throw new Exception('Single-file processing failed: ' . $e->getMessage());
        }
        
        fclose($csv_handle);
        fclose($archive_handle);
        
        // If not completed, schedule immediate resume and exit
        if (!$completed) {
            $this->filesystem->write_status('paused');
            $this->schedule_immediate_resume();
        } else {
            // Clean up files when completed
            if (file_exists($content_list_file)) {
                @unlink($content_list_file);
            }
            if (file_exists($resume_info_file)) {
                @unlink($resume_info_file);
            }

            // Final consistency check: ensure at least 99% of enumerated files were archived
            $total_files_enumerated = $this->count_lines_in_csv($content_list_file);
            $completion_rate = $total_files_enumerated > 0 ? ($files_processed / $total_files_enumerated) : 1;
            if ($total_files_enumerated > 0 && $completion_rate < 0.99) {
                $missing = $total_files_enumerated - $files_processed;
                $failed_size_mb = isset($bytes_failed) ? $bytes_failed / (1024 * 1024) : 0;
                $failed_count = isset($files_failed) ? $files_failed : 0;
                
                $error_msg = sprintf(
                    'Export incomplete: Only %d of %d files exported (%.1f%%). Missing %d files. ' .
                    'Failed files: %d (%.2f MB lost). This backup is unreliable.',
                    $files_processed,
                    $total_files_enumerated,
                    $completion_rate * 100,
                    $missing,
                    $failed_count,
                    $failed_size_mb
                );
                $this->filesystem->write_status('error: ' . $error_msg);
                $this->filesystem->log('❌ EXPORT VALIDATION FAILED: ' . $error_msg);
                throw new Exception($error_msg);
            } else {
                $total_time = microtime(true) - $start;
                $failed_info = '';
                if (isset($files_failed) && $files_failed > 0) {
                    $failed_size_mb = $bytes_failed / (1024 * 1024);
                    $failed_info = sprintf(' (Warning: %d files failed, %.2f MB lost)', $files_failed, $failed_size_mb);
                }
                
                $this->filesystem->log(sprintf(
                    'Single-file processing completed: %d files (%.2f MB) in %.2f seconds%s',
                    $files_processed,
                    $bytes_processed / (1024 * 1024),
                    $total_time,
                    $failed_info
                ));
                $this->filesystem->write_status('done');
                $this->filesystem->log('Export completed successfully');
            }
        }
        
        return $completed ? true : 'paused';
    }

    /**
     * Enumerate content files into CSV using unified enumerator.
     */
    private function enumerate_content_files($content_list_file) {
        // Use unified file enumerator with unlimited execution environment
        $enumerator = new Custom_Migrator_File_Enumerator($this->filesystem);
        
        $options = array(
            'progress_interval' => 1000,  // More frequent progress updates
            'use_exclusions' => true,
            'validate_files' => true,
            'skip_unreadable' => true,
            'log_errors' => true,
            'use_unlimited_execution' => true  // Enable unlimited execution time for enumeration
        );
        
        $stats = $enumerator->enumerate_to_csv($content_list_file, $options);
        
        // Log final results in format compatible with existing code
        $this->filesystem->log("File enumeration complete: {$stats['files_found']} files");
        
        return $stats;
    }

    /**
     * Add a file to the binary archive using the structured format.
     * UPDATED: Now uses All-in-One WP Migration's exact approach to prevent corruption.
     */
    private function add_file_to_archive($archive_handle, $file_info) {
        $file_path = $file_info['path'];
        $relative_path = $file_info['relative'];
        $file_size = $file_info['size'];
        
        // Enhanced file validation
        if (!file_exists($file_path)) {
            $this->filesystem->log("ERROR: File disappeared during processing: " . basename($file_path));
            return ['success' => false, 'bytes' => 0];
        }
        
        if (!is_readable($file_path)) {
            $this->filesystem->log("ERROR: File became unreadable: " . basename($file_path));
            return ['success' => false, 'bytes' => 0];
        }
        
        // Re-check file size (files can change during processing)
        $current_size = filesize($file_path);
        if ($current_size !== $file_size) {
            $this->filesystem->log("WARNING: File size changed during processing: " . basename($file_path) . " (was: $file_size, now: $current_size)");
            $file_size = $current_size;
            $file_info['size'] = $file_size; // Update for consistency
        }
        
        // Get file stats
        $stat = stat($file_path);
        if ($stat === false) {
            $this->filesystem->log("ERROR: Cannot get file stats: " . basename($file_path));
            return ['success' => false, 'bytes' => 0];
        }
        
        // CRITICAL FIX: Try to open the file FIRST before writing any headers
        $file_handle = fopen($file_path, 'rb');
        if (!$file_handle) {
            $this->filesystem->log("ERROR: Cannot open file for reading: " . basename($file_path));
            return ['success' => false, 'bytes' => 0];
        }
        
        // Prepare file info for binary block
        $file_name = basename($file_path);
        $file_date = $stat['mtime'];
        $file_dir = dirname($relative_path);
        
        // CRITICAL FIX: Ensure file_size and file_date are integers
        $file_size = (int)$file_size;
        $file_date = (int)$file_date;
        
        // Validate the data before creating binary block
        if ($file_size < 0) {
            $this->filesystem->log("ERROR: Invalid file size for " . basename($file_path) . ": $file_size");
            fclose($file_handle);
            return ['success' => false, 'bytes' => 0];
        }
        
        if ($file_date <= 0) {
            $this->filesystem->log("ERROR: Invalid modification time for " . basename($file_path) . ": $file_date");
            fclose($file_handle);
            return ['success' => false, 'bytes' => 0];
        }
        
        // Write full header with real file size immediately (All-in-One's approach)
        try {
            $block = Custom_Migrator_Helper::create_binary_block($file_name, $file_size, $file_date, $file_dir);
        } catch (Exception $e) {
            $this->filesystem->log("ERROR: " . $e->getMessage());
            fclose($file_handle);
            return ['success' => false, 'bytes' => 0];
        }
        
        $expected_size = Custom_Migrator_Helper::get_binary_block_size();
        
        // Write the full header block
        $header_written = fwrite($archive_handle, $block);
        if ($header_written === false || $header_written !== $expected_size) {
            $this->filesystem->log("ERROR: Failed to write header block for {$file_name}");
            fclose($file_handle);
            return ['success' => false, 'bytes' => 0];
        }
        
        // Copy file content
        $bytes_copied = 0;
        $chunk_size = 256 * 1024; // 256KB chunks
        
        while (!feof($file_handle)) {
            $chunk = fread($file_handle, $chunk_size);
            if ($chunk === false) {
                $this->filesystem->log("ERROR: Failed to read from file: " . basename($file_path));
                fclose($file_handle);
                return ['success' => false, 'bytes' => $bytes_copied];
            }
            
            $chunk_length = strlen($chunk);
            if ($chunk_length === 0) {
                break; // End of file
            }
            
            $written = fwrite($archive_handle, $chunk);
            if ($written === false || $written !== $chunk_length) {
                $this->filesystem->log("ERROR: Failed to write chunk for file: " . basename($file_path));
                fclose($file_handle);
                return ['success' => false, 'bytes' => $bytes_copied];
            }
            
            $bytes_copied += $written;
        }
        
        fclose($file_handle);
        
        // REMOVED: Size update logic (was causing potential corruption)
        
        // Final verification - ensure we copied the expected amount
        if ($bytes_copied !== $file_size) {
            $this->filesystem->log("WARNING: Bytes copied ($bytes_copied) doesn't match file size ($file_size) for: " . basename($file_path));
        }
        
        return ['success' => true, 'bytes' => $bytes_copied];
    }

    /**
     * Get PHP memory limit in bytes.
     */
    private function get_memory_limit() {
        $memory_limit = ini_get('memory_limit');
        
        if ($memory_limit == -1) {
            return PHP_INT_MAX; // Unlimited
        }
        
        $value = (int) $memory_limit;
        $unit = strtolower(substr($memory_limit, -1));
        
        switch ($unit) {
            case 'g':
                $value *= 1024 * 1024 * 1024;
                break;
            case 'm':
                $value *= 1024 * 1024;
                break;
            case 'k':
                $value *= 1024;
                break;
        }
        
        return $value;
    }

    /**
     * Format bytes for display
     */
    private function format_bytes($bytes) {
        if ($bytes >= 1073741824) {
            return round($bytes / 1073741824, 2) . ' GB';
        } elseif ($bytes >= 1048576) {
            return round($bytes / 1048576, 2) . ' MB';
        } elseif ($bytes >= 1024) {
            return round($bytes / 1024, 2) . ' KB';
        } else {
            return $bytes . ' bytes';
        }
    }

    /**
     * Load resume data with CSV offset tracking.
     */
    private function load_resume_data($resume_info_file) {
        if (!file_exists($resume_info_file)) {
            return [
                'csv_offset' => 0,
                'archive_offset' => 0,
                'files_processed' => 0,
                'bytes_processed' => 0
            ];
        }
        
        $data = json_decode(file_get_contents($resume_info_file), true);
        return $data ?: [
            'csv_offset' => 0,
            'archive_offset' => 0,
            'files_processed' => 0,
            'bytes_processed' => 0
        ];
    }

    /**
     * Save resume data.
     */
    private function save_resume_data($resume_info_file, $data) {
        file_put_contents($resume_info_file, json_encode($data));
    }

    /**
     * Calculate adaptive timeout based on restart patterns (Ultra-Conservative Timeout Structure).
     * Implements the timeout structure from your screenshot.
     */
    private function calculate_adaptive_timeout($resume_data) {
        $current_time = time();
        $last_restart = isset($resume_data['last_restart_time']) ? $resume_data['last_restart_time'] : $current_time;
        $restart_count = isset($resume_data['restart_count']) ? $resume_data['restart_count'] : 0;
        
        // Calculate time since last restart
        $restart_gap = $current_time - $last_restart;
        
        // Detect cron mode: if restart took >30 seconds, we're likely in cron fallback
        $is_cron_mode = $restart_gap > 30 || $resume_data['cron_mode'];
        
        if ($is_cron_mode) {
            // ULTRA-CONSERVATIVE mode: Maximum shared hosting compatibility
            if ($restart_gap > 300) {
                // Very slow restarts (5+ minutes) = ultra-conservative
                $timeout = 25; // 2.5x longer than normal (ultra-safe)
                $mode_name = "Ultra-Safe Maximum";
            } elseif ($restart_gap > 120) {
                // Slow restarts (2+ minutes) = conservative
                $timeout = 20; // 2x longer than normal (safe)
                $mode_name = "Ultra-Safe High";
            } else {
                // Medium slow restarts (30s-2min) = moderate
                $timeout = 15; // 1.5x longer than normal (moderate)
                $mode_name = "Ultra-Safe Moderate";
            }
        } else {
            // HTTP mode: stay nimble for fast restarts  
            $timeout = 10; // Standard timeout
            $mode_name = "HTTP Mode (Standard)";
        }
        
        // Enhanced logging for batch size detection
        if ($restart_count > 0) { // Don't log on first run
            $this->filesystem->log(sprintf(
                "🔄 Restart #%d detected: Gap=%ds (%.1fm), Mode=%s, Batch-Timeout=%ds",
                $restart_count,
                $restart_gap,
                $restart_gap / 60,
                $mode_name,
                $timeout
            ));
            
            if ($is_cron_mode) {
                $this->filesystem->log(sprintf(
                    "⚡ Cron mode: %ds gap → Batch size %dx larger (%ds vs 10s standard)",
                    $restart_gap,
                    $timeout / 10,
                    $timeout
                ));
            }
        }
        
        return [
            'timeout' => $timeout,
            'cron_mode' => $is_cron_mode,
            'mode_name' => $mode_name
        ];
    }

    /**
     * Get memory limit in bytes.
     */
    private function get_memory_limit_bytes() {
        $limit = ini_get('memory_limit');
        if ($limit == -1) {
            return PHP_INT_MAX;
        }
        
        $unit = strtolower(substr($limit, -1));
        $value = (int) $limit;
        
        switch ($unit) {
            case 'g': $value *= 1024;
            case 'm': $value *= 1024;
            case 'k': $value *= 1024;
        }
        
        return $value;
    }

    /**
     * Prepare export environment (step 1 of step-by-step export).
     * 
     * @return bool Success status.
     */
    public function prepare_export() {
        $this->filesystem->log('Preparing export environment');
        
        // Set exclusion paths
        $this->set_exclusion_paths();
        
        // Setup execution environment
        $this->setup_execution_environment();
        
        // Clean any previous resume info
        $resume_info_file = $this->filesystem->get_export_dir() . '/export-resume-info.json';
        if (file_exists($resume_info_file)) {
            @unlink($resume_info_file);
        }
        
        $this->filesystem->log('Export environment prepared');
        return true;
    }

    /**
     * Export only wp-content files (step 2 of step-by-step export).
     * 
     * @return mixed Success status (true), paused status ('paused'), or false on error.
     */
    public function export_content_only() {
        $this->filesystem->log('Starting wp-content export step');
        
        $file_paths = $this->filesystem->get_export_file_paths();
        $hstgr_file = $file_paths['hstgr'];
        
        // Check if .hstgr file already exists and has content
        if (file_exists($hstgr_file) && filesize($hstgr_file) > 0) {
            $file_size = $this->format_bytes(filesize($hstgr_file));
            $this->filesystem->log("wp-content file already exists ($file_size), skipping content export");
            return true;
        }
        
        $content_export_result = $this->export_wp_content_archive($hstgr_file);
        
        if ($content_export_result === 'paused') {
            $this->filesystem->log('wp-content export paused, will continue in next step');
            return 'paused'; // Return paused status instead of false
        }
        
        $this->filesystem->log('wp-content export completed');
        return true;
    }

    /**
     * Export only database (step 3 of step-by-step export) with protection.
     * 
     * @return bool Success status.
     */
    public function export_database_only() {
        $this->filesystem->log('Starting database export step');
        
        $file_paths = $this->filesystem->get_export_file_paths();
        $sql_file = $file_paths['sql'];
        
        // Use the same safe database export mechanism
        if (!$this->is_database_export_complete($sql_file)) {
            $this->safe_database_export($sql_file);
        } else {
            $this->filesystem->log('Database export already complete, skipping');
        }
        
        return true;
    }

    /**
     * Generate only metadata (step 4 of step-by-step export).
     * 
     * @return bool Success status.
     */
    public function generate_metadata_only() {
        $this->filesystem->log('Starting metadata generation step');
        
        $file_paths = $this->filesystem->get_export_file_paths();
        $meta_file = $file_paths['metadata'];
        
        // Use unified metadata generation with regular export configuration
        $metadata_options = array(
            'file_format' => $this->file_extension,
            'exporter_version' => CUSTOM_MIGRATOR_VERSION,
            'export_type' => 'regular',
            'export_method' => 'step_by_step',
        );
        
        return $this->metadata->generate_and_save($meta_file, $metadata_options);
    }

    /**
     * Schedule immediate resume (optimized background processing).
     */
    private function schedule_immediate_resume() {
        $ajax_url = admin_url('admin-ajax.php');
        $request_params = array(
            'action' => 'cm_run_export_now',
            'background_mode' => '1'
        );
        
        // Method 1: Non-blocking request (don't wait for response)
        wp_remote_post($ajax_url, array(
            'method'    => 'POST',
            'timeout'   => 0.01,  // Very short timeout
            'blocking'  => false, // Non-blocking so we can exit immediately
            'sslverify' => false,
            'headers'   => array('Connection' => 'close'),
            'body'      => $request_params,
        ));
        
        // Method 2: Additional non-blocking request as backup
        wp_remote_post($ajax_url, array(
            'method'    => 'POST',
            'timeout'   => 10,
            'blocking'  => false,
            'sslverify' => false,
            'body'      => $request_params,
        ));
        
        $this->filesystem->log('Sent immediate resume requests - exiting to allow new request');
        
        // Optimized pattern: Exit immediately after sending HTTP request
        // This ensures the current request ends and the new request can start immediately
        exit();
    }

    /**
     * Trigger cron execution.
     */
    private function trigger_cron_execution() {
        // Method 1: Standard WordPress spawn_cron
        if (function_exists('spawn_cron')) {
            spawn_cron();
        }
        
        // Method 2: Direct HTTP request to wp-cron.php
        $cron_url = site_url('wp-cron.php');
        $this->non_blocking_request($cron_url . '?doing_wp_cron=1');
    }

    /**
     * Make a non-blocking HTTP request.
     */
    private function non_blocking_request($url) {
        $args = array(
            'timeout'   => 5,
            'blocking'  => false,
            'sslverify' => apply_filters('https_local_ssl_verify', false),
            'user-agent' => 'WordPress/' . get_bloginfo('version') . '; ' . home_url(),
            'redirection' => 0
        );
        
        wp_remote_get($url, $args);
    }

    /**
     * Check if database export is complete and valid.
     */
    private function is_database_export_complete($sql_file) {
        // Only check the EXACT current database file, not old ones from previous exports
        
        // CRITICAL FIX: Handle both .sql and .sql.gz extensions properly
        
        // If sql_file already has .gz extension, check for that file directly
        if (substr($sql_file, -3) === '.gz') {
            $compressed_file = $sql_file;
            $uncompressed_file = substr($sql_file, 0, -3); // Remove .gz
        } else {
            $compressed_file = $sql_file . '.gz';
            $uncompressed_file = $sql_file;
        }
        
        // Method 1: Check if the exact compressed file exists and has content
        if (file_exists($compressed_file) && filesize($compressed_file) > 1024) {
            $this->filesystem->log('Current database export found: ' . basename($compressed_file) . ' (' . $this->format_bytes(filesize($compressed_file)) . ')');
            return true;
        }
        
        // Method 2: Check if the exact uncompressed file exists and is complete
        if (file_exists($uncompressed_file) && filesize($uncompressed_file) > 1024) {
            // Read last 1024 bytes to check for completion marker
            $handle = fopen($uncompressed_file, 'r');
            if ($handle) {
                fseek($handle, -1024, SEEK_END);
                $tail = fread($handle, 1024);
                fclose($handle);
                
                // Look for SQL completion markers
                if (strpos($tail, '-- Export completed') !== false || 
                    strpos($tail, 'COMMIT;') !== false ||
                    strpos($tail, '/*!40101 SET') !== false) {
                    $this->filesystem->log('Current database export found: ' . basename($uncompressed_file) . ' (' . $this->format_bytes(filesize($uncompressed_file)) . ')');
                    return true;
                }
            }
        }
        
        $this->filesystem->log('No current database export found - need to create: ' . basename($sql_file));
        return false;
    }

    /**
     * Safely export database with lock mechanism to prevent parallel exports.
     */
    private function safe_database_export($sql_file) {
        $export_dir = $this->filesystem->get_export_dir();
        $lock_file = $export_dir . '/database-export.lock';
        $status_file = $export_dir . '/database-export-status.json';
        
        // Check if another process is already exporting database
        if (file_exists($lock_file)) {
            $lock_content = file_get_contents($lock_file);
            $lock_data = json_decode($lock_content, true);
            $current_time = time();
            
            if ($lock_data) {
                $lock_time = $lock_data['timestamp'];
                $lock_pid = isset($lock_data['pid']) ? $lock_data['pid'] : null;
                $current_pid = function_exists('getmypid') ? getmypid() : null;
                
                // If this is the same process, continue (resume case)
                if ($lock_pid && $current_pid && $lock_pid == $current_pid) {
                    $this->filesystem->log('Database export lock belongs to current process, continuing');
                } 
                // PRODUCTION-SAFE: Very conservative lock timeouts for high-volume production (3000 sites/week)
                // If lock is older than 20 minutes, consider it definitely stale
                else if (($current_time - $lock_time) > 1200) {
                    $this->filesystem->log('Removing very stale database export lock (older than 20 minutes)');
                    @unlink($lock_file);
                    @unlink($status_file);
                }
                // If lock is 10-20 minutes old, check status more carefully
                else if (($current_time - $lock_time) > 600) {
                    if (file_exists($status_file)) {
                        $status = json_decode(file_get_contents($status_file), true);
                        $progress_time = isset($status['last_update']) ? $status['last_update'] : $lock_time;
                        
                        // If no progress in last 10 minutes, consider it stuck
                        if (($current_time - $progress_time) > 600) {
                            $this->filesystem->log('Database export appears stuck (no progress for 10+ minutes), removing lock');
                            @unlink($lock_file);
                            @unlink($status_file);
                        } else {
                            $this->filesystem->log('Database export in progress by another process (active within 10 minutes), skipping');
                            return;
                        }
                    } else {
                        $this->filesystem->log('Database export lock exists but no status file, removing stale lock');
                        @unlink($lock_file);
                    }
                } else {
                    // Lock is recent (less than 10 minutes old)
                    $this->filesystem->log('Recent database export lock detected, skipping to avoid conflicts');
                    return;
                }
            } else {
                // Invalid lock file format
                $this->filesystem->log('Invalid database export lock file format, removing');
                @unlink($lock_file);
                @unlink($status_file);
            }
        }
        
        // Create improved lock file with process info
        $lock_data = [
            'timestamp' => time(),
            'pid' => function_exists('getmypid') ? getmypid() : mt_rand(10000, 99999),
            'started' => time()
        ];
        file_put_contents($lock_file, json_encode($lock_data));
        file_put_contents($status_file, json_encode([
            'started' => time(),
            'last_update' => time(),
            'pid' => $lock_data['pid']
        ]));
        
        try {
            $this->filesystem->log('Starting protected database export');
            
            // Update status before starting
            $this->update_database_export_status($status_file, 'starting');
            
            // Use unified database exporter for regular export
            // Configure for regular export mode (no chunking, longer timeout)
            $config = array(
                'timeout' => 300,        // 5 minutes total timeout (vs 25s for fallback)
                'batch_size' => 500,     // Same as current regular export
                'transaction_size' => 100, // Same as current regular export
                'compression' => 'auto', // Smart compression like current
                'resume' => false,       // No resume for regular export (all-or-nothing)
                'charset' => 'utf8mb4',  // Same as current
                'chunk_tables' => 999,   // Process all tables in one chunk
            );
            
            // Create unified database exporter instance
            require_once dirname(__FILE__) . '/class-database-exporter.php';
            $db_exporter = new Custom_Migrator_Database_Exporter($config);
            
            // Execute database export (no resume state for regular export)
            $result = $db_exporter->export($sql_file);
            
            if (!$result['completed']) {
                throw new Exception('Database export failed to complete: ' . $result['message']);
            }
            
            // Update status after completion
            $this->update_database_export_status($status_file, 'completed');
            $this->filesystem->log('Database exported successfully to ' . basename($sql_file) . ' - ' . $result['total_tables'] . ' tables, ' . $result['rows_exported'] . ' rows');
            
        } catch (Exception $e) {
            $this->filesystem->log('Database export failed: ' . $e->getMessage());
            throw $e;
        } finally {
            // Always clean up lock files
            @unlink($lock_file);
            @unlink($status_file);
        }
    }

    /**
     * Update database export status.
     */
    private function update_database_export_status($status_file, $status) {
        if (file_exists($status_file)) {
            $current_status = json_decode(file_get_contents($status_file), true);
            $current_status['status'] = $status;
            $current_status['last_update'] = time();
            file_put_contents($status_file, json_encode($current_status));
        }
    }

    /**
     * Count lines in CSV file using unified method.
     */
    private function count_lines_in_csv($csv_file) {
        return Custom_Migrator_File_Enumerator::count_csv_lines($csv_file);
    }
}

Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists