@@ -113,10 +113,46 @@ public function register_routes() {
113113 'callback ' => array ( $ this , 'finalize_item ' ),
114114 'permission_callback ' => array ( $ this , 'edit_media_item_permissions_check ' ),
115115 'args ' => array (
116- 'id ' => array (
116+ 'id ' => array (
117117 'description ' => __ ( 'Unique identifier for the attachment. ' ),
118118 'type ' => 'integer ' ,
119119 ),
120+ 'sub_sizes ' => array (
121+ 'description ' => __ ( 'Array of sub-size metadata collected from sideload responses. ' ),
122+ 'type ' => 'array ' ,
123+ 'default ' => array (),
124+ 'items ' => array (
125+ 'type ' => 'object ' ,
126+ 'properties ' => array (
127+ 'image_size ' => array (
128+ 'type ' => 'string ' ,
129+ 'required ' => true ,
130+ ),
131+ 'width ' => array (
132+ 'type ' => 'integer ' ,
133+ 'minimum ' => 1 ,
134+ ),
135+ 'height ' => array (
136+ 'type ' => 'integer ' ,
137+ 'minimum ' => 1 ,
138+ ),
139+ 'file ' => array (
140+ 'type ' => 'string ' ,
141+ ),
142+ 'mime_type ' => array (
143+ 'type ' => 'string ' ,
144+ 'pattern ' => '^image/.* ' ,
145+ ),
146+ 'filesize ' => array (
147+ 'type ' => 'integer ' ,
148+ 'minimum ' => 1 ,
149+ ),
150+ 'original_image ' => array (
151+ 'type ' => 'string ' ,
152+ ),
153+ ),
154+ ),
155+ ),
120156 ),
121157 ),
122158 'allow_batch ' => $ this ->allow_batch ,
@@ -2082,16 +2118,19 @@ public function sideload_item( WP_REST_Request $request ) {
20822118
20832119 $ image_size = $ request ['image_size ' ];
20842120
2085- $ metadata = wp_get_attachment_metadata ( $ attachment_id , true );
2086-
2087- if ( ! $ metadata ) {
2088- $ metadata = array ();
2089- }
2121+ // Build sub-size data to return to the client.
2122+ // The client accumulates these and sends them all to the finalize
2123+ // endpoint, which writes the metadata in a single operation. This
2124+ // avoids the read-modify-write race that concurrent sideloads for the
2125+ // same attachment would otherwise hit.
2126+ $ sub_size_data = array (
2127+ 'image_size ' => $ image_size ,
2128+ );
20902129
20912130 if ( 'original ' === $ image_size ) {
2092- $ metadata [ ' original_image ' ] = wp_basename ( $ path );
2131+ $ sub_size_data [ ' file ' ] = wp_basename ( $ path );
20932132 } elseif ( 'scaled ' === $ image_size ) {
2094- // The current attached file is the original; record it as original_image .
2133+ // Record the current attached file as the original.
20952134 $ current_file = get_attached_file ( $ attachment_id , true );
20962135
20972136 if ( ! $ current_file ) {
@@ -2102,7 +2141,7 @@ public function sideload_item( WP_REST_Request $request ) {
21022141 );
21032142 }
21042143
2105- $ metadata ['original_image ' ] = wp_basename ( $ current_file );
2144+ $ sub_size_data ['original_image ' ] = wp_basename ( $ current_file );
21062145
21072146 // Validate the scaled image before updating the attached file.
21082147 $ size = wp_getimagesize ( $ path );
@@ -2117,6 +2156,7 @@ public function sideload_item( WP_REST_Request $request ) {
21172156 }
21182157
21192158 // Update the attached file to point to the scaled version.
2159+ // This writes to _wp_attached_file meta, not _wp_attachment_metadata.
21202160 if (
21212161 get_attached_file ( $ attachment_id , true ) !== $ path &&
21222162 ! update_attached_file ( $ attachment_id , $ path )
@@ -2128,42 +2168,21 @@ public function sideload_item( WP_REST_Request $request ) {
21282168 );
21292169 }
21302170
2131- $ metadata ['width ' ] = $ size [0 ];
2132- $ metadata ['height ' ] = $ size [1 ];
2133- $ metadata ['filesize ' ] = $ filesize ;
2134- $ metadata ['file ' ] = _wp_relative_upload_path ( $ path );
2171+ $ sub_size_data ['width ' ] = $ size [0 ];
2172+ $ sub_size_data ['height ' ] = $ size [1 ];
2173+ $ sub_size_data ['filesize ' ] = $ filesize ;
2174+ $ sub_size_data ['file ' ] = _wp_relative_upload_path ( $ path );
21352175 } else {
2136- $ metadata ['sizes ' ] = $ metadata ['sizes ' ] ?? array ();
2137-
21382176 $ size = wp_getimagesize ( $ path );
21392177
2140- $ metadata ['sizes ' ][ $ image_size ] = array (
2141- 'width ' => $ size ? $ size [0 ] : 0 ,
2142- 'height ' => $ size ? $ size [1 ] : 0 ,
2143- 'file ' => wp_basename ( $ path ),
2144- 'mime-type ' => $ type ,
2145- 'filesize ' => wp_filesize ( $ path ),
2146- );
2147- }
2148-
2149- wp_update_attachment_metadata ( $ attachment_id , $ metadata );
2150-
2151- $ response_request = new WP_REST_Request (
2152- WP_REST_Server::READABLE ,
2153- rest_get_route_for_post ( $ attachment_id )
2154- );
2155-
2156- $ response_request ['context ' ] = 'edit ' ;
2157-
2158- if ( isset ( $ request ['_fields ' ] ) ) {
2159- $ response_request ['_fields ' ] = $ request ['_fields ' ];
2178+ $ sub_size_data ['width ' ] = $ size ? $ size [0 ] : 0 ;
2179+ $ sub_size_data ['height ' ] = $ size ? $ size [1 ] : 0 ;
2180+ $ sub_size_data ['file ' ] = wp_basename ( $ path );
2181+ $ sub_size_data ['mime_type ' ] = $ type ;
2182+ $ sub_size_data ['filesize ' ] = wp_filesize ( $ path );
21602183 }
21612184
2162- $ response = $ this ->prepare_item_for_response ( get_post ( $ attachment_id ), $ response_request );
2163-
2164- $ response ->header ( 'Location ' , rest_url ( rest_get_route_for_post ( $ attachment_id ) ) );
2165-
2166- return $ response ;
2185+ return rest_ensure_response ( $ sub_size_data );
21672186 }
21682187
21692188 /**
@@ -2215,9 +2234,11 @@ private static function filter_wp_unique_filename( $filename, $dir, $number, $at
22152234 /**
22162235 * Finalizes an attachment after client-side media processing.
22172236 *
2218- * Triggers the 'wp_generate_attachment_metadata' filter so that
2219- * server-side plugins can process the attachment after all client-side
2220- * operations (upload, thumbnail generation, sideloads) are complete.
2237+ * Applies the sub-size metadata collected from sideload responses in a
2238+ * single metadata update, then triggers the 'wp_generate_attachment_metadata'
2239+ * filter so that server-side plugins can process the attachment after all
2240+ * client-side operations (upload, thumbnail generation, sideloads) are
2241+ * complete.
22212242 *
22222243 * @since 7.1.0
22232244 *
@@ -2237,6 +2258,35 @@ public function finalize_item( WP_REST_Request $request ) {
22372258 $ metadata = array ();
22382259 }
22392260
2261+ // Apply all sub-size metadata collected from sideload responses.
2262+ $ sub_sizes = $ request ['sub_sizes ' ] ?? array ();
2263+
2264+ foreach ( $ sub_sizes as $ sub_size ) {
2265+ $ image_size = $ sub_size ['image_size ' ];
2266+
2267+ if ( 'original ' === $ image_size ) {
2268+ $ metadata ['original_image ' ] = $ sub_size ['file ' ];
2269+ } elseif ( 'scaled ' === $ image_size ) {
2270+ if ( ! empty ( $ sub_size ['original_image ' ] ) ) {
2271+ $ metadata ['original_image ' ] = $ sub_size ['original_image ' ];
2272+ }
2273+ $ metadata ['width ' ] = $ sub_size ['width ' ] ?? 0 ;
2274+ $ metadata ['height ' ] = $ sub_size ['height ' ] ?? 0 ;
2275+ $ metadata ['filesize ' ] = $ sub_size ['filesize ' ] ?? 0 ;
2276+ $ metadata ['file ' ] = $ sub_size ['file ' ] ?? '' ;
2277+ } else {
2278+ $ metadata ['sizes ' ] = $ metadata ['sizes ' ] ?? array ();
2279+
2280+ $ metadata ['sizes ' ][ $ image_size ] = array (
2281+ 'width ' => $ sub_size ['width ' ] ?? 0 ,
2282+ 'height ' => $ sub_size ['height ' ] ?? 0 ,
2283+ 'file ' => $ sub_size ['file ' ] ?? '' ,
2284+ 'mime-type ' => $ sub_size ['mime_type ' ] ?? '' ,
2285+ 'filesize ' => $ sub_size ['filesize ' ] ?? 0 ,
2286+ );
2287+ }
2288+ }
2289+
22402290 /** This filter is documented in wp-admin/includes/image.php */
22412291 $ metadata = apply_filters ( 'wp_generate_attachment_metadata ' , $ metadata , $ attachment_id , 'update ' );
22422292
0 commit comments