Conversation
Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com>
wp media replace subcommand
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
Adds a new wp media replace <attachment-id> <file> subcommand to swap an existing attachment’s underlying file while keeping the attachment post/identity intact, aligning WP-CLI’s media tooling closer to common “replace media” workflows.
Changes:
- Introduces
Media_Command::replace()to sideload a new file, update_wp_attached_file, update MIME type, and regenerate attachment metadata. - Registers the new
media replacecommand incomposer.json. - Adds Behat coverage for local/URL replacement, porcelain output, basic metadata preservation, and error cases.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
src/Media_Command.php |
Implements the new replace() subcommand and its runtime behavior (sideload, cleanup, metadata refresh, output). |
composer.json |
Exposes media replace through WP-CLI’s command registration list. |
features/media-replace.feature |
Adds acceptance tests for the new subcommand’s primary behaviors and failures. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| $uploaded = wp_handle_sideload( $file_array, array( 'test_form' => false ) ); | ||
|
|
||
| if ( isset( $uploaded['error'] ) ) { |
There was a problem hiding this comment.
On wp_handle_sideload() failure, the temporary file created by make_copy()/download_url() is not cleaned up before exiting. This can leak files in the system temp dir; explicitly unlink the temp file (when it exists) before calling WP_CLI::error().
| if ( isset( $uploaded['error'] ) ) { | |
| if ( isset( $uploaded['error'] ) ) { | |
| if ( isset( $tempfile ) && is_string( $tempfile ) && file_exists( $tempfile ) ) { | |
| unlink( $tempfile ); | |
| } |
| ) | ||
| ); | ||
| if ( is_wp_error( $updated ) ) { | ||
| WP_CLI::warning( | ||
| sprintf( 'Failed to update MIME type for attachment %d: %s', $attachment_id, $updated->get_error_message() ) |
There was a problem hiding this comment.
wp_update_post() is called without setting $wp_error=true, so it will return 0 on failure rather than a WP_Error. The current is_wp_error() check will never fire, meaning MIME type update failures can be silently missed; pass true as the second parameter and/or handle a falsy return value.
| ) | |
| ); | |
| if ( is_wp_error( $updated ) ) { | |
| WP_CLI::warning( | |
| sprintf( 'Failed to update MIME type for attachment %d: %s', $attachment_id, $updated->get_error_message() ) | |
| ), | |
| true | |
| ); | |
| if ( false === $updated || is_wp_error( $updated ) ) { | |
| $message = is_wp_error( $updated ) ? $updated->get_error_message() : 'Unknown error.'; | |
| WP_CLI::warning( | |
| sprintf( 'Failed to update MIME type for attachment %d: %s', $attachment_id, $message ) |
|
|
||
| // Generate and update new attachment metadata. | ||
| $new_metadata = wp_generate_attachment_metadata( $attachment_id, $new_file_path ); | ||
| wp_update_attachment_metadata( $attachment_id, $new_metadata ); |
There was a problem hiding this comment.
The code replaces attachment metadata unconditionally, but wp_generate_attachment_metadata() can return an empty array on failure (see existing regeneration handling). Consider validating the return value (and halting or preserving old metadata on failure) to avoid wiping existing metadata for the attachment.
| wp_update_attachment_metadata( $attachment_id, $new_metadata ); | |
| if ( is_array( $new_metadata ) && ! empty( $new_metadata ) ) { | |
| wp_update_attachment_metadata( $attachment_id, $new_metadata ); | |
| } else { | |
| WP_CLI::warning( | |
| sprintf( | |
| "Failed to generate new attachment metadata for attachment ID %d. Existing metadata has been preserved.", | |
| $attachment_id | |
| ) | |
| ); | |
| } |
| && false !== $old_fullsizepath | ||
| && is_array( $old_metadata ) | ||
| ) { | ||
| $this->remove_old_images( $old_metadata, $old_fullsizepath, array() ); |
There was a problem hiding this comment.
Old intermediate sizes are deleted, but the previous full-size file itself is never removed, which will leave an orphaned file in uploads after every replacement. Consider deleting the old attached file (and, for big-image scaling on WP 5.3+, potentially the original image too) unless explicitly opted out via a flag.
| $this->remove_old_images( $old_metadata, $old_fullsizepath, array() ); | |
| $this->remove_old_images( $old_metadata, $old_fullsizepath, array() ); | |
| // Also delete the previous full-size file itself to avoid leaving an orphan. | |
| if ( $old_fullsizepath !== $new_file_path && file_exists( $old_fullsizepath ) ) { | |
| @unlink( $old_fullsizepath ); | |
| } | |
| // For big-image scaling (WP 5.3+), delete the original image if present in metadata. | |
| if ( ! empty( $old_metadata['original_image'] ) && ! empty( $old_metadata['file'] ) ) { | |
| $uploads = wp_get_upload_dir(); | |
| if ( ! empty( $uploads['basedir'] ) ) { | |
| $dirname = dirname( $old_metadata['file'] ); | |
| $original_image_rel = ( '.' === $dirname || '/' === $dirname ) ? $old_metadata['original_image'] : $dirname . '/' . $old_metadata['original_image']; | |
| $original_image_abspath = $uploads['basedir'] . '/' . $original_image_rel; | |
| if ( $original_image_abspath !== $new_file_path && file_exists( $original_image_abspath ) ) { | |
| @unlink( $original_image_abspath ); | |
| } | |
| } | |
| } |
| WP_CLI::log( | ||
| sprintf( "Replaced file for attachment ID %d with '%s'.", $attachment_id, $orig_filename ) | ||
| ); | ||
| Utils\report_batch_operation_results( 'image', 'replace', 1, 1, 0 ); |
There was a problem hiding this comment.
The success reporting is hard-coded to use the noun 'image' ("Success: Replaced 1 of 1 images."), but replace() currently allows any attachment post type. Either restrict replacements to images, or adjust the reporting noun/message to be accurate for non-image attachments.
| Utils\report_batch_operation_results( 'image', 'replace', 1, 1, 0 ); | |
| Utils\report_batch_operation_results( 'attachment', 'replace', 1, 1, 0 ); |
| if ( ! Utils\get_flag_value( $assoc_args, 'skip-delete' ) | ||
| && false !== $old_fullsizepath | ||
| && is_array( $old_metadata ) | ||
| ) { | ||
| $this->remove_old_images( $old_metadata, $old_fullsizepath, array() ); |
There was a problem hiding this comment.
The new --skip-delete behavior is introduced here, but the feature specs don't currently cover the flag (no scenario asserts old thumbnails are preserved when --skip-delete is used). Adding a Behat scenario would help prevent regressions.
Adds a
wp media replace <attachment-id> <file>subcommand to swap the underlying file of an existing attachment while preserving its identity (ID, title, alt text, caption, etc.).Changes
src/Media_Command.php— Newreplace()method:import)wp_handle_sideload()_wp_attached_filemeta, and regenerates attachment metadata--skip-delete— skip deletion of old thumbnail files--porcelain— output only the attachment ID (useful for scripting)composer.json— Registersmedia replaceinextra.commandsfeatures/media-replace.feature— Behat tests covering: local file, URL, porcelain mode, metadata preservation, and error cases (invalid ID, missing file)Usage
Original prompt
📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.