Schnellster Weg, um eine Datei mit PHP zu bedienen

98

Ich versuche, eine Funktion zusammenzustellen, die einen Dateipfad empfängt, identifiziert, was er ist, die entsprechenden Header festlegt und ihn wie Apache bereitstellt.

Der Grund, warum ich dies tue, ist, dass ich PHP verwenden muss, um einige Informationen über die Anforderung zu verarbeiten, bevor ich die Datei bereitstelle.

Geschwindigkeit ist entscheidend

virtual () ist keine Option

Muss in einer gemeinsam genutzten Hosting-Umgebung funktionieren, in der der Benutzer keine Kontrolle über den Webserver hat (Apache / Nginx usw.)

Folgendes habe ich bisher:

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], '.');
    }
}
}
?>
Kirk Ouimet
quelle
10
Warum lässt du Apache das nicht tun? Es wird immer erheblich schneller sein als das Starten des PHP-Interpreters ...
Billy ONeal
4
Ich muss die Anfrage verarbeiten und einige Informationen in der Datenbank speichern, bevor ich die Datei ausgeben kann.
Kirk Ouimet
3
Darf ich einen Weg vorschlagen, um die Erweiterung ohne die teureren regulären Ausdrücke zu erhalten: $extension = end(explode(".", $pathToFile))oder Sie können dies mit substr und strrpos tun : $extension = substr($pathToFile, strrpos($pathToFile, '.')). Als Ersatz für mime_content_type()können Sie auch einen Systemaufruf versuchen:$mimetype = exec("file -bi '$pathToFile'", $output);
Fanis Hatzidakis
Was meinst du mit am schnellsten ? Schnellste Downloadzeit?
Alix Axel

Antworten:

140

Meine vorherige Antwort war teilweise und nicht gut dokumentiert. Hier ist ein Update mit einer Zusammenfassung der Lösungen daraus und von anderen in der Diskussion.

Die Lösungen sind von der besten bis zur schlechtesten Lösung geordnet, aber auch von der Lösung, die die meiste Kontrolle über den Webserver benötigt, bis zu der Lösung, die die weniger benötigt. Es scheint keine einfache Möglichkeit zu geben, eine Lösung zu finden, die sowohl schnell ist als auch überall funktioniert.


Verwenden des X-SendFile-Headers

Wie von anderen dokumentiert, ist es tatsächlich der beste Weg. Die Basis ist, dass Sie Ihre Zugriffskontrolle in PHP durchführen und dann, anstatt die Datei selbst zu senden, den Webserver anweisen, dies zu tun.

Der grundlegende PHP-Code lautet:

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

Wo $file_nameist der vollständige Pfad im Dateisystem?

Das Hauptproblem bei dieser Lösung besteht darin, dass sie vom Webserver zugelassen werden muss und entweder nicht standardmäßig installiert ist (Apache), standardmäßig nicht aktiv ist (lighttpd) oder eine bestimmte Konfiguration benötigt (nginx).

Apache

Wenn Sie unter apache mod_php verwenden, müssen Sie ein Modul namens mod_xsendfile installieren und dann konfigurieren (entweder in apache config oder .htaccess, wenn Sie dies zulassen).

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

Mit diesem Modul kann der Dateipfad entweder absolut oder relativ zum angegebenen sein XSendFilePath.

Lighttpd

Das mod_fastcgi unterstützt dies, wenn es mit konfiguriert wird

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

Die Dokumentation für die Funktion befindet sich im lighttpd-Wiki. Sie dokumentiert den X-LIGHTTPD-send-fileHeader, aber der X-SendfileName funktioniert auch

Nginx

Unter Nginx können Sie den X-SendfileHeader nicht verwenden. Sie müssen einen eigenen Header mit dem Namen verwenden X-Accel-Redirect. Es ist standardmäßig aktiviert und der einzige wirkliche Unterschied besteht darin, dass das Argument ein URI und kein Dateisystem sein sollte. Die Folge ist, dass Sie einen Speicherort definieren müssen, der in Ihrer Konfiguration als intern markiert ist, um zu vermeiden, dass Clients die echte Datei-URL finden und direkt darauf zugreifen. Ihr Wiki enthält eine gute Erklärung dafür.

Symlinks und Standort-Header

Sie können Symlinks verwenden und zu diesen umleiten. Erstellen Sie einfach Symlinks zu Ihrer Datei mit zufälligen Namen, wenn ein Benutzer berechtigt ist, auf eine Datei zuzugreifen, und leiten Sie den Benutzer mit folgenden Methoden zu dieser Datei um.

header("Location: " . $url_of_symlink);

Natürlich benötigen Sie eine Möglichkeit, sie entweder beim Aufruf des Skripts zum Erstellen oder über cron (auf dem Computer, wenn Sie Zugriff haben, oder über einen anderen Webcron-Dienst) zu bereinigen.

Unter Apache müssen Sie in der Lage sein, FollowSymLinksin einer .htaccessoder in der Apache-Konfiguration zu aktivieren .

Zugriffskontrolle über IP und Location Header

Ein weiterer Hack besteht darin, Apache-Zugriffsdateien aus PHP zu generieren, die die explizite Benutzer-IP zulassen. Unter Apache bedeutet es, mod_authz_host( mod_access) Allow fromBefehle zu verwenden.

Das Problem ist, dass das Sperren des Zugriffs auf die Datei (da mehrere Benutzer dies gleichzeitig tun möchten) nicht trivial ist und dazu führen kann, dass einige Benutzer lange warten. Und Sie müssen die Datei trotzdem beschneiden.

Offensichtlich wäre ein weiteres Problem, dass mehrere Personen hinter derselben IP möglicherweise auf die Datei zugreifen könnten.

Wenn alles andere fehlschlägt

Wenn Sie wirklich keine Möglichkeit haben, Ihren Webserver dazu zu bringen, Ihnen zu helfen, ist die einzige verbleibende Lösung das Readfile , das in allen derzeit verwendeten PHP-Versionen verfügbar ist und ziemlich gut funktioniert (aber nicht wirklich effizient ist).


Lösungen kombinieren

Der beste Weg, um eine Datei sehr schnell zu senden, wenn Sie möchten, dass Ihr PHP-Code überall verwendet werden kann, besteht darin, irgendwo eine konfigurierbare Option zu haben, die Anweisungen zur Aktivierung in Abhängigkeit vom Webserver und möglicherweise eine automatische Erkennung in Ihrer Installation enthält Skript.

Es ist ziemlich ähnlich zu dem, was in einer Menge Software für gemacht wird

  • Saubere URLs ( mod_rewriteauf Apache)
  • Kryptofunktionen ( mcryptPHP-Modul)
  • Multibyte String Unterstützung ( mbstringPHP Modul)
Julien Roncaglia
quelle
Gibt es ein Problem mit einigen PHP-Arbeiten (überprüfen Sie Cookies / andere GET / POST-Parameter anhand der Datenbank), bevor Sie dies tun header("Location: " . $path);?
Afriza N. Arief
2
Kein Problem für eine solche Aktion. Sie müssen vorsichtig sein, wenn Sie Inhalte (Drucken, Echo) senden, da der Header vor jeglichem Inhalt stehen muss und nach dem Senden dieses Headers Dinge getan werden müssen. Es handelt sich nicht um eine sofortige Umleitung und einen Code danach Wird die meiste Zeit ausgeführt, aber Sie haben keine Garantie dafür, dass der Browser die Verbindung nicht trennt.
Julien Roncaglia
Jords: Ich wusste nicht, dass Apache dies auch unterstützt. Ich werde dies zu meiner Antwort hinzufügen, wenn ich Zeit habe. Das einzige Problem dabei ist, dass ich nicht einheitlich bin (z. B. X-Accel-Redirect nginx), sodass eine zweite Lösung erforderlich ist, wenn der Server dies entweder nicht unterstützt. Aber ich sollte es meiner Antwort hinzufügen.
Julien Roncaglia
Wo kann ich .htaccess erlauben, den XSendFilePath zu steuern?
Keyne Viana
1
@ Keyne Ich glaube nicht, dass du kannst. tn123.org/mod_xsendfile nicht Liste .htaccess im Rahmen der XSendFilePath Option
cheshirekow
33

Der schnellste Weg: Nicht. Schauen Sie in den x-sendfile-Header für nginx , es gibt ähnliche Dinge auch für andere Webserver. Dies bedeutet, dass Sie weiterhin Zugriffskontrolle usw. in PHP durchführen können, aber das eigentliche Senden der Datei an einen dafür vorgesehenen Webserver delegieren können.

PS: Ich bekomme Schüttelfrost, wenn ich nur daran denke, wie viel effizienter dies mit Nginx ist, verglichen mit dem Lesen und Senden der Datei in PHP. Stellen Sie sich vor, 100 Leute laden eine Datei herunter: Mit PHP + Apache, großzügig, das sind wahrscheinlich 100 * 15 MB = 1,5 GB (ungefähr, erschießen Sie mich) RAM genau dort. Nginx gibt die Datei einfach an den Kernel weiter und lädt sie dann direkt von der Festplatte in die Netzwerkpuffer. Schnell!

PPS: Und mit dieser Methode können Sie immer noch alle gewünschten Zugriffskontroll- und Datenbankaufgaben erledigen.

Jords
quelle
4
Lassen Sie mich nur hinzufügen, dass dies auch für Apache existiert: jasny.net/articles/how-i-php-x-sendfile . Sie können das Skript dazu bringen, den Server zu beschnüffeln und die entsprechenden Header zu senden. Wenn keine vorhanden sind (und der Benutzer gemäß der Frage keine Kontrolle über den Server hat), greifen Sie auf einen normalen readfile()
Wert
Das ist einfach fantastisch - ich habe es immer gehasst, das Speicherlimit in meinen virtuellen Hosts zu erhöhen, nur damit PHP eine Datei bereitstellt, und damit sollte ich nicht müssen. Ich werde es sehr bald ausprobieren.
Greg W
1
Und für Kredite, bei denen Kredite fällig sind, war Lighttpd der erste Webserver, der dies implementiert hat (und der Rest hat sie kopiert, was in Ordnung ist, da es eine großartige Idee ist. Aber geben Sie Kredite, bei denen Kredite fällig sind) ...
ircmaxell
1
Diese Antwort wird immer wieder aktualisiert, funktioniert jedoch nicht in einer Umgebung, in der der Webserver und seine Einstellungen außerhalb der Kontrolle des Benutzers liegen.
Kirk Ouimet
Sie haben das tatsächlich zu Ihrer Frage hinzugefügt, nachdem ich diese Antwort gepostet habe. Und wenn die Leistung ein Problem darstellt, muss der Webserver unter Ihrer Kontrolle sein.
Jords
23

Hier geht eine reine PHP-Lösung. Ich habe die folgende Funktion aus meinem persönlichen Rahmen angepasst :

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

Der Code ist so effizient wie möglich. Er schließt den Sitzungshandler, sodass andere PHP-Skripte gleichzeitig für denselben Benutzer / dieselbe Sitzung ausgeführt werden können. Es unterstützt auch das Bereitstellen von Downloads in Bereichen (was Apache vermutlich auch standardmäßig tut), sodass Benutzer Downloads pausieren / fortsetzen und von höheren Download-Geschwindigkeiten mit Download-Beschleunigern profitieren können. Außerdem können Sie die maximale Geschwindigkeit (in Kbit / s) angeben, mit der der Download (Teil) über das $speedArgument bereitgestellt werden soll .

Alix Axel
quelle
2
Dies ist natürlich nur dann eine gute Idee, wenn Sie X-Sendfile oder eine seiner Varianten nicht verwenden können, damit der Kernel die Datei sendet. Sie sollten in der Lage sein, die obige Schleife feof () / fread () durch den Aufruf [ php.net/manual/en/function.eio-sendfile.php weibl. ( PHPs eio_sendfile ()] zu ersetzen , der dasselbe in PHP bewirkt. Dies ist nicht so schnell wie direkt im Kernel, da jede in PHP generierte Ausgabe immer noch über den Webserver-Prozess ausgeführt werden muss, aber es wird viel schneller sein als in PHP-Code.
Brian C.
@BrianC: Sicher, aber Sie können die Geschwindigkeit oder die Mehrteiligkeit mit X-Sendfile (das möglicherweise nicht verfügbar ist) nicht einschränken und eioist auch nicht immer verfügbar. Trotzdem wusste +1 nichts von dieser Pecl-Erweiterung. =)
Alix Axel
Wäre es nützlich, die Übertragungscodierung: Chunked und die Inhaltscodierung: gzip zu unterstützen?
Skibulk
Warum $size = sprintf('%u', filesize($path))?
Svish
14
header('Location: ' . $path);
exit(0);

Lassen Sie Apache die Arbeit für Sie erledigen.

Amphetamachine
quelle
12
Das ist einfacher als die x-sendfile-Methode, beschränkt jedoch nicht den Zugriff auf eine Datei, dh nur angemeldete Personen. Wenn Sie das nicht tun müssen, dann ist es großartig!
Jords
Fügen Sie auch eine Referrer-Prüfung mit mod_rewrite hinzu.
Sanmai
1
Sie können authentifizieren, bevor Sie den Header übergeben. Auf diese Weise pumpen Sie auch nicht Tonnen von Dingen durch den Speicher von PHP.
Brent
7
@UltimateBrent Der Standort muss immer noch für alle zugänglich sein. Und eine Referenzprüfung ist überhaupt keine Sicherheit, da sie vom Kunden stammt
Øyvind Skaar
@ Jimbo Ein Benutzertoken, das Sie überprüfen möchten, wie? Mit PHP? Plötzlich wiederholt sich Ihre Lösung.
Mark Amery
1

Eine bessere Implementierung mit Cache-Unterstützung und angepassten http-Headern.

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;
    }
}
shawn
quelle
0

Wenn Sie die Möglichkeit haben, PECL-Erweiterungen zu Ihrem PHP hinzuzufügen, können Sie einfach die Funktionen aus dem Fileinfo-Paket verwenden , um den Inhaltstyp zu bestimmen und dann die richtigen Header zu senden ...

Andreas Linden
quelle
Ich habe diese Möglichkeit erwähnt. :)
Andreas Linden
0

Die Downloadhier erwähnte PHP- Funktion verursachte einige Verzögerungen, bevor die Datei tatsächlich heruntergeladen wurde. Ich weiß nicht , ob dies durch die Verwendung Lack - Cache verursacht wurde oder was, aber für mich half es die entfernen sleep(1);vollständig und Satz $speedzu 1024. Jetzt funktioniert es ohne Probleme, so schnell wie die Hölle. Vielleicht können Sie diese Funktion auch ändern, weil ich gesehen habe, dass sie im gesamten Internet verwendet wird.

user1601422
quelle
0

Ich habe eine sehr einfache Funktion codiert, um Dateien mit PHP und automatischer MIME-Typerkennung bereitzustellen:

function serve_file($filepath, $new_filename=null) {
    $filename = basename($filepath);
    if (!$new_filename) {
        $new_filename = $filename;
    }
    $mime_type = mime_content_type($filepath);
    header('Content-type: '.$mime_type);
    header('Content-Disposition: attachment; filename="downloaded.pdf"');
    readfile($filepath);
}

Verwendung

serve_file("/no_apache/invoice243.pdf");
Samuel Dauzon
quelle