web-dev-qa-db-fra.com

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 que c'est, définit les en-têtes appropriés et le sert comme Apache le ferait.

La raison pour laquelle je fais cela est que je dois utiliser PHP pour traiter certaines informations sur la demande avant de servir le fichier.

La vitesse est critique

virtual () n'est pas une option

Doit fonctionner 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 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], '.');
    }
}
}
?>
93
Kirk Ouimet

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

Les solutions sont classées de la meilleure solution à la pire mais aussi de la solution nécessitant le plus de contrôle sur le serveur Web à celle qui en a le moins besoin. Il ne semble pas y avoir de moyen simple d'avoir une solution à la fois rapide et efficace partout.


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

Tel que documenté par d'autres, c'est en fait la meilleure façon. La base est que vous effectuez votre contrôle d'accès en php, puis au lieu d'envoyer le fichier vous-même, vous dites 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 du système de fichiers.

Le principal problème avec 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 le configurer (soit dans Apache config ou .htaccess si vous l'autorisez)

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

Avec ce module, le chemin du fichier peut être absolu ou relatif au XSendFilePath spécifié.

Lighttpd

Le mod_fastcgi le supporte lorsqu'il est configuré avec

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

La documentation de la fonction est sur le lighttpd wiki ils documentent le X-LIGHTTPD-send-file en-tête mais le X-Sendfile le nom fonctionne aussi

Nginx

Sur Nginx, vous ne pouvez pas utiliser le X-Sendfile en-tête, vous devez utiliser leur propre en-tête 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 ne trouvent l'URL réelle du fichier et n'y accèdent directement, leur wiki contient ne bonne explication de cela.

Liens symboliques et en-tête d'emplacement

Vous pouvez utiliser liens symboliques et les rediriger, créez simplement 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 tailler 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 pouvoir activer FollowSymLinks dans un .htaccess ou dans la configuration Apache.

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

Un autre hack consiste à générer des fichiers d'accès Apache à partir de php permettant l'IP utilisateur explicite. Sous Apache, cela signifie utiliser mod_authz_Host (mod_access) Allow from 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 anodin et peut conduire certains utilisateurs à attendre longtemps. Et vous devez quand même tailler le fichier.

Évidemment, un autre problème serait que plusieurs personnes derrière la même adresse IP pourraient potentiellement accéder au fichier.

Quand tout le reste échoue

Si vous n'avez vraiment aucun moyen d'obtenir de l'aide de votre serveur Web, la seule solution qui reste est readfile il est disponible dans toutes les versions php actuellement utilisées et fonctionne plutôt bien (mais n'est pas vraiment efficace).


Combiner des solutions

En fin de compte, 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 installation scénario.

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

  • Nettoyer les URL (mod_rewrite sur Apache)
  • Fonctions Crypto (mcrypt module php)
  • Prise en charge de chaînes multi-octets (mbstring module php)
134
Julien Roncaglia

Le moyen le plus rapide: ne le faites pas. Regardez dans l'en-tête x-sendfile pour nginx , il y a des choses similaires pour les autres serveurs web aussi. Cela signifie que vous pouvez toujours faire le contrôle d'accès, etc. en php, mais déléguer l'envoi réel du fichier à un serveur Web conçu pour cela.

P.S: Je ressens des frissons en pensant à combien plus efficace d'utiliser ceci avec nginx, par rapport à la lecture et à l'envoi du fichier en php. Imaginez que 100 personnes téléchargent un fichier: avec php + Apache, étant généreux, c'est probablement 100 * 15 Mo = 1,5 Go (environ, tirez-moi), de RAM juste là. Nginx va simplement envoyer le fichier au noyau, puis il sera chargé directement du disque dans les tampons réseau. Rapide!

P.P.S: Et, avec cette méthode, vous pouvez toujours faire tout le contrôle d'accès, tout ce que vous voulez.

33
Jords

Voici une pure solution 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 que possible, 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 prend également en charge le service de téléchargement dans des plages (ce qui est également ce qu'Apache fait par défaut, je suppose), afin que les utilisateurs puissent suspendre/reprendre les téléchargements et bénéficier également de vitesses de téléchargement plus élevées avec des accélérateurs de téléchargement. servi via le $speed argument.

22
Alix Axel
header('Location: ' . $path);
exit(0);

Laissez Apache faire le travail pour vous.

13
amphetamachine

si vous avez la possibilité d'ajouter des extensions PECL à votre php, vous pouvez simplement utiliser les fonctions du paquet Fileinfo pour déterminer le type de contenu puis envoyer les en-têtes appropriés ...

0
Andreas Linden

Une meilleure implémentation, avec prise en charge du cache, en-têtes http personnalisés.

serveStaticFile($fn, array(
        'headers'=>array(
            'Content-Type' => 'image/x-icon',
            'Cache-Control' =>  'public, max-age=604800',
            'Expires' => gmdate("D, d M Y H:i:s", time() + 30 * 86400) . " GMT",
        )
    ));

function serveStaticFile($path, $options = array()) {
    $path = realpath($path);
    if (is_file($path)) {
        if(session_id())
            session_write_close();

        header_remove();
        set_time_limit(0);
        $size = filesize($path);
        $lastModifiedTime = filemtime($path);
        $fp = @fopen($path, 'rb');
        $range = array(0, $size - 1);

        header('Last-Modified: ' . gmdate("D, d M Y H:i:s", $lastModifiedTime)." GMT");
        if (( ! empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModifiedTime ) ) {
            header("HTTP/1.1 304 Not Modified", true, 304);
            return true;
        }

        if (isset($_SERVER['HTTP_RANGE'])) {
            //$valid = preg_match('^bytes=\d*-\d*(,\d*-\d*)*$', $_SERVER['HTTP_RANGE']);
            if(substr($_SERVER['HTTP_RANGE'], 0, 6) != 'bytes=') {
                header('HTTP/1.1 416 Requested Range Not Satisfiable', true, 416);
                header('Content-Range: bytes */' . $size); // Required in 416.
                return false;
            }

            $ranges = explode(',', substr($_SERVER['HTTP_RANGE'], 6));
            $range = explode('-', $ranges[0]); // to do: only support the first range now.

            if ($range[0] === '') $range[0] = 0;
            if ($range[1] === '') $range[1] = $size - 1;

            if (($range[0] >= 0) && ($range[1] <= $size - 1) && ($range[0] <= $range[1])) {
                header('HTTP/1.1 206 Partial Content', true, 206);
                header('Content-Range: bytes ' . sprintf('%u-%u/%u', $range[0], $range[1], $size));
            }
            else {
                header('HTTP/1.1 416 Requested Range Not Satisfiable', true, 416);
                header('Content-Range: bytes */' . $size);
                return false;
            }
        }

        $contentLength = $range[1] - $range[0] + 1;

        //header('Content-Disposition: attachment; filename="xxxxx"');
        $headers = array(
            'Accept-Ranges' => 'bytes',
            'Content-Length' => $contentLength,
            'Content-Type' => 'application/octet-stream',
        );

        if(!empty($options['headers'])) {
            $headers = array_merge($headers, $options['headers']);
        }
        foreach($headers as $k=>$v) {
            header("$k: $v", true);
        }

        if ($range[0] > 0) {
            fseek($fp, $range[0]);
        }
        $sentSize = 0;
        while (!feof($fp) && (connection_status() === CONNECTION_NORMAL)) {
            $readingSize = $contentLength - $sentSize;
            $readingSize = min($readingSize, 512 * 1024);
            if($readingSize <= 0) break;

            $data = fread($fp, $readingSize);
            if(!$data) break;
            $sentSize += strlen($data);
            echo $data;
            flush();
        }

        fclose($fp);
        return true;
    }
    else {
        header('HTTP/1.1 404 Not Found', true, 404);
        return false;
    }
}
0
shawn

La fonction PHP Download mentionnée ici causait un certain retard avant que le fichier ne commence réellement à télécharger. Je ne sais pas si cela a été causé par l'utilisation du cache de vernis ou quoi, mais pour moi, cela a aidé à supprimer complètement la sleep(1); et à définir $speed à 1024. Maintenant, cela fonctionne sans aucun problème et c'est aussi rapide que l'enfer. Peut-être pourriez-vous également modifier cette fonction, car je l'ai vue utilisée partout sur Internet.

0
user1601422