This tool scans the wp-content/uploads/YYYY/MM
directories and queues only original files for import—skipping any resized versions (e.g. image-150x150.jpg
). It then processes the queue in batches for optimized, background importing.

This code adds an admin page that queues and imports original media files from existing uploads directories, skipping resized images.
Import may take time depending on the size and count. Give it time it will be done. There is queue system it will handle it.
/** * Description: Adds an admin page that queues and imports original media files from existing uploads directories, skipping resized images. */ /** * Define an option key for the import queue. */ define( 'MY_MEDIA_IMPORT_QUEUE_OPTION', 'my_media_import_queue' ); /** * Add a custom "Import Media" submenu under the Media admin menu. */ add_action( 'admin_menu', function() { add_submenu_page( 'upload.php', // Parent slug (Media menu). 'Import Media', // Page title. 'Import Media', // Menu title. 'manage_options', // Capability. 'custom-media-import', // Menu slug. 'my_custom_media_import_page_cb' // Callback function. ); }); /** * Render the Import Media admin page. */ function my_custom_media_import_page_cb() { // Check for form submission. if ( isset( $_POST['import_media_nonce'] ) && wp_verify_nonce( $_POST['import_media_nonce'], 'import_media_action' ) ) { // Build the queue from existing upload folders. $queue = my_custom_setup_media_import_queue(); if ( ! empty( $queue ) ) { // Schedule our cron event if not already scheduled. if ( ! wp_next_scheduled( 'my_media_import_cron_event' ) ) { wp_schedule_single_event( time() + 5, 'my_media_import_cron_event' ); } echo '<div class="notice notice-success is-dismissible"><p>Import queued: ' . count( $queue ) . ' files to process.</p></div>'; } else { echo '<div class="notice notice-info is-dismissible"><p>No new original files were found to import.</p></div>'; } } // If there is a queue already saved, show the remaining count. $queue = get_option( MY_MEDIA_IMPORT_QUEUE_OPTION, array() ); ?> <div class="wrap"> <h1>Import Media from Uploads</h1> <p> This tool scans the <code>wp-content/uploads/YYYY/MM</code> directories and queues <strong>only original files</strong> for import—skipping any resized versions (e.g. <code>image-150x150.jpg</code>). It then processes the queue in batches for optimized, background importing. </p> <form method="post"> <?php wp_nonce_field( 'import_media_action', 'import_media_nonce' ); ?> <input type="submit" class="button button-primary" value="Start Import"> </form> <?php if ( ! empty( $queue ) ) : ?> <p><strong>Import in progress:</strong> <?php echo count( $queue ); ?> file(s) remaining in the queue.</p> <?php endif; ?> </div> <?php } /** * Scan the uploads folders (YYYY/MM) and build a queue of original files. * Files matching the pattern -[width]x[height] (e.g., image-150x150.jpg) are skipped. * * @return array List of files to import. */ function my_custom_setup_media_import_queue() { $wp_uploads = wp_upload_dir(); $base_dir = $wp_uploads['basedir']; // e.g., /var/www/html/wp-content/uploads if ( ! is_dir( $base_dir ) ) { return array(); } $queue = array(); $years = scandir( $base_dir ); if ( ! $years ) { return $queue; } foreach ( $years as $year ) { if ( $year === '.' || $year === '..' ) { continue; } $year_path = $base_dir . '/' . $year; if ( ! is_dir( $year_path ) || ! preg_match( '/^\d{4}$/', $year ) ) { continue; } $months = scandir( $year_path ); if ( ! $months ) { continue; } foreach ( $months as $month ) { if ( $month === '.' || $month === '..' ) { continue; } $month_path = $year_path . '/' . $month; if ( ! is_dir( $month_path ) || ! preg_match( '/^\d{2}$/', $month ) ) { continue; } $files = scandir( $month_path ); if ( ! $files ) { continue; } foreach ( $files as $file ) { if ( $file === '.' || $file === '..' ) { continue; } $full_path = $month_path . '/' . $file; if ( is_dir( $full_path ) ) { continue; } // Skip files that look like a resized image (e.g., image-150x150.jpg). if ( preg_match( '/-\d+x\d+(?=\.[^.]+$)/i', $file ) ) { continue; } // Check for valid media type. $filetype = wp_check_filetype( $file, null ); if ( ! $filetype['type'] ) { continue; } $queue[] = array( 'full_path' => $full_path, 'filename' => $file, 'year' => $year, 'month' => $month, 'mime_type' => $filetype['type'], ); } } } // Save the queue into an option. update_option( MY_MEDIA_IMPORT_QUEUE_OPTION, $queue ); return $queue; } /** * WP Cron callback to process a batch of queued media imports. * Processes 50 files per run. */ function my_media_import_cron_event_handler() { $queue = get_option( MY_MEDIA_IMPORT_QUEUE_OPTION, array() ); if ( empty( $queue ) ) { return; } $batch_size = 50; $processed = 0; foreach ( $queue as $key => $item ) { if ( $processed >= $batch_size ) { break; } // Check if file exists; if not, remove it from the queue. if ( ! file_exists( $item['full_path'] ) ) { unset( $queue[ $key ] ); continue; } // Check if the attachment already exists. if ( attachment_exists_by_filename( $item['filename'] ) ) { unset( $queue[ $key ] ); continue; } // Import the file. $attach_id = import_file_as_attachment( $item['full_path'], $item['filename'], $item['mime_type'], $item['year'], $item['month'] ); $processed++; // Remove processed item from the queue. unset( $queue[ $key ] ); } // Re-index the queue. $queue = array_values( $queue ); update_option( MY_MEDIA_IMPORT_QUEUE_OPTION, $queue ); // If there are still queued files, schedule the next batch. if ( ! empty( $queue ) ) { wp_schedule_single_event( time() + 10, 'my_media_import_cron_event' ); } } add_action( 'my_media_import_cron_event', 'my_media_import_cron_event_handler' ); /** * Helper function: Check whether an attachment with the same file name already exists. * * NOTE: This is a simple check that uses the attachment post title. * * @param string $filename File name. * @return bool True if already exists; false otherwise. */ function attachment_exists_by_filename( $filename ) { global $wpdb; $query = $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->posts WHERE post_type = 'attachment' AND post_title = %s", pathinfo( $filename, PATHINFO_FILENAME ) ); $count = $wpdb->get_var( $query ); return ( $count > 0 ); } /** * Helper function: Import a single file into the Media Library. * * The file's last modified time is used as the attachment's post_date. * * @param string $full_path Full file path. * @param string $filename File name. * @param string $mime_type MIME type. * @param string $year Year from folder structure. * @param string $month Month from folder structure. * @return int|false Attachment ID on success, or false on failure. */ function import_file_as_attachment( $full_path, $filename, $mime_type, $year, $month ) { // Use the file's last modified time as post_date. $filetime = filemtime( $full_path ); $post_date = ( $filetime ) ? date( 'Y-m-d H:i:s', $filetime ) : current_time( 'mysql' ); $attachment = array( 'guid' => $full_path, 'post_mime_type' => $mime_type, 'post_title' => pathinfo( $filename, PATHINFO_FILENAME ), 'post_content' => '', 'post_status' => 'inherit', 'post_date' => $post_date, 'post_date_gmt' => get_gmt_from_date( $post_date ), ); // Insert the attachment into the database. $attach_id = wp_insert_attachment( $attachment, $full_path ); if ( ! is_wp_error( $attach_id ) ) { require_once( ABSPATH . 'wp-admin/includes/image.php' ); $attach_data = wp_generate_attachment_metadata( $attach_id, $full_path ); if ( ! is_wp_error( $attach_data ) ) { wp_update_attachment_metadata( $attach_id, $attach_data ); } return $attach_id; } return false; }