Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 82 additions & 41 deletions lib/media/class-gutenberg-rest-attachments-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,42 @@ public function register_routes(): void {
'callback' => array( $this, 'finalize_item' ),
'permission_callback' => array( $this, 'edit_media_item_permissions_check' ),
'args' => array(
'id' => array(
'id' => array(
'description' => __( 'Unique identifier for the attachment.', 'gutenberg' ),
'type' => 'integer',
),
'sub_sizes' => array(
'description' => __( 'Array of sub-size metadata collected from sideload responses.', 'gutenberg' ),
'type' => 'array',
'default' => array(),
'items' => array(
'type' => 'object',
'properties' => array(
'image_size' => array(
Comment thread
adamsilverstein marked this conversation as resolved.
'type' => 'string',
'required' => true,
),
'width' => array(
'type' => 'integer',
),
'height' => array(
'type' => 'integer',
),
Comment thread
adamsilverstein marked this conversation as resolved.
'file' => array(
'type' => 'string',
),
'mime_type' => array(
'type' => 'string',
Comment thread
adamsilverstein marked this conversation as resolved.
Outdated
),
'filesize' => array(
'type' => 'integer',
Comment thread
adamsilverstein marked this conversation as resolved.
Outdated
),
'original_image' => array(
'type' => 'string',
),
),
),
),
),
),
'allow_batch' => $this->allow_batch,
Expand Down Expand Up @@ -300,6 +332,35 @@ public function finalize_item( WP_REST_Request $request ) {
$metadata = array();
}

// Apply all sub-size metadata collected from sideload responses.
$sub_sizes = $request['sub_sizes'] ?? array();

foreach ( $sub_sizes as $sub_size ) {
$image_size = $sub_size['image_size'];

if ( 'original' === $image_size ) {
$metadata['original_image'] = $sub_size['file'];
} elseif ( 'scaled' === $image_size ) {
if ( ! empty( $sub_size['original_image'] ) ) {
$metadata['original_image'] = $sub_size['original_image'];
}
$metadata['width'] = $sub_size['width'] ?? 0;
$metadata['height'] = $sub_size['height'] ?? 0;
$metadata['filesize'] = $sub_size['filesize'] ?? 0;
$metadata['file'] = $sub_size['file'] ?? '';
} else {
$metadata['sizes'] = $metadata['sizes'] ?? array();

$metadata['sizes'][ $image_size ] = array(
'width' => $sub_size['width'] ?? 0,
'height' => $sub_size['height'] ?? 0,
'file' => $sub_size['file'] ?? '',
'mime-type' => $sub_size['mime_type'] ?? '',
'filesize' => $sub_size['filesize'] ?? 0,
Comment thread
adamsilverstein marked this conversation as resolved.
);
}
}

/**
* Filters the attachment metadata after client-side processing.
*
Expand Down Expand Up @@ -476,59 +537,39 @@ public function sideload_item( WP_REST_Request $request ) {

$image_size = $request['image_size'];

$metadata = wp_get_attachment_metadata( $attachment_id, true );

if ( ! $metadata ) {
$metadata = array();
}
// Build sub-size data to return to the client.
// The client accumulates these and sends them all to the finalize endpoint.
$sub_size_data = array(
'image_size' => $image_size,
);

if ( 'original' === $image_size ) {
$metadata['original_image'] = wp_basename( $path );
$sub_size_data['file'] = wp_basename( $path );
} elseif ( 'scaled' === $image_size ) {
// The current attached file is the original; record it as original_image.
$current_file = get_attached_file( $attachment_id, true );
$metadata['original_image'] = wp_basename( $current_file );
// Record the current attached file as the original.
$current_file = get_attached_file( $attachment_id, true );
$sub_size_data['original_image'] = wp_basename( $current_file );

// Update the attached file to point to the scaled version.
// This writes to _wp_attached_file meta, not _wp_attachment_metadata.
update_attached_file( $attachment_id, $path );

$size = wp_getimagesize( $path );

$metadata['width'] = $size ? $size[0] : 0;
$metadata['height'] = $size ? $size[1] : 0;
$metadata['filesize'] = wp_filesize( $path );
$metadata['file'] = _wp_relative_upload_path( $path );
$sub_size_data['width'] = $size ? $size[0] : 0;
$sub_size_data['height'] = $size ? $size[1] : 0;
$sub_size_data['filesize'] = wp_filesize( $path );
$sub_size_data['file'] = _wp_relative_upload_path( $path );
} else {
$metadata['sizes'] = $metadata['sizes'] ?? array();

$size = wp_getimagesize( $path );

$metadata['sizes'][ $image_size ] = array(
'width' => $size ? $size[0] : 0,
'height' => $size ? $size[1] : 0,
'file' => wp_basename( $path ),
'mime-type' => $type,
'filesize' => wp_filesize( $path ),
);
}

wp_update_attachment_metadata( $attachment_id, $metadata );

$response_request = new WP_REST_Request(
WP_REST_Server::READABLE,
rest_get_route_for_post( $attachment_id )
);

$response_request['context'] = 'edit';

if ( isset( $request['_fields'] ) ) {
$response_request['_fields'] = $request['_fields'];
$sub_size_data['width'] = $size ? $size[0] : 0;
$sub_size_data['height'] = $size ? $size[1] : 0;
$sub_size_data['file'] = wp_basename( $path );
$sub_size_data['mime_type'] = $type;
$sub_size_data['filesize'] = wp_filesize( $path );
}

$response = $this->prepare_item_for_response( get_post( $attachment_id ), $response_request );

$response->header( 'Location', rest_url( rest_get_route_for_post( $attachment_id ) ) );

return $response;
return rest_ensure_response( $sub_size_data );
}
}
3 changes: 2 additions & 1 deletion packages/editor/src/utils/media-finalize/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
*/
import apiFetch from '@wordpress/api-fetch';

export default async function mediaFinalize( id ) {
export default async function mediaFinalize( id, subSizes = [] ) {
await apiFetch( {
path: `/wp/v2/media/${ id }/finalize`,
method: 'POST',
data: { sub_sizes: subSizes },
} );
}
26 changes: 25 additions & 1 deletion packages/editor/src/utils/media-finalize/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,38 @@ describe( 'mediaFinalize', () => {
jest.clearAllMocks();
} );

it( 'should call the finalize endpoint with the correct path and method', async () => {
it( 'should call the finalize endpoint with the correct path, method, and sub_sizes', async () => {
apiFetch.mockResolvedValue( {} );

const subSizes = [
{
image_size: 'thumbnail',
width: 150,
height: 150,
file: 'image-150x150.jpg',
mime_type: 'image/jpeg',
filesize: 5000,
},
];

await mediaFinalize( 123, subSizes );

expect( apiFetch ).toHaveBeenCalledWith( {
path: '/wp/v2/media/123/finalize',
method: 'POST',
data: { sub_sizes: subSizes },
} );
} );

it( 'should send empty sub_sizes array by default', async () => {
apiFetch.mockResolvedValue( {} );

await mediaFinalize( 123 );

expect( apiFetch ).toHaveBeenCalledWith( {
path: '/wp/v2/media/123/finalize',
method: 'POST',
data: { sub_sizes: [] },
} );
} );

Expand Down
4 changes: 4 additions & 0 deletions packages/media-utils/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ Private @wordpress/media-utils APIs.

Undocumented declaration.

### SubSizeData

Undocumented declaration.

### transformAttachment

Transforms an attachment object from the REST API shape into the shape expected by the block editor and other consumers.
Expand Down
2 changes: 1 addition & 1 deletion packages/media-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ export { validateFileSize } from './utils/validate-file-size';
export { validateMimeType } from './utils/validate-mime-type';
export { validateMimeTypeForUser } from './utils/validate-mime-type-for-user';

export type { Attachment, RestAttachment } from './utils/types';
export type { Attachment, RestAttachment, SubSizeData } from './utils/types';

export { privateApis } from './private-apis';
19 changes: 12 additions & 7 deletions packages/media-utils/src/utils/sideload-media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ import { __, sprintf } from '@wordpress/i18n';
* Internal dependencies
*/
import type {
OnChangeHandler,
OnErrorHandler,
CreateSideloadFile,
RestAttachment,
SubSizeData,
} from './types';
import { sideloadToServer } from './sideload-to-server';
import { UploadError } from './upload-error';

const noop = () => {};

type OnSubSizeHandler = ( subSize: SubSizeData ) => void;

interface SideloadMediaArgs {
// Additional data to include in the request.
additionalData?: CreateSideloadFile;
Expand All @@ -26,39 +28,42 @@ interface SideloadMediaArgs {
attachmentId: RestAttachment[ 'id' ];
// Function called when an error happens.
onError?: OnErrorHandler;
// Function called each time a file or a temporary representation of the file is available.
onFileChange?: OnChangeHandler;
// Function called when the sideload completes with sub-size data.
onSuccess?: OnSubSizeHandler;
// Abort signal.
signal?: AbortSignal;
}

/**
* Uploads a file to the server without creating an attachment.
*
* Returns sub-size data instead of a full attachment. The client
* accumulates this data and sends it to the finalize endpoint.
*
* @param $0 Parameters object passed to the function.
* @param $0.file Media File to Save.
* @param $0.attachmentId Parent attachment ID.
* @param $0.additionalData Additional data to include in the request.
* @param $0.signal Abort signal.
* @param $0.onFileChange Function called each time a file or a temporary representation of the file is available.
* @param $0.onSuccess Function called when the sideload completes with sub-size data.
* @param $0.onError Function called when an error happens.
*/
export async function sideloadMedia( {
file,
attachmentId,
additionalData = {},
signal,
onFileChange,
onSuccess,
onError = noop,
}: SideloadMediaArgs ) {
try {
const attachment = await sideloadToServer(
const subSizeData = await sideloadToServer(
file,
attachmentId,
additionalData,
signal
);
onFileChange?.( [ attachment ] );
onSuccess?.( subSizeData );
} catch ( error ) {
let message;
if ( error instanceof Error ) {
Expand Down
25 changes: 13 additions & 12 deletions packages/media-utils/src/utils/sideload-to-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,29 @@ import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import type { CreateSideloadFile, RestAttachment } from './types';
import type { CreateSideloadFile, RestAttachment, SubSizeData } from './types';
import { flattenFormData } from './flatten-form-data';
import { transformAttachment } from './transform-attachment';

/**
* Uploads a file to the server without creating an attachment.
*
* Returns lightweight sub-size data instead of a full attachment.
* The client accumulates these responses and sends them to the
* finalize endpoint.
*
* @param file Media File to Save.
* @param attachmentId Parent attachment ID.
* @param additionalData Additional data to include in the request.
* @param signal Abort signal.
*
* @return The saved attachment.
* @return Sub-size data for the uploaded file.
*/
export async function sideloadToServer(
file: File,
attachmentId: RestAttachment[ 'id' ],
additionalData: CreateSideloadFile = {},
signal?: AbortSignal
) {
): Promise< SubSizeData > {
// Create upload payload.
const data = new FormData();
data.append( 'file', file, file.name || file.type.replace( '/', '.' ) );
Expand All @@ -37,12 +40,10 @@ export async function sideloadToServer(
);
}

return transformAttachment(
await apiFetch< RestAttachment >( {
path: `/wp/v2/media/${ attachmentId }/sideload`,
body: data,
method: 'POST',
signal,
} )
);
return apiFetch< SubSizeData >( {
path: `/wp/v2/media/${ attachmentId }/sideload`,
body: data,
method: 'POST',
signal,
} );
}
18 changes: 14 additions & 4 deletions packages/media-utils/src/utils/test/sideload-media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,27 @@ describe( 'sideloadMedia', () => {
jest.clearAllMocks();
} );

it( 'should sideload to server', async () => {
it( 'should sideload to server and call onSuccess with sub-size data', async () => {
const mockSubSizeData = {
image_size: 'thumbnail',
width: 150,
height: 150,
file: 'test-150x150.jpeg',
mime_type: 'image/jpeg',
filesize: 5000,
};
( sideloadToServer as jest.Mock ).mockResolvedValue( mockSubSizeData );

const onError = jest.fn();
const onFileChange = jest.fn();
const onSuccess = jest.fn();
await sideloadMedia( {
file: imageFile,
attachmentId: 1,
onError,
onFileChange,
onSuccess,
} );

expect( sideloadToServer ).toHaveBeenCalled();
expect( onFileChange ).toHaveBeenCalled();
expect( onSuccess ).toHaveBeenCalledWith( mockSubSizeData );
} );
} );
Loading
Loading