WordPress Media Library CDN FTP Upload with CloudTB's Free Cloud Storage Integration Plugin

Great news for WordPress users! CloudTB has released "WordPress Media to FTP with CDN Optimizer" plugin completely free of charge. This enterprise-grade solution supports all WordPress versions from 5.0 through 6.7 and is fully compatible with PHP versions 7.2 up to the latest PHP 8.3, ensuring broad compatibility with virtually any WordPress installation.

Plugin Overview: WordPress Media to FTP with CloudTB CDN Optimizer

The WordPress Media to FTP with CloudTB CDN Optimizer plugin represents a revolutionary approach to WordPress media management, specifically designed to address the growing challenges of media-heavy websites. At its core, this solution seamlessly bridges WordPress's native media handling system with CloudTB's enterprise-grade cloud storage infrastructure, automatically relocating media files while maintaining perfect integration with WordPress's content management workflow. The plugin's sophisticated architecture ensures that while media files are physically stored on CloudTB's secure cloud servers, they remain seamlessly accessible through WordPress's native media library interface. This dual-system approach effectively eliminates common hosting problems such as storage limitations, inode restrictions, and backup complications, while simultaneously enhancing website performance through Cloudflare's global CDN network.

Built with versatility and efficiency in mind, the plugin offers comprehensive compatibility with WordPress versions 5.0 through 6.7 and supports PHP versions 7.2 up to 8.3, ensuring broad compatibility across different hosting environments. The system employs intelligent batch processing for reliable file transfers, automated URL rewriting for CDN implementation, and smart migration tools for existing media libraries. This technical foundation enables the plugin to handle both new uploads and existing media migration effortlessly, while maintaining all SEO benefits and media relationships within WordPress. The implementation requires minimal server resources and operates transparently in the background, allowing content creators and site administrators to continue using familiar WordPress workflows without any learning curve or disruption to existing processes. This seamless integration, combined with CloudTB's robust storage infrastructure and CDN capabilities, provides a future-proof solution that automatically scales with your website's growth while preventing the need for expensive hosting upgrades.

Free Plugin, Premium Features

The plugin comes with no hidden costs or premium versions, offering all features completely free:

  • Full WordPress version support (v5.0 - v6.7)
  • Comprehensive PHP version compatibility (v7.2 - v8.3)
  • Enterprise-level media management
  • CDN integration capabilities
  • Advanced file handling features

Challenges of WordPress Websites with Growing Media Files

WordPress websites, particularly those rich in multimedia content, face several critical challenges:

  • Rapidly expanding media libraries consuming valuable hosting space
  • Excessive media archive that slows down the hosting performance
  • Critical inode usage triggers account suspensions by hosting providers
  • Excessive inode usage leading to unexpected website downtime
  • Slower backup processes due to large media folders
  • Increased hosting costs for additional storage
  • Performance bottlenecks affecting page load times
  • Complex media file management across multiple sites

Core Features and Implementation

1. Intelligent Media Transfer System

The plugin implements a sophisticated batch processing system that handles media transfers efficiently:

  • Processes 5 files per batch to ensure stability
  • Maintains proper error handling and retry mechanisms
  • Verifies file integrity after transfer
  • Provides detailed transfer logs and status updates

2. CDN Integration

Every file transferred through the plugin automatically benefits from CloudTB's CDN infrastructure:

  • Global content delivery through Cloudflare's network
  • Automatic SSL certification
  • Custom domain support
  • Optimized delivery paths for faster access

3. Storage Management

The plugin implements smart storage management features:

  • Configurable automatic deletion of local files after successful transfer
  • Selective file type filtering
  • Maintained WordPress media library functionality
  • Seamless URL rewriting for CDN access

Plugin Implementation

Here's the complete plugin code that you can use to implement this solution:


<?php
/**
 * Plugin Name: WordPress Media to FTP with CloudTB CDN Optimizer
 * Objective: Optimize WordPress performance by relocating large media folders to CloudTB's dedicated storage while keeping the core WordPress installation lightweight and fully optimized for media delivery and SEO performance. This solution automatically offloads media files to CloudTB's CDN-enabled storage, ensuring fast content delivery through Cloudflare CDN while reducing primary hosting webspace and inode usage. Perfect for websites struggling with hosting space limitations, the plugin maintains SEO integrity during media migration while enhancing overall site performance through optimized media delivery.
 * Description: This advanced WordPress Media to FTP Plugin (v3.2) is designed to seamlessly handle media file transfers from your WordPress site to CloudTB's FTP server, supporting both new uploads and existing media migration. The plugin works with WordPress versions 5.2 and above, and requires PHP 7.2 or higher due to its use of modern PHP features and exception handling. Features smart media management that automatically frees up local storage space after FTP transfer to CloudTB's file storage.
 * File Name: WP-CloudTB.php
 * Version: v3.2
 * Plugin Tested upto: WordPress 6.7 with PHP v8.3
 * Author: CLOUDTB.COM
 */

// Prevent direct access
if (!defined('ABSPATH')) {
    exit;
}

// ----------------------------------------------------------------------------
// PLUGIN CONFIGURATION - MODIFY SETTINGS BELOW
// ----------------------------------------------------------------------------

// CloudTB FTP Configuration
define('CLOUDTB_FTP_HOST', 'host.cloudtb.com');           // FTP hostname
define('CLOUDTB_FTP_USER', 'username');                   // FTP username
define('CLOUDTB_FTP_PASS', 'password');                   // FTP password
define('CLOUDTB_CDN_URL', 'https://username.cloudtb.online/'); // CDN URL with trailing slash

// File Management Settings
define('CLOUDTB_DELETE_AFTER_NORMAL', true);  // Delete local files after normal upload
define('CLOUDTB_DELETE_AFTER_BULK', true);    // Delete local files after bulk upload
define('CLOUDTB_ALLOWED_TYPES', 'all');       // Use 'all' or specify '.pdf .mp4 .mp3'

// ----------------------------------------------------------------------------
// ADVANCED SETTINGS - MODIFY WITH CAUTION
// ----------------------------------------------------------------------------

// FTP Settings
define('WP_FTP_CHUNK_SIZE', 5);              // Files per batch
define('WP_FTP_DEFAULT_PORT', 21);           // FTP port
define('WP_FTP_DEFAULT_PATH', 'www/public_html/'); // CloudTB web folder
define('WP_FTP_PASSIVE_MODE', true);         // FTP passive mode
define('WP_FTP_TIMEOUT', 30);                // Timeout in seconds
define('WP_FTP_RETRY_ATTEMPTS', 3);          // Upload retry attempts

// System Requirements
define('WP_FTP_MIN_MEMORY', 64 * 1024 * 1024); // 64MB minimum memory

// Debug Logging
define('WP_FTP_LOG_FILE', WP_CONTENT_DIR . '/ftp-upload-debug.log');
define('WP_FTP_MAX_LOG_SIZE', 200 * 1024 * 1024); // 200MB max log size
define('WP_FTP_LOG_ROTATION_SIZE', WP_FTP_MAX_LOG_SIZE / 2);

// Security
define('WP_FTP_AJAX_NONCE', 'wp_ftp_ajax_nonce');

// Initialize default settings
$wp_ftp_default_settings = array(
    // User Configuration (pulls from constants)
    'host'     => CLOUDTB_FTP_HOST,
    'user'     => CLOUDTB_FTP_USER,
    'pass'     => CLOUDTB_FTP_PASS,
    'cdn'      => CLOUDTB_CDN_URL,
    'delete_media_after_normal_ftp' => CLOUDTB_DELETE_AFTER_NORMAL,
    'delete_media_after_bulk_ftp'   => CLOUDTB_DELETE_AFTER_BULK,
    'filetypes' => CLOUDTB_ALLOWED_TYPES,
    
    // System Settings (pulls from constants)
    'port'     => WP_FTP_DEFAULT_PORT,
    'path'     => WP_FTP_DEFAULT_PATH,
    'passive'  => WP_FTP_PASSIVE_MODE,
    
    // Internal tracking (don't modify)
    'last_processed_file' => 0,
    'processing_batch'    => false,
    'total_files'        => 0,
    'processed_files'    => 0,
    'failed_files'       => 0,
    'total_size'        => 0,
    'processed_size'    => 0,
    'transfer_session_id' => '',
    'last_error'         => '',
    'retry_count'        => 0
);

class WP_FTP_Logger {
    private $log_file;
    private $max_size;
    private static $instance = null;

    private function __construct() {
        $this->log_file = WP_FTP_LOG_FILE;
        $this->max_size = WP_FTP_MAX_LOG_SIZE;
        
        // Create log directory if it doesn't exist
        $log_dir = dirname($this->log_file);
        if (!file_exists($log_dir)) {
            wp_mkdir_p($log_dir);
        }
    }

    public static function get_instance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function log($message) {
        if (!defined('WP_DEBUG') || !WP_DEBUG) {
            return;
        }

        $log_entry = date('Y-m-d H:i:s') . ' - ' . $message . "\n";

        // Check if log rotation is needed
        $this->maybe_rotate_log();

        // Append to log file
        file_put_contents(
            $this->log_file,
            $log_entry,
            FILE_APPEND | LOCK_EX
        );
    }

    private function maybe_rotate_log() {
        if (!file_exists($this->log_file)) {
            return;
        }

        $size = filesize($this->log_file);
        if ($size > $this->max_size) {
            $this->rotate_log();
        }
    }

    private function rotate_log() {
        // Read the entire file
        $handle = fopen($this->log_file, 'r+');
        if (!$handle) {
            return;
        }

        // Get file size
        fseek($handle, 0, SEEK_END);
        $size = ftell($handle);

        // Calculate where to start keeping logs (half of file)
        $start_pos = max(0, $size - WP_FTP_LOG_ROTATION_SIZE);
        fseek($handle, $start_pos);

        // Read the remainder of the file
        $content = fread($handle, $size - $start_pos);

        // Find the first complete line
        $pos = strpos($content, "\n");
        if ($pos !== false) {
            $content = substr($content, $pos + 1);
        }

        // Add rotation marker
        $rotated_content = "--- Log rotated at " . date('Y-m-d H:i:s') . " ---\n" . $content;

        // Truncate and rewrite file
        ftruncate($handle, 0);
        rewind($handle);
        fwrite($handle, $rotated_content);
        fclose($handle);
    }

    public function get_logs($lines = 100) {
        if (!file_exists($this->log_file)) {
            return array();
        }

        $logs = array();
        $handle = fopen($this->log_file, 'r');
        if ($handle) {
            // Read from end of file
            $pos = -2;
            $line = '';
            $count = 0;

            while ($count < $lines && fseek($handle, $pos, SEEK_END) !== -1) {
                $char = fgetc($handle);
                if ($char === "\n") {
                    array_unshift($logs, $line);
                    $line = '';
                    $count++;
                } else {
                    $line = $char . $line;
                }
                $pos--;
            }
            fclose($handle);
        }

        return $logs;
    }
}

class WP_CloudTB_UploadFTP {
    private $settings;
    private $connection;
    private $upload_dir;
    private $processed_count = 0;
    private $error_log = array();
    private $logger;

    public function __construct() {
        global $wp_ftp_default_settings;
        $this->upload_dir = wp_upload_dir();
        $this->settings = wp_parse_args(get_option('wp_ftp_settings', array()), $wp_ftp_default_settings);
        $this->logger = WP_FTP_Logger::get_instance();
        
        // Register hooks with error handling
        try {
            add_filter('wp_generate_attachment_metadata', array($this, 'handle_new_upload'));
            add_action('admin_init', array($this, 'register_settings'));
            add_action('admin_menu', array($this, 'add_settings_page'));
            add_action('wp_ajax_process_existing_media', array($this, 'process_existing_media_batch'));
            add_action('wp_ajax_get_transfer_status', array($this, 'get_transfer_status'));
            add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts'));
            
            // Added cleanup hook
            add_action('shutdown', array($this, 'cleanup_connection'));
        } catch (Exception $e) {
            $this->log_error('Hook Registration Error: ' . $e->getMessage());
        }
    }
    
    private function check_system_requirements() {
        $requirements_met = true;
        
        // Check memory limit
        $memory_limit = ini_get('memory_limit');
        $memory_limit_bytes = wp_convert_hr_to_bytes($memory_limit);
        if ($memory_limit_bytes < WP_FTP_MIN_MEMORY) {
            $this->log_error('Low memory limit detected: ' . $memory_limit . '. Recommended minimum is 64MB.');
            $requirements_met = false;
        }
    
        // Check PHP version
        if (version_compare(PHP_VERSION, '7.2', '<')) {
            $this->log_error('PHP version ' . PHP_VERSION . ' detected. Plugin requires PHP 7.2 or higher.');
            $requirements_met = false;
        }
    
        // Check FTP extension
        if (!extension_loaded('ftp')) {
            $this->log_error('PHP FTP extension is not installed.');
            $requirements_met = false;
        }
    
        return $requirements_met;
    }
    
    private function validate_settings() {
        // Check required FTP settings
        if (empty($this->settings['host']) || empty($this->settings['user'])) {
            $this->log_error('FTP credentials not configured.');
            return false;
        }
    
        // Validate and normalize path - CloudTB specific
        if (!empty($this->settings['path'])) {
            $this->settings['path'] = rtrim($this->settings['path'], '/') . '/';
            
            // CloudTB specific path validation
            if ($this->settings['path'] !== 'www/public_html/') {
                $this->log_error('Invalid remote path. Path must be: www/public_html/');
                return false;
            }
        }
    
        // Validate CDN URL if provided
        if (!empty($this->settings['cdn'])) {
            if (!filter_var($this->settings['cdn'], FILTER_VALIDATE_URL)) {
                $this->log_error('Invalid CDN URL format.');
                return false;
            }
            $this->settings['cdn'] = rtrim($this->settings['cdn'], '/') . '/';
        }
    
        // Add file type validation
        if (!$this->validate_file_types()) {
            return false;
        }
    
        return true;
    }
    
    private function validate_file_types() {
        if (empty($this->settings['filetypes']) || $this->settings['filetypes'] === 'all') {
            return true;
        }
    
        // Convert filetypes string to array
        $allowed_types = array_map('trim', explode(' ', $this->settings['filetypes']));
        
        // Validate each extension format
        foreach ($allowed_types as $type) {
            if (!preg_match('/^\.\w+$/', $type)) {
                $this->log_error("Invalid file type format: $type. Format should be like '.pdf'");
                return false;
            }
        }
        
        return true;
    }
    
    private function is_file_type_allowed($file_path) {
        // If filetypes is 'all' or empty, allow all files
        if (empty($this->settings['filetypes']) || $this->settings['filetypes'] === 'all') {
            return true;
        }
    
        // Get file extension with dot
        $extension = strtolower('.' . pathinfo($file_path, PATHINFO_EXTENSION));
        
        // Convert filetypes string to array
        $allowed_types = array_map('trim', explode(' ', strtolower($this->settings['filetypes'])));
        
        // Check if extension is in allowed types
        return in_array($extension, $allowed_types);
    }

    // Added connection cleanup method
    public function cleanup_connection() {
        if ($this->connection && is_resource($this->connection)) {
            ftp_close($this->connection);
        }
    }

    private function connect_ftp() {
        // Check system requirements first
        if (!$this->check_system_requirements()) {
            return false;
        }

        // Validate settings before attempting connection
        if (!$this->validate_settings()) {
            return false;
        }

        if (!function_exists('ftp_connect')) {
            $this->log_error('FTP functions are not available on this server.');
            return false;
        }

        try {
            // Set timeout for connection
            $this->connection = @ftp_connect($this->settings['host'], $this->settings['port'], WP_FTP_TIMEOUT);
            
            if (!$this->connection) {
                throw new Exception('Could not connect to FTP server');
            }

            // Set keepalive
            @ftp_set_option($this->connection, FTP_TIMEOUT_SEC, WP_FTP_TIMEOUT);
            
            if (!@ftp_login($this->connection, $this->settings['user'], $this->settings['pass'])) {
                throw new Exception('FTP login failed');
            }

            if ($this->settings['passive']) {
                @ftp_pasv($this->connection, true);
            }

            return true;
        } catch (Exception $e) {
            $this->log_error('FTP Connection Error: ' . $e->getMessage());
            $this->cleanup_connection();
            return false;
        }
    }

    private function delete_local_file($file_path, $is_bulk = false) {
        // Check if deletion is enabled for this upload type
        if (!$this->settings[$is_bulk ? 'delete_media_after_bulk_ftp' : 'delete_media_after_normal_ftp']) {
            return false;
        }

        try {
            // Simple file existence and permission check
            if (!file_exists($file_path) || !is_writable($file_path)) {
                throw new Exception("File not accessible for deletion: $file_path");
            }

            // Simple deletion
            if (!@unlink($file_path)) {
                throw new Exception("Failed to delete file: $file_path");
            }

            return true;

        } catch (Exception $e) {
            $this->log_error($e->getMessage());
            return false;
        }
    }

    private function get_attachment_id_from_path($file_path) {
        global $wpdb;
        $upload_dir = wp_upload_dir();
        $relative_path = str_replace($upload_dir['basedir'] . '/', '', $file_path);
        
        return $wpdb->get_var($wpdb->prepare(
            "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_wp_attached_file' AND meta_value = %s",
            $relative_path
        ));
    }

    private function upload_file_to_ftp($file_path, $is_bulk = false) {
        $retry_count = 0;
        $max_retries = WP_FTP_RETRY_ATTEMPTS;

        // Verify file before attempting upload
        try {
            $file_size = $this->verify_file_operation($file_path);
            if ($file_size === false) {
                $this->log_error("File verification failed for: $file_path");
                return false;
            }
        } catch (Exception $e) {
            $this->log_error("File verification error: " . $e->getMessage());
            return false;
        }

        while ($retry_count < $max_retries) {
            try {
                $rel_path = str_replace($this->upload_dir['basedir'], '', $file_path);
                $remote_path = $this->settings['path'] . $rel_path;
                
                // Ensure remote directory exists
                if (!$this->create_ftp_path(dirname($remote_path))) {
                    throw new Exception("Failed to create remote directory structure");
                }

                // Upload with error checking
                if (!@ftp_put($this->connection, $remote_path, $file_path, FTP_BINARY)) {
                    throw new Exception("FTP upload failed");
                }

                // Verify file exists on remote server
                $remote_size = @ftp_size($this->connection, $remote_path);
                
                if ($remote_size !== $file_size) { // Now using verified file size
                    throw new Exception("Size mismatch after upload: local($file_size) != remote($remote_size)");
                }

                // Update URLs if CDN is configured
                if (!empty($this->settings['cdn'])) {
                    $this->update_attachment_urls($file_path);
                }
                
                // Delete local file if enabled
                $this->delete_local_file($file_path, $is_bulk);
                
                return true;

            } catch (Exception $e) {
                $retry_count++;
                $this->log_error("Upload attempt $retry_count failed: " . $e->getMessage());
                
                if ($retry_count >= $max_retries) {
                    $this->log_error("Max retries reached for: $file_path");
                    return false;
                }
                
                // Wait before retry
                sleep(2);
                
                // Try to reconnect if connection lost
                if (!$this->connection || !@ftp_systype($this->connection)) {
                    $this->cleanup_connection();
                    $this->connect_ftp();
                }
            }
        }

        return false;
    }

    private function create_ftp_path($path) {
        try {
            $path = ltrim($path, '/');
            $parts = explode('/', $path);
            $current = '';
            
            foreach ($parts as $part) {
                if (empty($part)) continue;
                
                $current .= '/' . $part;
                
                // Try to change to directory
                if (@ftp_chdir($this->connection, $current)) {
                    continue;
                }
                
                // Create directory if it doesn't exist
                if (!@ftp_mkdir($this->connection, $current)) {
                    throw new Exception("Failed to create directory: $current");
                }
                
                // Set directory permissions
                @ftp_chmod($this->connection, 0755, $current);
            }
            
            return true;
        } catch (Exception $e) {
            $this->log_error($e->getMessage());
            return false;
        }
    }

    private function update_attachment_urls($file_path) {
        global $wpdb;
        
        try {
            $rel_path = str_replace($this->upload_dir['basedir'], '', $file_path);
            $old_url = $this->upload_dir['baseurl'] . $rel_path;
            $new_url = rtrim($this->settings['cdn'], '/') . $rel_path;
            
            // Update encoded URLs as well
            $old_url_encoded = rawurlencode($old_url);
            $new_url_encoded = rawurlencode($new_url);

            // Start transaction
            $wpdb->query('START TRANSACTION');

            // Update post content
            $wpdb->query($wpdb->prepare(
                "UPDATE {$wpdb->posts} 
                SET post_content = REPLACE(REPLACE(post_content, %s, %s), %s, %s)",
                $old_url, $new_url, $old_url_encoded, $new_url_encoded
            ));

            // Update serialized data in postmeta
            $serialized_meta = $wpdb->get_results(
                "SELECT meta_id, meta_value 
                FROM {$wpdb->postmeta} 
                WHERE meta_value LIKE '%{$old_url}%'"
            );

            foreach ($serialized_meta as $meta) {
                $updated_value = $this->update_serialized_url($meta->meta_value, $old_url, $new_url);
                if ($updated_value !== false) {
                    $wpdb->update(
                        $wpdb->postmeta,
                        array('meta_value' => $updated_value),
                        array('meta_id' => $meta->meta_id)
                    );
                }
            }

            $wpdb->query('COMMIT');
            return true;

        } catch (Exception $e) {
            $wpdb->query('ROLLBACK');
            $this->log_error('URL Update Error: ' . $e->getMessage());
            return false;
        }
    }

    private function update_serialized_url($data, $old_url, $new_url) {
        try {
            if (is_serialized($data)) {
                $unserialized = @unserialize($data);
                if ($unserialized === false) {
                    return false;
                }
                
                $updated = $this->replace_urls_recursive($unserialized, $old_url, $new_url);
                return serialize($updated);
            }
            
            return str_replace($old_url, $new_url, $data);
        } catch (Exception $e) {
            $this->log_error('Serialization Error: ' . $e->getMessage());
            return false;
        }
    }

    private function replace_urls_recursive($data, $old_url, $new_url) {
        if (is_array($data)) {
            foreach ($data as $key => $value) {
                $data[$key] = $this->replace_urls_recursive($value, $old_url, $new_url);
            }
        } elseif (is_string($data)) {
            $data = str_replace($old_url, $new_url, $data);
        }
        return $data;
    }

    private function log_error($message) {
        // Keep the in-memory error log for the current session
        $this->error_log[] = date('Y-m-d H:i:s') . ' - ' . $message;
        
        // Update settings with last error
        $this->settings['last_error'] = $message;
        update_option('wp_ftp_settings', $this->settings);

        // Log to file if debug is enabled
        $this->logger->log($message);
    }
    
    private function display_recent_logs() {
        $logs = $this->logger->get_logs(50); // Get last 50 log entries
        if (!empty($logs)) {
            echo '<div class="log-viewer" style="margin-top: 20px; padding: 10px; background: #fff; border: 1px solid #ccc; max-height: 300px; overflow-y: auto;">';
            echo '<h3>Recent Logs</h3>';
            echo '<pre>';
            foreach ($logs as $log) {
                echo esc_html($log) . "\n";
            }
            echo '</pre>';
            echo '</div>';
        }
    }

public function enqueue_admin_scripts($hook) {
        if ($hook != 'settings_page_wp-ftp-upload') {
            return;
        }

        // Inline JavaScript for progress tracking
        add_action('admin_footer', function() {
            ?>
<script type="text/javascript">
jQuery(document).ready(function($) {
    var progressCheck;
    
    // Check if transfer was in progress when page loads
    if (localStorage.getItem('ftp_transfer_active') === 'true') {
        startProgressChecking();
    }
    
    function formatBytes(bytes, decimals = 2) {
        if (bytes === 0) return '0 Bytes';
        const k = 1024;
        const dm = decimals < 0 ? 0 : decimals;
        const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
        const i = Math.floor(Math.log(bytes) / Math.log(k));
        return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
    }

    function startProgressChecking() {
        $('#start-bulk-upload').prop('disabled', true);
        localStorage.setItem('ftp_transfer_active', 'true');
        progressCheck = setInterval(updateProgress, 2000);
        
        // Add warning when trying to leave page
        $(window).on('beforeunload', function(e) {
            if (localStorage.getItem('ftp_transfer_active') === 'true') {
                e.preventDefault();
                return "File transfer is in progress. Leaving this page will hide the progress but transfer will continue in background. Are you sure?";
            }
        });
    }

    function stopProgressChecking() {
        clearInterval(progressCheck);
        localStorage.removeItem('ftp_transfer_active');
        $('#start-bulk-upload').prop('disabled', false);
        $(window).off('beforeunload');
    }

    function updateProgress() {
        $.ajax({
            url: ajaxurl,
            data: {
                action: 'get_transfer_status',
                nonce: '<?php echo wp_create_nonce(WP_FTP_AJAX_NONCE); ?>'
            },
            success: function(response) {
                if (response.success) {
                    var data = response.data;
                    var percent = Math.round((data.processed_files / data.total_files) * 100);
                    
                    $('#progress-bar').css('width', percent + '%').attr('aria-valuenow', percent);
                    $('#progress-text').text(percent + '%');
                    
                    $('#stats-files').text(data.processed_files + ' / ' + data.total_files);
                    $('#stats-size').text(formatBytes(data.processed_size) + ' / ' + formatBytes(data.total_size));
                    $('#stats-failed').text(data.failed_files);

                    if (!data.processing_batch) {
                        stopProgressChecking();
                        alert('Transfer completed!\nProcessed: ' + data.processed_files + ' files\nFailed: ' + data.failed_files + ' files');
                    }
                }
            },
            error: function() {
                $('#transfer-status').html('<div class="notice notice-warning"><p>Unable to fetch progress. Transfer continuing in background...</p></div>');
            }
        });
    }

    $('#start-bulk-upload').click(function(e) {
        e.preventDefault();
        
        $.ajax({
            url: ajaxurl,
            method: 'POST',
            data: {
                action: 'process_existing_media',
                nonce: '<?php echo wp_create_nonce(WP_FTP_AJAX_NONCE); ?>'
            },
            success: function(response) {
                if (response.success) {
                    startProgressChecking();
                } else {
                    alert('Failed to start transfer: ' + response.data);
                }
            }
        });
    });

    // Add resume button if transfer was interrupted
    if (localStorage.getItem('ftp_transfer_active') === 'true') {
        $('.bulk-upload-section').prepend(
            '<div class="notice notice-warning">' +
            '<p>A file transfer was in progress. Current status:</p>' +
            '<button class="button button-secondary" id="resume-tracking">Show Progress</button>' +
            '</div>'
        );

        $('#resume-tracking').click(function() {
            startProgressChecking();
        });
    }
});
</script>
            <?php
        });
    }

    public function get_transfer_status() {
        check_ajax_referer(WP_FTP_AJAX_NONCE, 'nonce');
        wp_send_json_success($this->settings);
    }

    public function register_settings() {
        register_setting('wp_ftp_upload_settings', 'wp_ftp_settings');
    }

    public function add_settings_page() {
        add_options_page(
            'FTP Upload Settings',
            'FTP Upload',
            'manage_options',
            'wp-ftp-upload',
            array($this, 'render_settings_page')
        );
    }

    public function render_settings_page() {
        if (!current_user_can('manage_options')) {
            return;
        }

        ?>
        <div class="wrap">
            <h1>FTP Upload Manager Settings</h1>
            <form method="post" action="options.php">
                <?php
                settings_fields('wp_ftp_upload_settings');
                ?>
                <table class="form-table">
                    <tr>
                        <th>FTP Host</th>
                        <td><input type="text" name="wp_ftp_settings[host]" value="<?php echo esc_attr($this->settings['host']); ?>" class="regular-text"></td>
                    </tr>
                    <tr>
                        <th>FTP Port</th>
                        <td><input type="number" name="wp_ftp_settings[port]" value="<?php echo esc_attr($this->settings['port']); ?>" class="small-text"></td>
                    </tr>
                    <tr>
                        <th>FTP Username</th>
                        <td><input type="text" name="wp_ftp_settings[user]" value="<?php echo esc_attr($this->settings['user']); ?>" class="regular-text"></td>
                    </tr>
                    <tr>
                        <th>FTP Password</th>
                        <td><input type="password" name="wp_ftp_settings[pass]" value="<?php echo esc_attr($this->settings['pass']); ?>" class="regular-text"></td>
                    </tr>
                    <tr>
                        <th>CDN URL (optional)</th>
                        <td><input type="url" name="wp_ftp_settings[cdn]" value="<?php echo esc_attr($this->settings['cdn']); ?>" class="regular-text"></td>
                    </tr>
                    <tr>
                        <th>Remote Path</th>
                        <td><input type="text" name="wp_ftp_settings[path]" value="<?php echo esc_attr($this->settings['path']); ?>" class="regular-text"></td>
                    </tr>
                    
                    <tr>
                        <th>Allowed File Types</th>
                        <td>
                        <input type="text" name="wp_ftp_settings[filetypes]" value="<?php echo esc_attr($this->settings['filetypes']); ?>" class="regular-text">
                        <p class="description">Enter 'all' for any file type, or specific extensions with dots (e.g. '.pdf .mp4 .mp3')</p>
                        </td>
                    </tr>
                    
                    <tr>
                        <th>Passive Mode</th>
                        <td><input type="checkbox" name="wp_ftp_settings[passive]" <?php checked($this->settings['passive'], true); ?> value="1"></td>
                    </tr>
                    <tr>
                        <th>File Deletion Settings</th>
                        <td>
                            <label>
                                <input type="checkbox" 
                                       name="wp_ftp_settings[delete_media_after_normal_ftp]" 
                                       <?php checked($this->settings['delete_media_after_normal_ftp'], true); ?> 
                                       value="1">
                                Delete local files after normal FTP upload
                            </label>
                            <br><br>
                            <label>
                                <input type="checkbox" 
                                       name="wp_ftp_settings[delete_media_after_bulk_ftp]" 
                                       <?php checked($this->settings['delete_media_after_bulk_ftp'], true); ?> 
                                       value="1">
                                Delete local files after bulk FTP upload
                            </label>
                            <p class="description">Warning: Enable these options only if you're sure your FTP uploads are working correctly and you have backups.</p>
                        </td>
                    </tr>
                </table>
                <?php submit_button(); ?>
            </form>

            <hr>

            <h2>Bulk Upload Existing Media</h2>
            <div class="bulk-upload-section">
                <p>Current chunk size: <?php echo WP_FTP_CHUNK_SIZE; ?> files per batch</p>
                <p>You can modify WP_FTP_CHUNK_SIZE in the plugin file based on your server capabilities.</p>
                
                <!-- Progress Bar -->
                <div class="progress" style="height: 20px; margin: 20px 0; background-color: #f5f5f5; border-radius: 4px; overflow: hidden;">
                    <div id="progress-bar" class="progress-bar" role="progressbar" style="width: 0%; height: 100%; background-color: #0073aa; transition: width .6s ease;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
                        <span id="progress-text" style="color: white; line-height: 20px; margin-left: 5px;">0%</span>
                    </div>
                </div>

                <!-- Statistics -->
                <div class="transfer-stats" style="margin: 20px 0; padding: 15px; background: #fff; border: 1px solid #ccc;">
                    <h3>Transfer Statistics</h3>
                    <p><strong>Files Processed:</strong> <span id="stats-files">0 / 0</span></p>
                    <p><strong>Total Size:</strong> <span id="stats-size">0 B / 0 B</span></p>
                    <p><strong>Failed Uploads:</strong> <span id="stats-failed">0</span></p>
                </div>

                <!-- Start Button -->
                <button id="start-bulk-upload" class="button button-primary" <?php echo $this->settings['processing_batch'] ? 'disabled' : ''; ?>>
                    Start Bulk Upload
                </button>
            </div>
            
            <hr>
            <h2>Debug Logs</h2>
            <?php $this->display_recent_logs(); ?>
            
        </div>
        <?php
    }

    public function process_existing_media_batch() {
        check_ajax_referer(WP_FTP_AJAX_NONCE, 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error('Unauthorized access');
            return;
        }
        
        if (!$this->settings['processing_batch']) {
            if (!$this->validate_file_types()) {
                wp_send_json_error('Invalid file type configuration');
                return;
            }
            $this->initialize_transfer();
        }
        
        // Check memory usage before starting
        $memory_limit = wp_convert_hr_to_bytes(ini_get('memory_limit'));
        $current_usage = memory_get_usage(true);
        if ($current_usage > ($memory_limit * 0.8)) { // If using more than 80% of available memory
            $this->log_error('High memory usage detected. Current usage: ' . size_format($current_usage));
            wp_send_json_error('Insufficient memory available');
            return;
        }

        // Initialize transfer if not already started
        if (!$this->settings['processing_batch']) {
            $this->initialize_transfer();
        }

        if (!$this->connect_ftp()) {
            $this->settings['processing_batch'] = false;
            update_option('wp_ftp_settings', $this->settings);
            wp_send_json_error('FTP connection failed');
            return;
        }

        $args = array(
            'post_type' => 'attachment',
            'posts_per_page' => WP_FTP_CHUNK_SIZE,
            'offset' => $this->settings['last_processed_file'],
            'post_status' => 'inherit'
        );

        $attachments = get_posts($args);

        if (empty($attachments)) {
            $this->settings['processing_batch'] = false;
            $this->settings['last_processed_file'] = 0;
            update_option('wp_ftp_settings', $this->settings);
            wp_send_json_success('All files processed');
            return;
        }

        foreach ($attachments as $attachment) {
            $file_path = get_attached_file($attachment->ID);
            if ($file_path && file_exists($file_path)) {
                $file_size = filesize($file_path);
                if ($this->upload_file_to_ftp($file_path, true)) {
                    $this->settings['processed_size'] += $file_size;
                    $this->settings['processed_files']++;
                } else {
                    $this->settings['failed_files']++;
                }
            }
        }

        $this->settings['last_processed_file'] += WP_FTP_CHUNK_SIZE;
        update_option('wp_ftp_settings', $this->settings);

        if ($this->connection) {
            ftp_close($this->connection);
        }

        wp_send_json_success($this->settings);
    }

    private function initialize_transfer() {
        $args = array(
            'post_type' => 'attachment',
            'posts_per_page' => -1,
            'post_status' => 'inherit',
            'fields' => 'ids' // Only get post IDs to save memory
        );
        
        $attachments = get_posts($args);
        $total_size = 0;
        
        foreach ($attachments as $attachment_id) {
            $file_path = get_attached_file($attachment_id);
            if ($file_path && file_exists($file_path)) {
                $total_size += filesize($file_path);
            }
        }

        $this->settings['total_files'] = count($attachments);
        $this->settings['total_size'] = $total_size;
        $this->settings['processed_files'] = 0;
        $this->settings['processed_size'] = 0;
        $this->settings['failed_files'] = 0;
        $this->settings['last_processed_file'] = 0;
        $this->settings['processing_batch'] = true;
        $this->settings['transfer_session_id'] = uniqid('ftp_transfer_', true);

        update_option('wp_ftp_settings', $this->settings);
    }
    
    private function verify_file_operation($file_path) {
        // Check if file exists
        if (!file_exists($file_path)) {
            throw new Exception("File does not exist: $file_path");
        }

        // Check file permissions
        if (!is_readable($file_path)) {
            throw new Exception("File is not readable: $file_path");
        }

        // Check file size
        $file_size = filesize($file_path);
        if ($file_size === 0) {
            throw new Exception("File is empty: $file_path");
        }

        // Check if file is within WordPress upload directory
        if (strpos($file_path, $this->upload_dir['basedir']) !== 0) {
            throw new Exception("File is not within WordPress upload directory: $file_path");
        }

        return $file_size;
    }
    
}

// Initialize the plugin
new WP_CloudTB_UploadFTP();

Installing the WP-CloudTB.php Plugin in WordPress

Follow these step-by-step instructions to install the WordPress Media to FTP with CloudTB CDN Optimizer plugin on your WordPress website:

Method 1: Using WordPress Dashboard (Recommended)

  1. Downloading the Plugin
    • Save the above code into a file WP-CloudTB.php
    • or Download File: by Clicking Here
  2. Access WordPress Admin:
    • Log in to your WordPress dashboard
    • Navigate to Plugins → Add New → Upload Plugin
  3. Upload Plugin:
    • Click the 'Choose File' button
    • Select the WP-CloudTB.php file from your computer
    • Click 'Install Now'
  4. Activate Plugin:
    • After installation completes, click 'Activate Plugin'
    • The plugin will now appear in your installed plugins list

Method 2: Manual FTP Installation

  1. Downloading the Plugin
    • Save the above code into a file WP-CloudTB.php
    • or Download File: by Clicking Here
  2. Prepare Plugin Folder:
    • Create a new folder named 'wp-cloudtb'
    • Place WP-CloudTB.php inside this folder
  3. Upload via FTP:
    • Open your FTP client (like FileZilla, WinSCP, etc.)
    • Connect to your website using your FTP credentials
    • Navigate to your WordPress plugins directory: /wp-content/plugins/
    • Upload the entire 'wp-cloudtb' folder (not just the PHP file)
    • The final path should be: /wp-content/plugins/wp-cloudtb/WP-CloudTB.php
  4. Activate in WordPress:
    • Go to WordPress dashboard
    • Navigate to Plugins
    • Find "WordPress Media to FTP with CloudTB CDN Optimizer"
    • Click 'Activate'

After Installation

  1. Initial Configuration:
    • Go to Settings → FTP Upload in your WordPress dashboard
    • Enter your CloudTB FTP credentials:
      • FTP Host: host.cloudtb.com
      • FTP Username: Your CloudTB username
      • FTP Password: Your CloudTB password
      • CDN URL: Your CloudTB CDN URL (e.g., https://username.cloudtb.online/)
    • Save your settings
  2. Verify Installation:
    • Upload a test image to your media library
    • Verify that the file appears in your CloudTB storage
    • Check that the image URL has been updated to use your CDN

Troubleshooting

If you encounter any issues during installation:

  • Ensure your WordPress version is 5.0 or higher
  • Verify PHP version is 7.2 or higher
  • Check that the /wp-content/plugins/ directory is writable
  • Confirm your hosting environment supports FTP functions
  • Ensure your CloudTB FTP credentials are correct

Support

For installation assistance or troubleshooting:

  • Contact CloudTB support through your client area
  • Submit a support ticket with detailed error messages if encountered
  • Check WordPress error logs for any installation-related issues
View CLOUDTB Plans