103 votes

Le moyen le plus rapide de servir un fichier en utilisant PHP

J'essaie de mettre en place une fonction qui reçoit un chemin de fichier, identifie ce qu'il est, définit les en-têtes appropriés et le sert comme le ferait Apache.

La raison pour laquelle je fais cela est que j'ai besoin d'utiliser PHP pour traiter certaines informations sur la requête avant de servir le fichier.

La vitesse est essentielle

virtual() n'est pas une option

Doit travailler dans un environnement d'hébergement partagé où l'utilisateur n'a aucun contrôle sur le serveur web (Apache/nginx, etc.)

Voici ce que j'ai obtenu jusqu'à présent :

File::output($path);

<?php
class File {
static function output($path) {
    // Check if the file exists
    if(!File::exists($path)) {
        header('HTTP/1.0 404 Not Found');
        exit();
    }

    // Set the content-type header
    header('Content-Type: '.File::mimeType($path));

    // Handle caching
    $fileModificationTime = gmdate('D, d M Y H:i:s', File::modificationTime($path)).' GMT';
    $headers = getallheaders();
    if(isset($headers['If-Modified-Since']) && $headers['If-Modified-Since'] == $fileModificationTime) {
        header('HTTP/1.1 304 Not Modified');
        exit();
    }
    header('Last-Modified: '.$fileModificationTime);

    // Read the file
    readfile($path);

    exit();
}

static function mimeType($path) {
    preg_match("|\.([a-z0-9]{2,4})$|i", $path, $fileSuffix);

    switch(strtolower($fileSuffix[1])) {
        case 'js' :
            return 'application/x-javascript';
        case 'json' :
            return 'application/json';
        case 'jpg' :
        case 'jpeg' :
        case 'jpe' :
            return 'image/jpg';
        case 'png' :
        case 'gif' :
        case 'bmp' :
        case 'tiff' :
            return 'image/'.strtolower($fileSuffix[1]);
        case 'css' :
            return 'text/css';
        case 'xml' :
            return 'application/xml';
        case 'doc' :
        case 'docx' :
            return 'application/msword';
        case 'xls' :
        case 'xlt' :
        case 'xlm' :
        case 'xld' :
        case 'xla' :
        case 'xlc' :
        case 'xlw' :
        case 'xll' :
            return 'application/vnd.ms-excel';
        case 'ppt' :
        case 'pps' :
            return 'application/vnd.ms-powerpoint';
        case 'rtf' :
            return 'application/rtf';
        case 'pdf' :
            return 'application/pdf';
        case 'html' :
        case 'htm' :
        case 'php' :
            return 'text/html';
        case 'txt' :
            return 'text/plain';
        case 'mpeg' :
        case 'mpg' :
        case 'mpe' :
            return 'video/mpeg';
        case 'mp3' :
            return 'audio/mpeg3';
        case 'wav' :
            return 'audio/wav';
        case 'aiff' :
        case 'aif' :
            return 'audio/aiff';
        case 'avi' :
            return 'video/msvideo';
        case 'wmv' :
            return 'video/x-ms-wmv';
        case 'mov' :
            return 'video/quicktime';
        case 'zip' :
            return 'application/zip';
        case 'tar' :
            return 'application/x-tar';
        case 'swf' :
            return 'application/x-shockwave-flash';
        default :
            if(function_exists('mime_content_type')) {
                $fileSuffix = mime_content_type($path);
            }
            return 'unknown/' . trim($fileSuffix[0], '.');
    }
}
}
?>

11 votes

Pourquoi tu ne laisses pas Apache faire ça ? Ce sera toujours considérablement plus rapide que de lancer l'interpréteur PHP...

4 votes

Je dois traiter la demande et stocker certaines informations dans la base de données avant de sortir le fichier.

3 votes

Puis-je suggérer un moyen d'obtenir l'extension sans les expressions régulières plus coûteuses : $extension = end(explode(".", $pathToFile)) ou vous pouvez le faire avec substr et strrpos : $extension = substr($pathToFile, strrpos($pathToFile, '.')) . En outre, comme solution de rechange à mime_content_type() vous pouvez essayer un appel système : $mimetype = exec("file -bi '$pathToFile'", $output);

146voto

VirtualBlackFox Points 9565

Ma réponse précédente était partielle et peu documentée, voici une mise à jour avec un résumé des solutions de celle-ci et de celles des autres participants à la discussion.

Les solutions sont classées de la meilleure à la pire, mais aussi de celle qui nécessite le plus de contrôle sur le serveur web à celle qui en nécessite le moins. Il ne semble pas y avoir de moyen facile d'avoir une solution qui soit à la fois rapide et qui fonctionne partout.


Utilisation de l'en-tête X-SendFile

Comme l'ont montré d'autres personnes, c'est en fait le meilleur moyen. La base est que vous effectuez votre contrôle d'accès en php et qu'au lieu d'envoyer le fichier vous-même, vous demandez au serveur web de le faire.

Le code php de base est :

header("X-Sendfile: $file_name");
header("Content-type: application/octet-stream");
header('Content-Disposition: attachment; filename="' . basename($file_name) . '"');

$file_name est le chemin complet sur le système de fichiers.

Le principal problème de cette solution est qu'elle doit être autorisée par le serveur web et qu'elle n'est pas installée par défaut (apache), n'est pas active par défaut (lighttpd) ou nécessite une configuration spécifique (nginx).

Apache

Sous apache, si vous utilisez mod_php, vous devez installer un module appelé mod_xsendfile puis configurez-le (soit dans la configuration d'apache, soit dans le fichier .htaccess si vous l'autorisez).

XSendFile on
XSendFilePath /home/www/example.com/htdocs/files/

Avec ce module, le chemin d'accès au fichier peut être soit absolu, soit relatif à l'emplacement spécifié. XSendFilePath .

Lighttpd

Le mod_fastcgi prend en charge cette fonction lorsqu'il est configuré avec l'option

"allow-x-send-file" => "enable" 

La documentation relative à cette fonctionnalité se trouve sur le site wiki lighttpd ils documentent le X-LIGHTTPD-send-file mais l'en-tête X-Sendfile le nom fonctionne également

Nginx

Sur Nginx, vous ne pouvez pas utiliser l'option X-Sendfile vous devez utiliser leur propre en-tête qui est nommé X-Accel-Redirect . Il est activé par défaut et la seule vraie différence est que son argument doit être un URI et non un système de fichiers. La conséquence est que vous devez définir un emplacement marqué comme interne dans votre configuration pour éviter que les clients trouvent l'url réelle du fichier et y aillent directement, leur wiki contient une bonne explication de ceci.

Symlinks et en-tête de localisation

Vous pourriez utiliser liens symétriques et rediriger vers eux, il suffit de créer des liens symboliques vers votre fichier avec des noms aléatoires lorsqu'un utilisateur est autorisé à accéder à un fichier et rediriger l'utilisateur vers celui-ci en utilisant :

header("Location: " . $url_of_symlink);

Évidemment, vous aurez besoin d'un moyen de les élaguer soit lorsque le script pour les créer est appelé, soit via cron (sur la machine si vous y avez accès ou via un service webcron sinon).

Sous apache, vous devez être capable d'activer FollowSymLinks dans un .htaccess ou dans la configuration d'apache.

Contrôle d'accès par l'en-tête IP et de localisation

Une autre astuce consiste à générer les fichiers d'accès à apache à partir de php en autorisant l'utilisateur IP explicite. Sous apache, cela signifie utiliser mod_authz_host ( mod_access ) Allow from des commandes.

Le problème est que le verrouillage de l'accès au fichier (car plusieurs utilisateurs peuvent vouloir le faire en même temps) n'est pas trivial et pourrait conduire certains utilisateurs à attendre longtemps. Et il faut quand même élaguer le fichier.

Un autre problème évident serait que plusieurs personnes derrière la même IP pourraient potentiellement accéder au fichier.

Quand tout le reste échoue

Si vous n'avez vraiment aucun moyen de faire en sorte que votre serveur web vous aide, la seule solution qui reste est la suivante fichier de lecture elle est disponible dans toutes les versions de php actuellement utilisées et fonctionne assez bien (mais n'est pas vraiment efficace).


Combinaison de solutions

In fine, la meilleure façon d'envoyer un fichier très rapidement si vous voulez que votre code php soit utilisable partout est d'avoir une option configurable quelque part, avec des instructions sur la façon de l'activer en fonction du serveur web et peut-être une détection automatique dans votre install script.

C'est assez similaire à ce qui est fait dans beaucoup de logiciels pour

  • Nettoyer les urls ( mod_rewrite sur apache)
  • Fonctions cryptographiques ( mcrypt module php)
  • Support des chaînes de caractères multi-octets ( mbstring module php)

0 votes

Y a-t-il un problème à effectuer certains travaux en PHP (vérifier les cookies/autres paramètres GET/POST par rapport à la base de données) avant d'effectuer les opérations suivantes header("Location: " . $path); ?

2 votes

Aucun problème pour ce genre d'action, la chose à laquelle il faut faire attention est d'envoyer du contenu (print, echo) car l'en-tête doit venir avant tout contenu et de faire des choses après avoir envoyé cet en-tête, ce n'est pas une redirection immédiate et le code après celui-ci sera exécuté la plupart du temps mais vous n'avez aucune garantie que le navigateur ne coupera pas la connexion.

0 votes

Jords : Je ne savais pas qu'Apache supportait aussi cela, je l'ajouterai à ma réponse quand j'aurai le temps. Le seul problème avec cela est que ce n'est pas unifié (X-Accel-Redirect nginx par exemple) donc une deuxième solution est nécessaire si le serveur ne le supporte pas. Mais je devrais l'ajouter à ma réponse.

33voto

Jords Points 1157

Le moyen le plus rapide : Ne le faites pas. Regardez dans le En-tête x-sendfile pour nginx il existe des choses similaires pour d'autres serveurs web. Cela signifie que vous pouvez toujours effectuer le contrôle d'accès, etc. en php, mais déléguer l'envoi effectif du fichier à un serveur web conçu à cet effet.

P.S. : J'ai des frissons rien qu'en pensant à l'efficacité de l'utilisation de ce système avec nginx, par rapport à la lecture et à l'envoi du fichier en php. Pensez simplement que 100 personnes téléchargent un fichier : Avec php + apache, en étant généreux, c'est probablement 100*15mb = 1.5GB (approx, tirez moi dessus), de ram juste là. Nginx se chargera simplement d'envoyer le fichier au noyau, puis il sera chargé directement du disque dans les tampons du réseau. Rapide !

P.P.S : Et, avec cette méthode, vous pouvez toujours faire tous les contrôles d'accès, les trucs de base de données que vous voulez.

4 votes

Permettez-moi d'ajouter que cela existe aussi pour Apache : jasny.net/articles/how-i-php-x-sendfile . Vous pouvez faire en sorte que le script renifle le serveur et envoie les en-têtes appropriés. S'il n'y en a pas (et que l'utilisateur n'a aucun contrôle sur le serveur, comme indiqué dans la question), il faut revenir à une commande normale readfile()

0 votes

C'est tout simplement génial - j'ai toujours détesté augmenter la limite de mémoire de mes hôtes virtuels juste pour que PHP puisse servir un fichier, et avec ceci je ne devrais pas avoir à le faire. Je vais l'essayer très bientôt.

1 votes

Et pour le crédit où le crédit est dû, Lighttpd a été le premier serveur web à l'implémenter (et les autres l'ont copié, ce qui est bien puisque c'est une excellente idée. Mais il faut rendre à César ce qui appartient à César)...

25voto

Alix Axel Points 63455

Voici une solution purement PHP. J'ai adapté la fonction suivante à partir de mon cadre personnel :

function Download($path, $speed = null, $multipart = true)
{
    while (ob_get_level() > 0)
    {
        ob_end_clean();
    }

    if (is_file($path = realpath($path)) === true)
    {
        $file = @fopen($path, 'rb');
        $size = sprintf('%u', filesize($path));
        $speed = (empty($speed) === true) ? 1024 : floatval($speed);

        if (is_resource($file) === true)
        {
            set_time_limit(0);

            if (strlen(session_id()) > 0)
            {
                session_write_close();
            }

            if ($multipart === true)
            {
                $range = array(0, $size - 1);

                if (array_key_exists('HTTP_RANGE', $_SERVER) === true)
                {
                    $range = array_map('intval', explode('-', preg_replace('~.*=([^,]*).*~', '$1', $_SERVER['HTTP_RANGE'])));

                    if (empty($range[1]) === true)
                    {
                        $range[1] = $size - 1;
                    }

                    foreach ($range as $key => $value)
                    {
                        $range[$key] = max(0, min($value, $size - 1));
                    }

                    if (($range[0] > 0) || ($range[1] < ($size - 1)))
                    {
                        header(sprintf('%s %03u %s', 'HTTP/1.1', 206, 'Partial Content'), true, 206);
                    }
                }

                header('Accept-Ranges: bytes');
                header('Content-Range: bytes ' . sprintf('%u-%u/%u', $range[0], $range[1], $size));
            }

            else
            {
                $range = array(0, $size - 1);
            }

            header('Pragma: public');
            header('Cache-Control: public, no-cache');
            header('Content-Type: application/octet-stream');
            header('Content-Length: ' . sprintf('%u', $range[1] - $range[0] + 1));
            header('Content-Disposition: attachment; filename="' . basename($path) . '"');
            header('Content-Transfer-Encoding: binary');

            if ($range[0] > 0)
            {
                fseek($file, $range[0]);
            }

            while ((feof($file) !== true) && (connection_status() === CONNECTION_NORMAL))
            {
                echo fread($file, round($speed * 1024)); flush(); sleep(1);
            }

            fclose($file);
        }

        exit();
    }

    else
    {
        header(sprintf('%s %03u %s', 'HTTP/1.1', 404, 'Not Found'), true, 404);
    }

    return false;
}

Le code est aussi efficace qu'il peut l'être, il ferme le gestionnaire de session afin que d'autres scripts PHP puissent s'exécuter simultanément pour le même utilisateur / session. Il permet également de servir les téléchargements dans des plages (ce qui est également ce que fait Apache par défaut, je le soupçonne), de sorte que les gens peuvent mettre en pause / reprendre les téléchargements et également bénéficier de vitesses de téléchargement plus élevées avec les accélérateurs de téléchargement. Il vous permet également de spécifier la vitesse maximale (en Kbps) à laquelle le téléchargement (partie) doit être servi via l'attribut $speed argument.

2 votes

Évidemment, ce n'est une bonne idée que si vous ne pouvez pas utiliser X-Sendfile ou une de ses variantes pour que le noyau envoie le fichier. Vous devriez pouvoir remplacer la boucle feof()/fread() ci-dessus par [ [php.net/manual/fen/function.eio-sendfile.php] (la fonction de PHP](http://php.net/manual/en/function.eio-sendfile.php](PHP's) eio_sendfile()], qui accomplit la même chose en PHP. Ce n'est pas aussi rapide que de le faire directement dans le noyau, car toute sortie générée en PHP doit toujours repasser par le processus du serveur web, mais ce sera beaucoup plus rapide que de le faire dans le code PHP.

0 votes

@BrianC : Bien sûr, mais vous ne pouvez pas limiter la vitesse ou la capacité multipartite avec X-Sendfile (qui n'est peut-être pas disponible) et @BrianC : Bien sûr. eio n'est pas non plus toujours disponible. Quand même, +1, je ne connaissais pas cette extension pecl. =)

0 votes

Serait-il utile de prendre en charge transfer-encoding:chunked et content-encoding:gzip ?

14voto

amphetamachine Points 7384
header('Location: ' . $path);
exit(0);

Laissez Apache faire le travail pour vous.

12 votes

C'est plus simple que la méthode x-sendfile, mais cela ne fonctionnera pas pour restreindre l'accès à un fichier, par exemple aux seules personnes connectées. Si vous n'avez pas besoin de faire cela, c'est parfait !

0 votes

Ajoutez également une vérification des référents avec mod_rewrite.

1 votes

Vous pouvez vous authentifier avant de passer l'en-tête. De cette façon, vous n'aurez pas à pomper des tonnes de données dans la mémoire de PHP.

0voto

Andreas Linden Points 6344

Si vous avez la possibilité d'ajouter des extensions PECL à votre php, vous pouvez simplement utiliser les fonctions de l'application Paquet Fileinfo pour déterminer le type de contenu et envoyer les bons en-têtes...

0 votes

/bump, avez-vous mentionné cette possibilité ? :)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X