Skip to content

Commit 02cb9ec

Browse files
CopilotswissspidyCopilot
authored
Add STDIN support to wp media import (#221)
* Initial plan * Implement STDIN support for wp media import command Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> * Simplify STDIN file naming logic and update class documentation Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> * Add clarifying comment about WordPress slug sanitization in test Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> * Fix STDIN file type detection using mime_content_type Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> * Fix test to use correct field name post_name instead of name Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> Agent-Logs-Url: https://github.com/wp-cli/media-command/sessions/4e4b402c-3f76-450c-abcd-1f37785681e8 * Fix STDIN imports to use generated filename for title instead of '-' Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> Agent-Logs-Url: https://github.com/wp-cli/media-command/sessions/0f687dc2-b2b2-4363-8524-c9f12cb1338a * Update src/Media_Command.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update features/media-import.feature Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix double extension issue by stripping extension from file_name before appending Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> Agent-Logs-Url: https://github.com/wp-cli/media-command/sessions/25fc9db3-37e7-41b5-aef7-c41b4b633a8c * Stream STDIN directly to temp file to avoid memory issues with large files Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> Agent-Logs-Url: https://github.com/wp-cli/media-command/sessions/2d71c7b3-1844-42e8-ab88-e67d3b17e044 * Update test expectation for post_name after fixing double extension bug Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> Agent-Logs-Url: https://github.com/wp-cli/media-command/sessions/125cfbc3-1470-4b02-98d1-cadf88177d9c --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> Co-authored-by: Pascal Birchler <pascalb@google.com> Co-authored-by: Pascal Birchler <pascal.birchler@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 3c223c6 commit 02cb9ec

2 files changed

Lines changed: 156 additions & 27 deletions

File tree

features/media-import.feature

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,51 @@ Feature: Manage WordPress attachments
287287
Error: Invalid value for <porcelain>: invalid. Expected flag or 'url'.
288288
"""
289289

290+
Scenario: Import media from STDIN
291+
Given download:
292+
| path | url |
293+
| {CACHE_DIR}/codeispoetry.png | http://wp-cli.org/behat-data/codeispoetry.png |
294+
295+
When I run `cat {CACHE_DIR}/codeispoetry.png | wp media import - --title="From STDIN" --porcelain`
296+
Then save STDOUT as {ATTACHMENT_ID}
297+
298+
When I run `wp post get {ATTACHMENT_ID} --field=title`
299+
Then STDOUT should be:
300+
"""
301+
From STDIN
302+
"""
303+
304+
Scenario: Import media from STDIN with file_name
305+
Given download:
306+
| path | url |
307+
| {CACHE_DIR}/codeispoetry.png | http://wp-cli.org/behat-data/codeispoetry.png |
308+
309+
When I run `cat {CACHE_DIR}/codeispoetry.png | wp media import - --file_name=my-image.png --porcelain`
310+
Then save STDOUT as {ATTACHMENT_ID}
311+
312+
When I run `wp post get {ATTACHMENT_ID} --field=post_name`
313+
Then STDOUT should be:
314+
"""
315+
my-image
316+
"""
317+
318+
When I run `wp post meta get {ATTACHMENT_ID} _wp_attached_file`
319+
Then STDOUT should contain:
320+
"""
321+
my-image.png
322+
"""
323+
And STDOUT should not contain:
324+
"""
325+
my-image.png.png
326+
"""
327+
Scenario: Fail to import from STDIN when no input provided
328+
When I try `wp media import - </dev/null`
329+
Then STDERR should contain:
330+
"""
331+
Warning: Unable to import file from STDIN. Reason: No input provided.
332+
"""
333+
And the return code should be 1
334+
290335
Scenario: Upload files into a custom directory, relative to ABSPATH, when --destination-dir flag is applied.
291336
Given download:
292337
| path | url |

src/Media_Command.php

Lines changed: 111 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
* Imported file '/home/person/Downloads/image.png' as attachment ID 1753 and attached to post 123 as featured image.
2222
* Success: Imported 1 of 1 images.
2323
*
24+
* # Import an image from STDIN.
25+
* $ curl http://example.com/image.jpg | wp media import -
26+
* Imported file 'STDIN' as attachment ID 1754.
27+
* Success: Imported 1 of 1 items.
28+
*
2429
* # List all registered image sizes
2530
* $ wp media image-size
2631
* +---------------------------+-------+--------+-------+
@@ -377,6 +382,7 @@ public function prune( $args, $assoc_args = array() ) {
377382
* : Path to file or files to be imported. Supports the glob(3) capabilities of the current shell.
378383
* If file is recognized as a URL (for example, with a scheme of http or ftp), the file will be
379384
* downloaded to a temp file before being sideloaded.
385+
* Use '-' to read file data from STDIN.
380386
*
381387
* [--post_id=<post_id>]
382388
* : ID of the post to attach the imported files to.
@@ -453,6 +459,11 @@ public function prune( $args, $assoc_args = array() ) {
453459
* $ wp media import http://s.wordpress.org/style/images/wp-header-logo.png --porcelain | xargs -I {} wp post list --post__in={} --field=url --post_type=attachment
454460
* http://wordpress-develop.dev/wp-header-logo/
455461
*
462+
* # Import an image from STDIN.
463+
* $ curl http://example.com/image.jpg | wp media import - --title="From STDIN"
464+
* Imported file 'STDIN' as attachment ID 1756.
465+
* Success: Imported 1 of 1 items.
466+
*
456467
* @param string[] $args Positional arguments.
457468
* @param array{post_id?: string, post_name?: string, file_name?: string, title?: string, caption?: string, alt?: string, desc?: string, 'skip-copy'?: bool, 'destination-dir'?: string, 'preserve-filetime'?: bool, featured_image?: bool, porcelain?: bool|string} $assoc_args Associative arguments.
458469
* @return void
@@ -514,41 +525,109 @@ public function import( $args, $assoc_args = array() ) {
514525
Utils\wp_clear_object_cache();
515526
}
516527

517-
// phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- parse_url will only be used in absence of wp_parse_url.
518-
$is_file_remote = function_exists( 'wp_parse_url' ) ? wp_parse_url( $file, PHP_URL_HOST ) : parse_url( $file, PHP_URL_HOST );
519-
$orig_filename = $file;
520-
$file_time = '';
528+
// Handle STDIN input
529+
if ( '-' === $file ) {
530+
if ( ! Utils\has_stdin() ) {
531+
WP_CLI::warning( 'Unable to import file from STDIN. Reason: No input provided.' );
532+
++$errors;
533+
continue;
534+
}
521535

522-
if ( empty( $is_file_remote ) ) {
523-
if ( ! file_exists( $file ) ) {
524-
WP_CLI::warning( "Unable to import file '$file'. Reason: File doesn't exist." );
536+
// Read from STDIN and save to a temporary file
537+
// Stream STDIN directly to temp file to avoid memory issues with large files
538+
$stdin_handle = fopen( 'php://stdin', 'rb' );
539+
if ( false === $stdin_handle ) {
540+
WP_CLI::warning( 'Unable to import file from STDIN. Reason: Could not open STDIN.' );
525541
++$errors;
526542
continue;
527543
}
528-
if ( Utils\get_flag_value( $assoc_args, 'skip-copy' ) ) {
529-
$tempfile = $file;
530-
} else {
531-
$tempfile = $this->make_copy( $file );
544+
545+
// Create a temporary file to store STDIN content
546+
$tempfile = wp_tempnam( 'wp-media-import-' );
547+
if ( false === $tempfile ) {
548+
fclose( $stdin_handle );
549+
WP_CLI::warning( 'Unable to import file from STDIN. Reason: Could not create temporary file.' );
550+
++$errors;
551+
continue;
532552
}
533-
$name = Path::basename( $file );
534553

535-
if ( Utils\get_flag_value( $assoc_args, 'preserve-filetime' ) ) {
536-
$file_time = @filemtime( $file );
554+
$temp_handle = fopen( $tempfile, 'wb' );
555+
if ( false === $temp_handle ) {
556+
fclose( $stdin_handle );
557+
WP_CLI::warning( 'Unable to import file from STDIN. Reason: Could not write to temporary file.' );
558+
++$errors;
559+
continue;
537560
}
538-
} else {
539-
$tempfile = download_url( $file );
540-
if ( is_wp_error( $tempfile ) ) {
541-
WP_CLI::warning(
542-
sprintf(
543-
"Unable to import file '%s'. Reason: %s",
544-
$file,
545-
implode( ', ', $tempfile->get_error_messages() )
546-
)
547-
);
561+
562+
// Stream data from STDIN to temp file
563+
$bytes_copied = stream_copy_to_stream( $stdin_handle, $temp_handle );
564+
fclose( $stdin_handle );
565+
fclose( $temp_handle );
566+
567+
if ( false === $bytes_copied || 0 === $bytes_copied ) {
568+
WP_CLI::warning( 'Unable to import file from STDIN. Reason: No input provided.' );
548569
++$errors;
549570
continue;
550571
}
551-
$name = (string) strtok( Path::basename( $file ), '?' );
572+
573+
// Determine file extension from content
574+
$mimetype = mime_content_type( $tempfile );
575+
576+
// Map MIME type to extension
577+
$ext = '';
578+
if ( $mimetype && function_exists( 'wp_get_mime_types' ) ) {
579+
$mime_types = wp_get_mime_types();
580+
foreach ( $mime_types as $exts => $mime ) {
581+
if ( $mime === $mimetype ) {
582+
$ext_array = explode( '|', $exts );
583+
$ext = '.' . $ext_array[0];
584+
break;
585+
}
586+
}
587+
}
588+
589+
// Generate filename with proper extension
590+
$name = 'stdin-' . time() . $ext;
591+
592+
$orig_filename = 'STDIN';
593+
$file_time = '';
594+
} else {
595+
// phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- parse_url will only be used in absence of wp_parse_url.
596+
$is_file_remote = function_exists( 'wp_parse_url' ) ? wp_parse_url( $file, PHP_URL_HOST ) : parse_url( $file, PHP_URL_HOST );
597+
$orig_filename = $file;
598+
$file_time = '';
599+
600+
if ( empty( $is_file_remote ) ) {
601+
if ( ! file_exists( $file ) ) {
602+
WP_CLI::warning( "Unable to import file '$file'. Reason: File doesn't exist." );
603+
++$errors;
604+
continue;
605+
}
606+
if ( Utils\get_flag_value( $assoc_args, 'skip-copy' ) ) {
607+
$tempfile = $file;
608+
} else {
609+
$tempfile = $this->make_copy( $file );
610+
}
611+
$name = Path::basename( $file );
612+
613+
if ( Utils\get_flag_value( $assoc_args, 'preserve-filetime' ) ) {
614+
$file_time = @filemtime( $file );
615+
}
616+
} else {
617+
$tempfile = download_url( $file );
618+
if ( is_wp_error( $tempfile ) ) {
619+
WP_CLI::warning(
620+
sprintf(
621+
"Unable to import file '%s'. Reason: %s",
622+
$file,
623+
implode( ', ', $tempfile->get_error_messages() )
624+
)
625+
);
626+
++$errors;
627+
continue;
628+
}
629+
$name = (string) strtok( Path::basename( $file ), '?' );
630+
}
552631
}
553632

554633
if ( ! empty( $assoc_args['file_name'] ) ) {
@@ -594,7 +673,9 @@ public function import( $args, $assoc_args = array() ) {
594673
}
595674

596675
if ( empty( $post_array['post_title'] ) ) {
597-
$post_array['post_title'] = preg_replace( '/\.[^.]+$/', '', Path::basename( $file ) );
676+
// For STDIN imports, use the generated filename instead of the '-' argument
677+
$title_source = ( '-' === $file ) ? $name : $file;
678+
$post_array['post_title'] = preg_replace( '/\.[^.]+$/', '', Path::basename( $title_source ) );
598679
}
599680

600681
if ( Utils\get_flag_value( $assoc_args, 'skip-copy' ) ) {
@@ -2005,7 +2086,10 @@ private function get_image_name( $basename, $slug ) {
20052086

20062087
$extension = pathinfo( $basename, PATHINFO_EXTENSION );
20072088

2008-
return $slug . '.' . $extension;
2089+
// Strip any extension from the slug to prevent double extensions
2090+
$slug_without_ext = preg_replace( '/\.[^.]+$/', '', $slug );
2091+
2092+
return $slug_without_ext . '.' . $extension;
20092093
}
20102094

20112095
/**

0 commit comments

Comments
 (0)