Skip to content

Commit 658fa47

Browse files
REST API: Move sideload metadata writing to the finalize endpoint
Backport of Gutenberg PR #75888. Eliminate the read-modify-write race between concurrent sideloads for the same attachment by no longer writing attachment metadata in the sideload endpoint. Instead, sideload returns lightweight sub-size data (dimensions, filename, filesize) which the client accumulates and passes to the finalize endpoint, which writes all collected sub-sizes in a single metadata update. This matches how core generates sub-sizes (one metadata write after all sizes exist) and replaces the earlier per-attachment locking approach that the merged Gutenberg PR ultimately abandoned.
1 parent bea7d26 commit 658fa47

2 files changed

Lines changed: 277 additions & 47 deletions

File tree

src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php

Lines changed: 93 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)