From 1714a7e02294d4d13655c2f230c80a56840e5a83 Mon Sep 17 00:00:00 2001 From: selul Date: Fri, 9 Oct 2026 19:26:35 +0300 Subject: [PATCH 1/3] fix: keep new uploads local while an Image Storage rollback runs During a rollback, offload_media is off but is_offload_enabled() stays true so that already offloaded images keep being served from Optimole. That also kept the upload hooks on, so a new upload was offloaded while everything else was being moved back. generate_image_meta() and the filename deduplication in insert() are now registered only while offload_media is on; they belong together, because insert() renames the attached file for offloaded uploads only. Co-Authored-By: Claude Opus 5.5 --- inc/media_offload.php | 23 ++++++++++++++++------- tests/test-media.php | 25 +++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/inc/media_offload.php b/inc/media_offload.php index cd66df44..b12ca246 100644 --- a/inc/media_offload.php +++ b/inc/media_offload.php @@ -161,8 +161,14 @@ public static function instance() { self::$instance->init(); } if ( self::$instance->settings->is_offload_enabled() ) { + // While a rollback runs, offloaded images are still served from Optimole, but new uploads + // stay local: offloading them and their filename deduplication needs offload_media on. + $offload_new_uploads = self::$instance->settings->get( 'offload_media' ) === 'enabled'; + add_filter( 'image_downsize', [ self::$instance, 'generate_filter_downsize_urls' ], 10, 3 ); - add_filter( 'wp_generate_attachment_metadata', [ self::$instance, 'generate_image_meta' ], 10, 2 ); + if ( $offload_new_uploads ) { + add_filter( 'wp_generate_attachment_metadata', [ self::$instance, 'generate_image_meta' ], 10, 2 ); + } add_filter( 'wp_get_attachment_url', [ self::$instance, 'get_image_attachment_url' ], - 999, 2 ); add_filter( 'wp_insert_post_data', [ self::$instance, 'filter_uploaded_images' ] ); @@ -176,12 +182,15 @@ public static function instance() { add_filter( 'wp_calculate_image_srcset', [ self::$instance, 'calculate_image_srcset' ], 1, 5 ); add_action( 'post_updated', [ self::$instance, 'update_offload_meta' ], 10, 3 ); - // Backwards compatibility for older versions of WordPress < 6.0.0 requiring 3 parameters for this specific filter. - $below_6_0_0 = version_compare( get_bloginfo( 'version' ), '6.0.0', '<' ); - if ( $below_6_0_0 ) { - add_filter( 'wp_insert_attachment_data', [ self::$instance, 'insert_legacy' ], 10, 3 ); - } else { - add_filter( 'wp_insert_attachment_data', [ self::$instance, 'insert' ], 10, 4 ); + // Local uploads keep WordPress's own filename deduplication. + if ( $offload_new_uploads ) { + // Backwards compatibility for older versions of WordPress < 6.0.0 requiring 3 parameters for this specific filter. + $below_6_0_0 = version_compare( get_bloginfo( 'version' ), '6.0.0', '<' ); + if ( $below_6_0_0 ) { + add_filter( 'wp_insert_attachment_data', [ self::$instance, 'insert_legacy' ], 10, 3 ); + } else { + add_filter( 'wp_insert_attachment_data', [ self::$instance, 'insert' ], 10, 4 ); + } } add_action( 'optml_start_processing_images', [ self::$instance, 'start_processing_images' ], 10, 6 ); diff --git a/tests/test-media.php b/tests/test-media.php index 529b5373..d10e4576 100644 --- a/tests/test-media.php +++ b/tests/test-media.php @@ -345,6 +345,31 @@ public function test_image_sync() { $this->assertStringContainsString( '/w:300/h:200/q:mauto/process:' . self::$sample_attachement .'/id:579c7f7707ce87caa65fdf50c238a117', $image_medium_size[0] ); } + /** + * While a rollback runs (offload_media off, rollback_status on), offloaded images are still + * served from Optimole, but a new upload stays in the media library. + */ + public function test_upload_during_rollback_stays_local() { + $settings = new Optml_Settings(); + $settings->update( 'offload_media', 'disabled' ); + $settings->update( 'rollback_status', 'enabled' ); + + // The hooks of this request, registered with the rollback state. + remove_all_filters( 'wp_generate_attachment_metadata' ); + remove_all_filters( 'wp_insert_attachment_data' ); + Optml_Media_Offload::instance(); + + $this->assertFalse( has_filter( 'wp_generate_attachment_metadata' ) ); + $this->assertFalse( has_filter( 'wp_insert_attachment_data' ) ); + $this->assertNotFalse( has_filter( 'wp_get_attachment_url' ), 'Offloaded images are still served from Optimole.' ); + + $attachment_id = self::factory()->attachment->create_upload_object( OPTML_PATH . 'tests/assets/' . self::$files[0] . '.jpg' ); + $meta = wp_get_attachment_metadata( $attachment_id ); + + $this->assertFalse( Optml_Media_Offload::is_uploaded_image( $meta['file'] ) ); + $this->assertFileExists( get_attached_file( $attachment_id ) ); + } + public function test_image_rollback() { Optml_Media_Offload::instance()->rollback_images( 100 ); From bfa5ef95d91971d88ee7846c2b416a7e90416d8b Mon Sep 17 00:00:00 2001 From: selul Date: Sun, 11 Oct 2026 16:39:58 +0300 Subject: [PATCH 2/3] test: add a void return type to the rollback upload test Co-Authored-By: Claude Opus 5.5 --- tests/test-media.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test-media.php b/tests/test-media.php index d10e4576..7c3b5906 100644 --- a/tests/test-media.php +++ b/tests/test-media.php @@ -349,7 +349,7 @@ public function test_image_sync() { * While a rollback runs (offload_media off, rollback_status on), offloaded images are still * served from Optimole, but a new upload stays in the media library. */ - public function test_upload_during_rollback_stays_local() { + public function test_upload_during_rollback_stays_local(): void { $settings = new Optml_Settings(); $settings->update( 'offload_media', 'disabled' ); $settings->update( 'rollback_status', 'enabled' ); From aed9286862f67da504f6553282eba11d561689b9 Mon Sep 17 00:00:00 2001 From: selul Date: Sun, 11 Oct 2026 16:49:15 +0300 Subject: [PATCH 3/3] test: move the rollback upload test away from the other new media tests Three open PRs added tests at the same place in tests/test-media.php, which would conflict as they merge. Co-Authored-By: Claude Opus 5.5 --- tests/test-media.php | 50 ++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/tests/test-media.php b/tests/test-media.php index 7c3b5906..37e5d63b 100644 --- a/tests/test-media.php +++ b/tests/test-media.php @@ -224,6 +224,31 @@ static function unlinkRecursive( $pattern, $files ) { } } } + /** + * While a rollback runs (offload_media off, rollback_status on), offloaded images are still + * served from Optimole, but a new upload stays in the media library. + */ + public function test_upload_during_rollback_stays_local(): void { + $settings = new Optml_Settings(); + $settings->update( 'offload_media', 'disabled' ); + $settings->update( 'rollback_status', 'enabled' ); + + // The hooks of this request, registered with the rollback state. + remove_all_filters( 'wp_generate_attachment_metadata' ); + remove_all_filters( 'wp_insert_attachment_data' ); + Optml_Media_Offload::instance(); + + $this->assertFalse( has_filter( 'wp_generate_attachment_metadata' ) ); + $this->assertFalse( has_filter( 'wp_insert_attachment_data' ) ); + $this->assertNotFalse( has_filter( 'wp_get_attachment_url' ), 'Offloaded images are still served from Optimole.' ); + + $attachment_id = self::factory()->attachment->create_upload_object( OPTML_PATH . 'tests/assets/' . self::$files[0] . '.jpg' ); + $meta = wp_get_attachment_metadata( $attachment_id ); + + $this->assertFalse( Optml_Media_Offload::is_uploaded_image( $meta['file'] ) ); + $this->assertFileExists( get_attached_file( $attachment_id ) ); + } + public function test_retryable_error() { Optml_Media_Offload::instance()->rollback_and_update_images([self::$sample_attachement]); self::$error_mark = true; @@ -345,31 +370,6 @@ public function test_image_sync() { $this->assertStringContainsString( '/w:300/h:200/q:mauto/process:' . self::$sample_attachement .'/id:579c7f7707ce87caa65fdf50c238a117', $image_medium_size[0] ); } - /** - * While a rollback runs (offload_media off, rollback_status on), offloaded images are still - * served from Optimole, but a new upload stays in the media library. - */ - public function test_upload_during_rollback_stays_local(): void { - $settings = new Optml_Settings(); - $settings->update( 'offload_media', 'disabled' ); - $settings->update( 'rollback_status', 'enabled' ); - - // The hooks of this request, registered with the rollback state. - remove_all_filters( 'wp_generate_attachment_metadata' ); - remove_all_filters( 'wp_insert_attachment_data' ); - Optml_Media_Offload::instance(); - - $this->assertFalse( has_filter( 'wp_generate_attachment_metadata' ) ); - $this->assertFalse( has_filter( 'wp_insert_attachment_data' ) ); - $this->assertNotFalse( has_filter( 'wp_get_attachment_url' ), 'Offloaded images are still served from Optimole.' ); - - $attachment_id = self::factory()->attachment->create_upload_object( OPTML_PATH . 'tests/assets/' . self::$files[0] . '.jpg' ); - $meta = wp_get_attachment_metadata( $attachment_id ); - - $this->assertFalse( Optml_Media_Offload::is_uploaded_image( $meta['file'] ) ); - $this->assertFileExists( get_attached_file( $attachment_id ) ); - } - public function test_image_rollback() { Optml_Media_Offload::instance()->rollback_images( 100 );