web-dev-qa-db-fra.com

Le téléchargement d'images vers la médiathèque échoue et la mémoire est épuisée

J'essaie de télécharger plusieurs centaines d'images par jour d'un dossier du serveur vers la médiathèque à l'aide du script suivant, programmé via 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();

Le problème est que peu importe ce que je fais, le script échoue après seulement deux images avec cette erreur:

Erreur irrécupérable: taille de mémoire autorisée de 1073741824 octets épuisés (tentative d'allocation de 28672 octets) dans /home/forge/morselandcompany.com/public/wordpress/wp-includes/wp-db.php à la ligne 1841

J'ai la limite de mémoire définie à 1024M dans wordpress, ainsi que PHP. Je ne vois pas pourquoi ce script aurait besoin de plus de 128M, vraiment. Comment ce script peut-il être optimisé pour fonctionner correctement? (La taille moyenne de l'image est de 800 Ko.)

Un débogage initial avec BlackFire.io suggère les problèmes de mémoire suivants: - wpdb-> requête: 3,2 Mo, appelée 31 fois - mysqli_fetch_object: 1,89 Mo, appelée 595 fois - run_init () dans wp-settings.php: 5,4 Mo, appelée une fois Au total, blackfire suggère qu'il faut plus de 8 Mo pour exécuter ce script!

J'ai également testé avec tous les plugins désactivés, et cela s'est terminé avec le même résultat.

Je cours sur - PHP 7.1
- Ubuntu 16.04
- DigitalOcean VPS (1 CPU, 1 Go de RAM)
- Wordpress 4.8
- NGINX 1.11.5

Merci pour toute aide!

Mise à jour: dans l'intérêt de l'exhaustivité afin que d'autres puissent utiliser la solution aux fuites de mémoire associées à get_post et à wp_update_post, j'ai publié le code finalisé qui a résolu le problème ci-dessus. Comme vous pouvez le constater, la solution consistait à écrire mes propres requêtes à l'aide de $ wpdb au lieu de s'appuyer sur les deux méthodes WP à l'origine des fuites de mémoire:

<?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

Quelques choses

  • Utilisez media_handle_sideload pour que WordPress déplace les fichiers au bon endroit et les valide pour vous, crée la publication en pièce jointe, etc., rien de ce manuel.
  • Ne courez pas cette fois et attendez-vous à tout faire. Vous allez simplement rencontrer le même problème mais plus loin dans l'importation. Si vous lui accordez une mémoire infinie, vous rencontrerez un problème d’exécution limitée dans le temps où le script n’exécutera tout simplement plus de temps.
  • Traite 5 fichiers à la fois, et appelez-le à plusieurs reprises, exécutez-le jusqu'à ce qu'il ne reste plus rien à traiter.
  • Utilisez une commande WP CLI, ne démarrez pas WordPress et appelez-le à partir de l'interface graphique. Appelez-le directement à partir de Cron et ignorez le commerce ping d'une entreprise d'URL. Les commandes CLI ont un temps de travail illimité et vous ne pouvez pas les appeler à partir du navigateur. La variable GET avec la clé devient complètement inutile.
  • Évasion évasion échapper, vous faisant écho à ces tableaux et valeurs, en supposant ce qu'ils contiennent est sûr, mais si je faufilé une balise de script là-dedans? Unable to delete file <script>...</script> after import.. La plus grande mesure de sécurité que vous puissiez prendre qui fait la plus grande différence mais la moins utilisée

Une commande de prototype WP CLI

Voici une simple commande WP CLI qui devrait faire l'affaire. Je ne l'ai pas testée, mais toutes les pièces importantes sont présentes. J'espère que vous n'êtes pas un novice en matière de PHP et que vous pouvez resserrer toutes les erreurs ou erreurs mineures, aucune connaissance supplémentaire de l'API n'est nécessaire.

Vous souhaiterez l'inclure uniquement dans un contexte CLI WP, par exemple:

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

Modifiez en conséquence, si vous exportez la commande dans functions.php d'un thème et que tout fonctionnera correctement, des erreurs se produiront car WP les classes CLI ne sont chargées que sur la ligne de commande, jamais lors du traitement d'une demande du navigateur.

Usage:

wp mbimport run

Classe:

<?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' );

Appelez à plusieurs reprises à partir d'un vrai travail cron. Si vous ne le pouvez pas, utilisez WP Cron ou installez un hook sur admin_init qui recherche une variable GET. Utilisez le code dans la commande run avec quelques modifications.

Lorsque WP CLI n'est pas une option

L'utilisation d'un fichier autonome PHP amorçant WP constitue un risque pour la sécurité et constitue une excellente cible pour les attaquants qui souhaitent épuiser les ressources de votre serveur (ou déclencher des problèmes de duplication en frappant l'URL plusieurs fois tous.) immediatement ).

Par exemple:

// 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;
}

Appel répété

Il se peut que votre service externe ne puisse pas appeler cela à plusieurs reprises. Pour ce que je dis:

  • Ne comptez pas sur le service externe, demandez à votre propre serveur de l'appeler quand même, même s'il n'y a pas de travail à faire.
  • Une tâche standard WP Cron fonctionnerait également
  • Exécutez-le toutes les 5 minutes
  • Faites en sorte que la tâche s’appelle elle-même s’il ya encore du travail à faire, en utilisant une demande non bloquante. De cette façon, les nouvelles instances continueront à être générées jusqu'à la fin, par exemple.

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

GUI?

Si vous souhaitez un indicateur de progression pour une interface utilisateur dans le tableau de bord, il suffit de compter le nombre de fichiers JPEG restant dans le dossier à importer. Si vous devez le configurer, créez une interface utilisateur, enregistrez les paramètres dans les options, puis sélectionnez-le à partir du script CLI.

Avez-vous envisagé d'utiliser l'API REST?

Faites un détour par tout le processus et ajoutez les fichiers via l'API REST. Vous pouvez faire une demande POST à example.com/wp-json/wp/v2/media pour télécharger les fichiers jpeg. Aucun code sur votre site nécessaire

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

3
Tom J Nowell

Il existe déjà une fonction intégrée créée uniquement à cette fin. Vous n'avez pas besoin d'écrire des murs de codes pour télécharger des images à partir de votre disque. Vous pouvez utiliser media_sideload_image à la place.

Cette fonction permettra de télécharger vos fichiers, de s’occuper du nom du fichier, de la date, de l’ID et du reste.

Je n'ai pas testé cela avec un chemin absolu (il faut l'URL), mais il serait facile de convertir un chemin absolu en URL, en fonction de vos compétences en écriture du script ci-dessus.

// 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');
}

C'est tout ce dont vous avez besoin. Les images doivent être attachées à un message, mais vous pouvez les détacher par la suite.

3
Jack Johansson