Aller au contenu

[Snippet #05 - PHP] téléchargement de fichiers par lots

Icône dev

Je viens de me réaliser un petit script de "batch download" en PHP (téléchargement par lot, en bon français) et me disais qu'il pourrait être intéressant de vous le partager avec quelques petites explications.

L'histoire

Comme vous le savez peut-être déjà, le site Wallbase.cc (merveilleux site de wallpapers, sans pub, très agréable à naviguer et très bien fourni) est en train de mourir depuis quelques mois, du fait de la disparition de son administrateur.

De plus, depuis peu quelqu'un (par le biais d'un bot ?) est en train de supprimer tous les fonds d'écran du site, et les modérateurs galèrent à freiner ce problème. Ils conseillent donc de télécharger les wallpapers qu'on a mis en favoris avant que ceux-ci ne disparaissent.

C'est donc ce que j'ai fait, aidé d'un script Greasemonkey pour récupérer plus rapidement les liens de téléchargement. Ensuite, j'aurais pu donner un petit coup de DownThemAll! et le tour était joué. Mais j'ai préféré scripter ça et envoyer direct sur mon propre serveur.

Edit 2013-12-17 : j'ai même l'impression que c'est pire que cela. Le forum ne semble plus accessibles (certains fichiers manquants ?). Mais si c'est effectivement une attaque, elle me semble plutôt étrange...

Le principe

En réalité, tout repose sur une fonction PHP bien simple nommée copy(). Celle-ci permet, comme vous vous en doutez, de copier un fichier d'un emplacement à un autre, et ce même depuis une URL (du moins depuis PHP 4.3).

Ainsi, pour copier une image d'un autre serveur vers le votre, il suffit de faire :

<?php
$result = copy(
    'http://www.example.com/image.jpg', /* other server */
    '/var/www/my-images/image.jpg'      /* your server */
);

Le reste de mon script n'est en fait qu'un ensemble de commodités :

  • multiples URLs : saisie dans un textarea des URLs de tous les fichiers à récupérer (une URL par ligne)
  • Erreurs : affichage d'un log des réussites et échecs de téléchargement
  • Téléchargement groupé : compression de tous les fichiers dans un ZIP pour pouvoir les télécharger en une fois

Vous pouvez vous-même choisir le nom du zip généré. Si vous demandez à regénérer un zip du même nom, l'ancien sera écrasé. Ainsi, si vous avez besoin d'en créer plusieurs en vue de les récupérer plus tard, c'est possible.

Configuration requise

  • PHP 4.3 ou supérieur (à vérifier, PHP 5 conseillé)
  • Droits d'écriture sur le dossier où vous placerez le script. A noter que des sous-dossier et des fichiers zips seront créés et supprimés, il est donc préférable de donner au script son propre dossier.

Le code complet :

Edit : J'ai créé un Gist correspondant, où je pourrai faire d'éventuelles révisions, et où vous pourrez me faire vos remarques.

<!doctype html>
<!--
PHP Batch Download Script

@author Yosko <www.yosko.net>
@copyright none: free and opensource
@link https://www.yosko.net/2013/snippet-php-batch-download/
-->
<html lang="en-US">
<head>
    <meta charset="UTF-8">
    <title>Batch Downloader</title>
    <style>
textarea {
    width: 100%;
    min-height: 10em;
}
    </style>
</head>
<body>
    <h1>Batch Downloader</h1>
<?php

//config
error_reporting(E_ALL | E_STRICT);
ini_set('display_errors','On');
set_time_limit ( 180 ); //facultative: to avoid timeout
ob_flush(); flush();

//recursive rmdir from php.net
function delTree($dir) {
    $files = array_diff(scandir($dir), array('.','..'));
    foreach ($files as $file) {
        (is_dir($dir.'/'.$file)) ? delTree($dir.'/'.$file) : unlink($dir.'/'.$file);
    }
    return rmdir($dir);
}

$errors = array();
$urlString = '';

//a list of URLs to download was given
if(isset($_POST['urls'])
    && isset($_POST['directory'])
    && strpos($_POST['directory'], '..') === false
    && strpos($_POST['directory'], '/') === false
) {
?>
    <ul>
<?php

    $urls = explode(PHP_EOL, $_POST['urls']);
    $directory = trim($_POST['directory']);
    if(empty($directory)) { $directory = 'files'; }
    $zipFile = $directory.'.zip';

    //temporary directory to put the files into
    if(!file_exists($directory)) {
        mkdir($directory);
    }
    //temporary archive file for download
    if(file_exists($zipFile)) {
        unlink($zipFile);
    }

    //try and download each file
    foreach($urls as $url) {
        $url = trim($url);
        $filename = basename($url);
        $success = copy($url, $directory.DIRECTORY_SEPARATOR.$filename);
        if($success) {
            echo '<li>'.$filename.' : OK</li>';
        } else {
            $errors[] = $url;
            echo '<li>'.$filename.' : KO (source = <a href="'.$url.'">'.$url.'</a>)</li>';
        }
        ob_flush(); flush();
    }

    //zip files
    $zip = new ZipArchive();
    if ($zip->open($zipFile, ZIPARCHIVE::CREATE) === true) {
        foreach (glob($directory.'/*') as $file) {
            $zip->addFile($file);
        }
        $zip->close();
    }

    //delete directory
    delTree($directory);

?>
    </ul>
    <p>
        <?php echo count($urls); ?> file(s),
        <?php echo count($urls) - count($errors); ?> successe(s),
        <?php echo count($errors); ?> error(s)
    </p>
    <p>Download <a href="<?php echo $zipFile; ?>"><?php echo $zipFile; ?></a></p>
    <p>Errors :</p>
    <ul>
<?php foreach($errors as $error) { ?>
        <li><?php echo $errors; ?></li>
<?php } // end loop on errors ?>
    </ul>
<?php } // end form post ?>
    <form method="POST" target="">
        <input type="text" name="directory" placeholder="Destination file...">.zip
        <textarea name="urls" placeholder="Put each URL on a different line">
<?php
foreach($errors as $error) {
    echo $error.PHP_EOL;
}
?></textarea>
        <input type="submit" name="submitURLs" value="Download">
    </form>
</body>
</html>

Conclusion

Certes, le code est encore un peu grossier ("rough around the edges", j'ai envie de dire). Mais à défaut de la vie, il m'aura au moins sauvé mes favoris Wallbase.cc. Comme toujours, si vous avez des critiques ou questions, je suis à votre écoute :)

Keep calm & download!