Skip to content

Add wp media replace subcommand#240

Draft
Copilot wants to merge 2 commits intomainfrom
copilot/add-media-replace-subcommand
Draft

Add wp media replace subcommand#240
Copilot wants to merge 2 commits intomainfrom
copilot/add-media-replace-subcommand

Conversation

Copy link
Contributor

Copilot AI commented Mar 19, 2026

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 — New replace() method:

    • Accepts a local path or URL as the replacement file (same handling as import)
    • Sideloads the new file into the uploads directory via wp_handle_sideload()
    • Deletes old thumbnails, updates MIME type, refreshes _wp_attached_file meta, and regenerates attachment metadata
    • --skip-delete — skip deletion of old thumbnail files
    • --porcelain — output only the attachment ID (useful for scripting)
  • composer.json — Registers media replace in extra.commands

  • features/media-replace.feature — Behat tests covering: local file, URL, porcelain mode, metadata preservation, and error cases (invalid ID, missing file)

Usage

# Replace with a local file
$ wp media replace 123 ~/new-image.jpg
Replaced file for attachment ID 123 with '/home/user/new-image.jpg'.
Success: Replaced 1 of 1 images.

# Replace with a remote URL, return ID only
$ wp media replace 123 'http://example.com/image.jpg' --porcelain
123
Original prompt

This section details on the original issue you should resolve

<issue_title>Feature request: media replace subcommand</issue_title>
<issue_description>Hi,

I would like to suggest a new feature (subcommand) that would allow to seamlessly replace media file of existing attachment: media replace <file> <attachment_id>.

For the time being, I use Enable Media Replace whenever I need to replace media file, but it would be great to have a command line equivalent.</issue_description>

Comments on the Issue (you are @copilot in this section)


📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.

@github-actions github-actions bot added command:media Related to 'media' command help-wanted Extra attention is needed state:unconfirmed labels Mar 19, 2026
Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com>
Copilot AI changed the title [WIP] Add media replace subcommand for seamless media replacement Add wp media replace subcommand Mar 19, 2026
Copilot AI requested a review from swissspidy March 19, 2026 23:00
@codecov
Copy link

codecov bot commented Mar 19, 2026

Codecov Report

❌ Patch coverage is 81.13208% with 10 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/Media_Command.php 81.13% 10 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 replace command in composer.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'] ) ) {
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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().

Suggested change
if ( isset( $uploaded['error'] ) ) {
if ( isset( $uploaded['error'] ) ) {
if ( isset( $tempfile ) && is_string( $tempfile ) && file_exists( $tempfile ) ) {
unlink( $tempfile );
}

Copilot uses AI. Check for mistakes.
Comment on lines +656 to +660
)
);
if ( is_wp_error( $updated ) ) {
WP_CLI::warning(
sprintf( 'Failed to update MIME type for attachment %d: %s', $attachment_id, $updated->get_error_message() )
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
)
);
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 )

Copilot uses AI. Check for mistakes.

// 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 );
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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
)
);
}

Copilot uses AI. Check for mistakes.
&& false !== $old_fullsizepath
&& is_array( $old_metadata )
) {
$this->remove_old_images( $old_metadata, $old_fullsizepath, array() );
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
$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 );
}
}
}

Copilot uses AI. Check for mistakes.
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 );
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
Utils\report_batch_operation_results( 'image', 'replace', 1, 1, 0 );
Utils\report_batch_operation_results( 'attachment', 'replace', 1, 1, 0 );

Copilot uses AI. Check for mistakes.
Comment on lines +644 to +648
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() );
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

command:media Related to 'media' command help-wanted Extra attention is needed state:unconfirmed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: media replace subcommand

3 participants