Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
32 changes: 32 additions & 0 deletions features/media-import.feature
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,35 @@ Feature: Manage WordPress attachments
"""
Error: Invalid value for <porcelain>: invalid. Expected flag or 'url'.
"""

Scenario: Upload files into a custom directory, relative to ABSPATH, when --destdir flag is applied.
Given download:
| path | url |
| {CACHE_DIR}/large-image.jpg | http://wp-cli.org/behat-data/large-image.jpg |
When I run `wp media import --destdir="foo" {CACHE_DIR}/large-image.jpg --porcelain=url`

Then STDOUT should not contain:
"""
https://example.com/wp-content/uploads/
"""

And STDOUT should contain:
"""
https://example.com/foo/large-image.jpg
"""

Scenario: Upload files into a custom directory, not relative to ABSPATH, when --destdir flag is applied.
Given download:
| path | url |
| {CACHE_DIR}/large-image.jpg | http://wp-cli.org/behat-data/large-image.jpg |
When I run `wp media import --destdir="{RUN_DIR}/foo" {CACHE_DIR}/large-image.jpg --porcelain=url`

Then STDOUT should not contain:
"""
https://example.com/wp-content/uploads/
"""

And STDOUT should contain:
"""
/foo/large-image.jpg
"""
Comment on lines +284 to +314
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The test scenarios have gaps in coverage for the new --destdir feature:

  1. No test for importing multiple files with --destdir to ensure the feature works correctly when processing multiple files in a single command
  2. No test for importing remote files (URLs) with --destdir to verify the feature works for both local and remote files

Consider adding test cases to cover these scenarios.

Copilot uses AI. Check for mistakes.
58 changes: 57 additions & 1 deletion src/Media_Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,13 @@ public function regenerate( $args, $assoc_args = array() ) {
*
* [--skip-copy]
* : If set, media files (local only) are imported to the library but not moved on disk.
* File names will not be run through wp_unique_filename() with this set.
* File names will not be run through wp_unique_filename() with this set. When used, files
* will remain at their current location and will not be copied into any destination directory.
*
* [--destdir=<destdir>]
* : Path to the destination directory for uploaded imported files.
* Can be absolute or relative to ABSPATH. Ignored when used together with --skip-copy, as
* files are not moved on disk in that case.
*
* [--preserve-filetime]
* : Use the file modified time as the post published & modified dates.
Expand Down Expand Up @@ -272,6 +278,7 @@ public function import( $args, $assoc_args = array() ) {
'alt' => '',
'desc' => '',
'post_name' => '',
'destdir' => '',
)
);

Expand Down Expand Up @@ -411,6 +418,12 @@ public function import( $args, $assoc_args = array() ) {
}
wp_update_attachment_metadata( $success, wp_generate_attachment_metadata( $success, $file ) );
} else {

$destdir = Utils\get_flag_value( $assoc_args, 'destdir' );
if ( is_string( $destdir ) && $destdir && ! isset( $custom_upload_dir_filter ) ) {
$custom_upload_dir_filter = $this->add_upload_dir_filter( $destdir );
}

// Deletes the temporary file.
$success = media_handle_sideload( $file_array, $assoc_args['post_id'], $assoc_args['title'], $post_array );
if ( is_wp_error( $success ) ) {
Expand Down Expand Up @@ -468,6 +481,10 @@ public function import( $args, $assoc_args = array() ) {
++$successes;
}

if ( ! empty( $custom_upload_dir_filter ) ) {
$this->remove_upload_dir_filter( $custom_upload_dir_filter );
}
Comment on lines +484 to +486
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The variable $custom_upload_dir_filter is referenced here but may not be defined if the destdir condition was never met in the loop above (line 421-422). This will cause an undefined variable warning in PHP. The variable should be initialized before the foreach loop starts.

Copilot uses AI. Check for mistakes.

// Report the result of the operation
if ( ! Utils\get_flag_value( $assoc_args, 'porcelain' ) ) {
Utils\report_batch_operation_results( $noun, 'import', count( $args ), $successes, $errors );
Expand Down Expand Up @@ -939,6 +956,45 @@ private function remove_image_size_filters( $image_size_filters ) {
}
}

private function add_upload_dir_filter( $upload_dir ) {

$custom_upload_dir_filter = function () use ( $upload_dir ) {
static $custom_upload_dir;
if ( $custom_upload_dir ) {
return $custom_upload_dir;
}

if ( 0 !== strpos( $upload_dir, ABSPATH ) ) {
// $dir is absolute, $upload_dir is (maybe) relative to ABSPATH.
$dir = path_join( ABSPATH, $upload_dir );
} else {
$dir = $upload_dir;
// normalize $upload_dir.
$upload_dir = substr( $upload_dir, strlen( ABSPATH ) );
}

$siteurl = get_option( 'siteurl' );
$url = trailingslashit( $siteurl ) . $upload_dir;

$custom_upload_dir = array(
'path' => $dir,
'url' => $url,
'subdir' => '',
'basedir' => $dir,
'baseurl' => $url,
'error' => false,
);

return $custom_upload_dir;
};

add_filter( 'upload_dir', $custom_upload_dir_filter, PHP_INT_MAX, 0 );
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The add_upload_dir_filter method does not return the filter callback, but the calling code expects it to return a value that can be passed to remove_upload_dir_filter. Add a return statement at the end of the method to return the $custom_upload_dir_filter closure.

Suggested change
add_filter( 'upload_dir', $custom_upload_dir_filter, PHP_INT_MAX, 0 );
add_filter( 'upload_dir', $custom_upload_dir_filter, PHP_INT_MAX, 0 );
return $custom_upload_dir_filter;

Copilot uses AI. Check for mistakes.
}

private function remove_upload_dir_filter( $upload_dir_filter ) {
remove_filter( 'upload_dir', $upload_dir_filter, PHP_INT_MAX );
}

// Update attachment sizes metadata just for a particular intermediate image size.
private function update_attachment_metadata_for_image_size( $id, $new_metadata, $image_size, $metadata ) {

Expand Down