web-dev-qa-db-ja.com

Media Libraryへの画像のアップロードがメモリ不足で失敗する

CRON経由でスケジュールされている次のスクリプトを使用して、サーバー上のフォルダーからメディアライブラリに毎日何百もの画像をアップロードしようとしています。

<?php
require_once('../../../../public/wordpress/wp-load.php');
require_once('../../../../public/wordpress/wp-admin/includes/image.php');

function importImage($imagePath, $postId)
{
    $succeededFileCount = 0;
    $failedFileCount = 0;
    $files = scandir($imagePath);

    foreach ($files as $file) {
        if (in_array($file, ['.', '..'])) {
            continue;
        }

        $newPath = $imagePath . "/" . $file;
        $filePath = realpath($newPath);

        if (is_dir($newPath) && $item != '.' && $item != '..' && $item != 'failed_files') {
            importImage($newPath, $postId);
        } elseif ($item != '.' && $item != '..' && $item != 'failed_files') {
            $filename = basename($file);
            $uploadFile = wp_upload_bits($filename, null, file_get_contents($filePath));
            $wp_upload_dir = wp_upload_dir();

            if (! $uploadFile['error']) {
                $fileType = wp_check_filetype($filename, null);
                $attachment = [
                    'guid' => $wp_upload_dir['url'] . '/' . basename( $filename ),
                    'post_mime_type' => $fileType['type'],
                    'post_parent' => $postId,
                    'post_title' => preg_replace('/\.[^.]+$/', '', $filename),
                    'post_content' => '',
                    'post_status' => 'inherit'
                ];
                $attachmentId = wp_insert_attachment($attachment, $uploadFile['file'], $postId);

                if (! is_wp_error($attachmentId)) {
                    $attachmentData = wp_generate_attachment_metadata($attachmentId, $uploadFile['file']);
                    wp_update_attachment_metadata($attachmentId, $attachmentData);
                }
            } else {
                echo '<span style="color: red; font-weight: bold;">Error: ' . $uploadFile['error'] . '</span>';
            }


            if ($attachmentId > 0) {
                $succeededFileCount++;
                echo '<span style="color: green; font-weight: normal;">File import succeeded: ' . $filePath . "</span><br />";

                if (! unlink($filePath)) {
                    echo '<span style="color: red; font-weight: bold;">Unable to delete file ' . $filePath . " after import.</span><br />";
                }

                $page = get_post($postId);

                if ($page->post_content) {
                    $content = $page->post_content;
                    $start = strpos($content, "[gallery ") + strlen("[gallery ");
                    $end = strpos(substr($content, $start), "]");
                    $shortcode = substr($content, $start, $end);
                    $attrs = shortcode_parse_atts($shortcode);
                    $attrs["ids"] .= "," . $attachmentId;
                    $tempIds = explode(",", $attrs["ids"]);
                    $tempIds = array_filter($tempIds);
                    rsort($tempIds);
                    $attrs["ids"] = implode(",", $tempIds);
                    $shortcode = "";

                    foreach ($attrs as $key => $value) {
                        if (strlen($shortcode) > 0) {
                            $shortcode .= " ";
                        }

                        $shortcode .= $key . "=\"" . $value . "\"";
                    }

                    $newContent = substr($content, 0, $start);
                    $newContent .= $shortcode;
                    $newContent .= substr($content, $start + $end, strlen($content));
                    $page->post_content = $newContent;
                    wp_update_post($page);
                }
            }
        }
    }

    echo $succeededFileCount . " files uploaded and imported successfully. <br />";
    echo $failedFileCount . " files failed to uploaded or import successfully.";
}

get_header();

if (get_option('rmm_image_importer_key') != urldecode($_GET['key'])) {
    echo '<div id="message" class="error">';
    echo "<p><strong>Incorrect authentication key: you are not allowed to import images into this site.</strong></p></div>";
} else {
    echo '<br /><br />';
    $mtime = microtime();
    $mtime = explode(" ", $mtime);
    $mtime = $mtime[1] + $mtime[0];
    $starttime = $mtime;
    $dataset = get_option('rmm_image_importer_settings');
    if (is_array($dataset)) {
        foreach ($dataset as $data) {
            if (isset($data['folder'])
                || isset($data['page'])) {
?>
    <h2>Import from folder: <?php echo $data['folder']; ?></h2>
    <p>
<?php
                importImage(realpath(str_replace('//', '/', ABSPATH . '../../' . $data['folder'])), $data['page']); ?>
    </p>
<?php
            }
        }
    }
    $mtime = microtime();
    $mtime = explode(" ", $mtime);
    $mtime = $mtime[1] + $mtime[0];
    $endtime = $mtime;
    $totaltime = ($endtime - $starttime);
    echo 'Files imported to media library over ' . $totaltime . ' seconds.<br /><br />';
}

get_footer();

問題は、私が何をしても、このエラーが発生した2つの画像の後でスクリプトが失敗することです。

致命的なエラー:1841行目の/home/forge/morselandcompany.com/public/wordpress/wp-includes/wp-db.phpで許容メモリサイズ1073741824バイトを使い果たしました(28672バイトを割り当てようとしました)

PHPと同様に、ワードプレスでもメモリ制限を1024Mに設定しています。このスクリプトが128M以上必要な理由はわかりません。このスクリプトを正しく機能させるためにどのように最適化できますか? (平均画像サイズは800kBです。)

BlackFire.ioを使った初期デバッグでは、以下のメモリ消費量が示唆されています。 - wpdb-> query:3.2MB、31回呼び出されました - mysqli_fetch_object:1.89MB、595回呼び出されました - 全体として、blackfireはこのスクリプトを実行するために8MB以上が必要であることを示唆しています。

私はすべてのプラグインを無効にしてテストしたところ、同じ結果になりました。

実行しています - PHP 7.1
- Ubuntu 16.04
- DigitalOcean VPS(1 CPU、1 GB RAM)
- ワードプレス4.8
- NGINX 1.11.5

助けてくれてありがとう!

更新: / get_postとwp_update_postに関連するメモリリークの解決策を他の人が利用できるように完全性のために、上記の問題を解決したコードを完成させました。ご覧のとおり、解決策は、メモリリークの原因となる2つのWPメソッドに頼るのではなく、$ wpdbを使用して自分のクエリを作成することでした。

<?php

require_once('../../../../public/wordpress/wp-load.php');
require_once(ABSPATH . 'wp-admin/includes/media.php');
require_once(ABSPATH . 'wp-admin/includes/file.php');
require_once(ABSPATH . 'wp-admin/includes/image.php');

function importImage($imagePath, $postId)
{
    $succeededFileCount = 0;
    $failedFileCount = 0;
    $files = scandir($imagePath);

    foreach ($files as $file) {
        if (in_array($file, ['.', '..'])) {
            continue;
        }

        $newPath = $imagePath . "/" . $file;
        $filePath = realpath($newPath);

        if (is_dir($newPath) && $file != 'failed_files') {
            importImage($newPath, $postId);
        } elseif ($file != 'failed_files') {
            $webPath = str_replace($_SERVER['DOCUMENT_ROOT'], '', $imagePath);
            $imageUrl = str_replace('/wordpress', '', get_site_url(null, "{$webPath}/" . urlencode($file)));
            $imageUrl = str_replace(':8000', '', $imageUrl);
            $attachmentId = media_sideload_image($imageUrl, 0, '', 'id');

            if ($attachmentId > 0) {
                $succeededFileCount++;
                echo '<span style="color: green; font-weight: normal;">File import succeeded: ' . $filePath . "</span><br />";

                if (! unlink($filePath)) {
                    echo '<span style="color: red; font-weight: bold;">Unable to delete file ' . $filePath . " after import.</span><br />";
                }

                global $wpdb;
                $page = $wpdb->get_results("SELECT * FROM wp_posts WHERE ID = {$postId}")[0];

                if (is_array($page)) {
                    $page = $page[0];
                }

                if ($page->post_content) {
                    $content = $page->post_content;
                    $start = strpos($content, "[gallery ") + strlen("[gallery ");
                    $end = strpos(substr($content, $start), "]");
                    $shortcode = substr($content, $start, $end);
                    $attrs = shortcode_parse_atts($shortcode);
                    $attrs["ids"] .= "," . $attachmentId;
                    $tempIds = explode(",", $attrs["ids"]);
                    $tempIds = array_filter($tempIds);
                    rsort($tempIds);
                    $attrs["ids"] = implode(",", $tempIds);
                    $shortcode = "";

                    foreach ($attrs as $key => $value) {
                        if (strlen($shortcode) > 0) {
                            $shortcode .= " ";
                        }

                        $shortcode .= $key . "=\"" . $value . "\"";
                    }

                    $newContent = substr($content, 0, $start);
                    $newContent .= $shortcode;
                    $newContent .= substr($content, $start + $end, strlen($content));
                    $wpdb->update(
                        'post_content',
                        ['post_content' => $newContent],
                        ['ID' => $postId]
                    );
                }
            }
        }
    }

    echo $succeededFileCount . " files uploaded and imported successfully. <br />";
    echo $failedFileCount . " files failed to uploaded or import successfully.";
}

get_header();

if (get_option('rmm_image_importer_key') != urldecode($_GET['key'])) {
    echo '<div id="message" class="error">';
    echo "<p><strong>Incorrect authentication key: you are not allowed to import images into this site.</strong></p></div>";
} else {
    echo '<br /><br />';
    $mtime = microtime();
    $mtime = explode(" ", $mtime);
    $mtime = $mtime[1] + $mtime[0];
    $starttime = $mtime;
    $dataset = get_option('rmm_image_importer_settings');

    if (is_array($dataset)) {
        foreach ($dataset as $data) {
            if (isset($data['folder'])
                || isset($data['page'])) {
?>
    <h2>Import from folder: <?php echo $data['folder']; ?></h2>
    <p>
<?php
                importImage(realpath(str_replace('//', '/', ABSPATH . '../../' . $data['folder'])), $data['page']); ?>
    </p>
<?php
            }
        }
    }
    $mtime = microtime();
    $mtime = explode(" ", $mtime);
    $mtime = $mtime[1] + $mtime[0];
    $endtime = $mtime;
    $totaltime = ($endtime - $starttime);
    echo 'Files imported to media library over ' . $totaltime . ' seconds.<br /><br />';
}

get_footer();
2
mike.bronner

いくつかのこと

  • WordPressがファイルを正しい場所に移動して検証し、添付ファイルのポストなどを作成するようにmedia_handle_sideloadを使用します。
  • これを一度だけ実行してはいけません。あなたはただ同じ問題に出くわすつもりですが、さらに輸入に行きます。あなたがそれに無限のメモリを与えるならば、あなたはスクリプトが単に時間切れになるという制限時間実行問題を持つでしょう
  • 一度に5つのファイルを処理し、何も処理されなくなるまで繰り返しrun run
  • WP CLIコマンドを使用し、WordPressをブートストラップしないでGUIから呼び出します。 Cronから直接呼び出してURLビジネスのpingをスキップしてください。 CLIコマンドは仕事をするための時間が無制限にかかるため、ブラウザから呼び出すことはできません。キーを持つGET変数は完全に不要になります。
  • エスケープエスケープエスケープ、あなたはこれらが含んでいるものが安全であると仮定して、あなたはこれらの配列と値をエコーアウトしています、しかし私がそこにスクリプトタグをスナックしたらどうでしょう? Unable to delete file <script>...</script> after import.。あなたが取ることができる最大のセキュリティステップはそれが最も差があるが最も使用されないものにする

プロトタイプWP CLIコマンド

ここにトリックをするべきである簡単なWP CLIコマンドがあります。私はテストしていませんが、すべての重要な部分があり、PHPに関して完全な初心者ではないと確信しています。

たとえば、WP CLIコンテキストの場合にのみ含めます。

if ( defined( 'WP_CLI' ) && WP_CLI ) {
    require_once dirname( __FILE__ ) . '/inc/class-plugin-cli-command.php';
}

テーマをfunctions.phpにコマンドをダンプしてそれが機能することを期待すると、WP CLIクラスがコマンドラインでのみロードされるため、エラーが発生します。

使用法:

wp mbimport run

クラス:

<?php
/**
 * Implements image importer command.
 */
class MBronner_Import_Images extends WP_CLI_Command {

    /**
     * Runs the import script and imports several images
     *
     * ## EXAMPLES
     *
     *     wp mbimport run
     *
     * @when after_wp_load
     */
    function run( $args, $assoc_args ) {
        if ( !function_exists('media_handle_upload') ) {
            require_once(ABSPATH . "wp-admin" . '/includes/image.php');
            require_once(ABSPATH . "wp-admin" . '/includes/file.php');
            require_once(ABSPATH . "wp-admin" . '/includes/media.php');
        }

        // Set the directory
        $dir = ABSPATH .'/wpse';
        // Define the file type
        $images = glob( $dir . "*.jpg" );
        if ( empty( $images ) {
            WP_CLI::success( 'no images to import' );
            exit;
        }
        // Run a loop and transfer every file to media library
        // $count = 0;
        foreach ( $images as $image ) {
            $file_array = array();
            $file_array['name'] = $image;
            $file_array['tmp_name'] = $image;

            $id = media_handle_sideload( $file_array, 0 );
            if ( is_wp_error( $id ) ) {
                WP_CLI::error( "failed to sideload ".$image );
                exit;
            }

            // only do 5 at a time, dont worry we can run this
            // several times till they're all done
            $count++;
            if ( $count === 5 ) {
                break; 
            }
        }
        WP_CLI::success( "import ran" );
    }
}

WP_CLI::add_command( 'mbimport', 'MBronner_Import_Images' );

本物のcronジョブから繰り返し呼び出します。それができない場合は、WP Cronを使用するか、またはGET変数をチェックするadmin_initをフックしてください。 runコマンド内のコードを変更して使用します。

WP CLIがオプションではない場合

PHPをブートストラップするスタンドアロンのWPファイルを使用することはセキュリティリスクであり、サーバーリソースを使い果たしたい場合(またはURLを複数回ヒットして重複の問題を引き起こす場合)すぐに)。

例えば:

// example.com/?mbimport=true
add_action( 'init', function() {
    if ( $_GET['action'] !== 'mbimport' ) {
        return;
    }
    if ( $_GET['key'] !== get_option('key thing' ) ) {
        return;
    }
    // the code from the run function in the CLI command, but with the WP_CLI::success bits swapped out
    // ...
    exit;
}

繰り返し呼び出し

あなたの外部サービスはこれを繰り返し呼び出すことができないかもしれません。私が言うところ:

  • たとえやるべき仕事がなくても、外部サービスに頼らず、あなた自身のサーバに関係なくそれを呼ばせてください
  • 標準のWP Cronタスクも機能します
  • 5分ごとに実行
  • それでもやることがある場合は、ノンブロッキングリクエストを使用して、タスクを自分自身で呼び出します。このようにして、終了するまで新しいインスタンスを生成し続けます。

            if ( $count === 5 ) {
                wp_remote_get( home_url('?mbimport=true&key=abc'), [ 'blocking' => false ]);
                exit;
            )
    

GUI?

ダッシュボードのUIの進行状況メーターが必要な場合は、インポートするフォルダーに残っているjpegファイルの数を数えるだけです。設定する必要がある場合は、UIを構築してoptionsに設定を保存してから、CLIスクリプトのオプションから取得します。

REST AP​​Iの使用を検討しましたか?

プロセス全体を回避し、REST AP​​Iを介してファイルを追加します。 jpegsをアップロードするためにexample.com/wp-json/wp/v2/mediaへのPOSTリクエストを行うことができます。あなたのサイトにコードは必要ありません

https://stackoverflow.com/questions/37432114/wp-rest-api-upload-image

3
Tom J Nowell

その目的のためだけに作成された組み込み関数がすでにあります。あなたはあなたのディスクから画像をアップロードするためにコードの壁を書く必要はありません。代わりに media_sideload_image を使用できます。

この機能はあなたのファイルをアップロードし、ファイル名、日付、IDそしてその他のものの面倒を見ます。

これは絶対パスでテストしていません(URLが必要です)が、上記のスクリプトを書く上でのスキルに基づいて、絶対パスをURLに変換するのは簡単です。

// Set the directory
$dir = ABSPATH .'/wpse';
// Define the file type
$images = glob($directory . "*.jpg");
// Run a loop and upload every file to media library
foreach($images as $image) {
    // Upload a single image
    media_sideload_image($image,'SOME POST ID HERE');
}

必要なものはこれだけです。画像は投稿に添付する必要がありますが、後で削除することができます。

3
Jack Johansson