PHPを使用してファイルを提供する最速の方法


98

私はファイルパスを受け取り、それが何であるかを識別し、適切なヘッダーを設定し、Apacheと同じように機能する関数をまとめようとしています。

これを行う理由は、ファイルを提供する前に、PHPを使用してリクエストに関する情報を処理する必要があるためです。

スピードが重要

virtual()はオプションではありません

ユーザーがWebサーバーを制御できない共有ホスティング環境で動作する必要があります(Apache / nginxなど)

これが私がこれまでに得たものです:

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], '.');
    }
}
}
?>

10
なぜApacheにこれをさせないのですか?PHPインタープリターを起動するよりも常にかなり高速になります...
Billy ONeal

4
ファイルを出力する前に、リクエストを処理してデータベースに情報を保存する必要があります。
カークウイメット

3
より高価な正規表現なしで拡張機能を取得する方法を提案$extension = end(explode(".", $pathToFile))できますか:または、substrとstrrpos:でそれを行うことができます$extension = substr($pathToFile, strrpos($pathToFile, '.'))。また、へのフォールバックとしてmime_content_type()、あなたは、システムコールを試すことができます:$mimetype = exec("file -bi '$pathToFile'", $output);
Fanisハジダキス

最速とはどういう意味ですか?最速のダウンロード時間?
Alix Axel

回答:


140

私の以前の回答は部分的であり、十分に文書化されていませんでした。ここでは、その回答と他のディスカッションからの解決策の要約を更新します。

ソリューションは、最高のソリューションから最悪のソリューションへと並べられますが、Webサーバーを最も制御する必要があるソリューションから、必要とするソリューションまで順番に並べられます。高速でどこでも機能する1つのソリューションを実現する簡単な方法はないようです。


X-SendFileヘッダーの使用

他の人が文書化したように、それが実際に最良の方法です。基本は、phpでアクセス制御を行い、自分でファイルを送信する代わりに、Webサーバーにそれを行うように指示することです。

基本的なphpコードは次のとおりです。

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

$file_nameファイルシステムの完全パスはどこにありますか。

このソリューションの主な問題は、Webサーバーによって許可される必要があり、デフォルトでインストールされていない(apache)、デフォルトでアクティブになっていない(lighttpd)、または特定の構成が必要な(nginx)ことです。

Apache

Apacheの下でmod_phpを使用する場合、mod_xsendfileというモジュールをインストールして構成する必要があります(許可する場合は、apache configまたは.htaccessで)。

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

このモジュールを使用すると、ファイルパスは絶対パスまたは指定したパスの相対パスになりますXSendFilePath

Lighttpd

mod_fastcgiは、

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

機能のドキュメントはlighttpd wikiにあり、X-LIGHTTPD-send-fileヘッダーをドキュメント化していますが、X-Sendfile名前も機能します

Nginx

NginxではX-Sendfileヘッダーを使用できませんX-Accel-Redirect。という名前の独自のヘッダーを使用する必要があります。これはデフォルトで有効になっており、唯一の違いは、引数がファイルシステムではなくURIであることです。その結果、クライアントが実際のファイルのURLを見つけて直接アクセスするのを防ぐために、構成で内部としてマークされた場所を定義する必要があります。クライアントのWikiにはこれに関する適切な説明が含まれています。

SymlinksとLocationヘッダー

シンボリックリンクを使用してそれらにリダイレクトできます。ユーザーがファイルへのアクセスを許可されているときにランダムな名前でファイルへのシンボリックリンクを作成し、次のコマンドを使用してユーザーをファイルにリダイレクトできます。

header("Location: " . $url_of_symlink);

明らかに、それらを作成するスクリプトが呼び出されたとき、またはcronを介して(アクセス権がある場合はマシン上で、そうでない場合はいくつかのwebcronサービスを介して)、それらをプルーニングする方法が必要です。

Apacheの下FollowSymLinksでは、.htaccessまたはApache設定で有効にする必要があります。

IPおよびLocationヘッダーによるアクセス制御

別のハックは、明示的なユーザーIPを許可するphpからApacheアクセスファイルを生成することです。Apacheでは、mod_authz_hostmod_accessAllow fromコマンドを使用することを意味します。

問題は、ファイルへのアクセスのロック(複数のユーザーが同時にこれを実行したい場合がある)は簡単ではなく、一部のユーザーが長時間待機する可能性があることです。とにかく、ファイルを削除する必要があります。

明らかに、別の問題は、同じIPの背後にいる複数の人がファイルにアクセスする可能性があることです。

他のすべてが失敗したとき

あなたが本当にあなたのウェブサーバーを手助けする方法がないなら、残っている唯一の解決策は、現在使用されているすべてのphpバージョンで利用可能であり、かなりうまく動作するreadfileです(しかし本当に効率的ではありません)。


ソリューションの組み合わせ

phpコードをどこでも使用できるようにしたい場合、ファイルを本当に速く送信するための最善の方法は、どこかに構成可能なオプションを設定することです。Webサーバーに応じてファイルをアクティブ化する方法と、インストール時に自動検出する方法があります。脚本。

それは多くのソフトウェアで行われていることとかなり似ています

  • クリーンなURL(mod_rewriteApache上)
  • 暗号関数(mcryptphpモジュール)
  • マルチバイト文字列のサポート(mbstringphpモジュール)

実行する前に、いくつかのPHPの動作(データベースに対してcookie /その他のGET / POSTパラメータを確認する)を実行することに問題はありますheader("Location: " . $path);か?
Afriza N. Arief

2
そのようなアクションには問題ありません。ヘッダーはコンテンツの前に来る必要があり、このヘッダーを送信した後に何かを行う必要があるため、コンテンツ(印刷、エコー)を送信する際に注意する必要があります。ほとんどの場合実行されますが、ブラウザが接続を切断しないという保証はありません。
Julien Roncaglia

Jords:apacheもこれをサポートしていることを知りませんでした。時間があれば、これを回答に追加します。それに関する唯一の問題は、iが統合されていないこと(たとえば、X-Accel-Redirect nginx)なので、サーバーがサポートしていない場合は、2番目のソリューションが必要です。しかし、私はそれを私の答えに追加する必要があります。
ジュリアンロンカリア

.htaccessがXSendFilePathを制御できるようにするにはどうすればよいですか?
Keyne Viana、

1
@Keyneできるとは思いません。tn123.org/mod_xsendfileは XSendFilePathオプションのコンテキストではないリストの.htaccessない
cheshirekow

33

最速の方法:しないでください。見てnginxのためのx-sendfileのヘッダ、他のWebサーバのための同様のものもあります。これは、phpでアクセス制御などを行うことはできますが、そのために設計されたWebサーバーにファイルの実際の送信を委任することを意味します。

PS:nginxでこれを使用すると、phpでファイルを読み取ったり送信したりするのに比べて、どれほど効率的かを考えているだけで、ひどい目に遭います。100人がファイルをダウンロードしている場合を考えてみてください。php+ apacheを使用すると、たぶん100 * 15mb = 1.5GB(おおよそ、私を撃ちます)のRAMがあります。Nginxはファイルをカーネルに送信するだけで、ディスクからネットワークバッファーに直接ロードされます。スピーディー!

PPS:そして、この方法を使用すれば、必要なすべてのアクセス制御やデータベース処理を実行できます。


4
これはApacheにも存在することを追加しましょう:jasny.net/articles/how-i-php-x-sendfile。スクリプトでサーバーを探知し、適切なヘッダーを送信できます。何も存在しない場合(およびユーザーが質問に従ってサーバーを制御できない場合)、通常に戻りますreadfile()
Fanis Hatzidakis

さて、これはすごいです。PHPがファイルを提供できるように、仮想ホストのメモリ制限を増やすことは常に嫌いでしたが、これを使用する必要はありません。もうすぐやってみます。
グレッグW

1
そして、クレジットの
期日が到来

1
この回答は常に賛成票を集めていますが、Webサーバーとその設定がユーザーの制御の及ばない環境では機能しません。
カークウイメット

私がこの回答を投稿した後、実際にそれを質問に追加しました。また、パフォーマンスが問題になる場合は、Webサーバーを管理する必要があります。
ジョーズ、

23

純粋なPHPソリューションを紹介します。私の個人的なフレームワークから次の機能を採用しました:

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

コードは可能な限り効率的であり、セッションハンドラーを閉じ、他のPHPスクリプトが同じユーザー/セッションで同時に実行できるようにします。また、範囲内でダウンロードを提供することもサポートします(これは、Apacheがデフォルトで行うことでもあると思います)。ダウンロードを一時停止/再開したり、ダウンロードアクセラレータを使用してダウンロード速度を向上したりできます。また、$speed引数(ダウンロード)が提供される最大速度(Kbps)を引数で指定することもできます。


2
明らかに、これは、X-Sendfileまたはそのバリアントの1つを使用してカーネルにファイルを送信させることができない場合にのみ有効です。上記のfeof()/ fread()ループを[ php.net/manual/en/function.eio-sendfile.php](PHPの eio_sendfile()]呼び出しで置き換えることができるはずです。これにより、PHPで同じことを実行できます。これはカーネルで直接実行するほど高速ではありません。PHPで生成された出力は、依然としてwebserverプロセスを経由して戻る必要があるためですが、PHPコードで実行するよりもはるかに高速です
Brian C

@BrianC:もちろんですが、X-Sendfile(これは使用できない場合があります)を使用して速度やマルチパート機能を制限することはできませeioん。また、常に使用できるとは限りません。それでも、+ 1はそのpecl拡張については知りませんでした。=)
Alix Axel

transfer-encoding:chunkedおよびcontent-encoding:gzipをサポートすることは有用でしょうか?
Skibulk 2014年

なんで$size = sprintf('%u', filesize($path))
Svish

14
header('Location: ' . $path);
exit(0);

Apacheに任せてください。


12
これはx-sendfileメソッドよりも単純ですが、ファイルへのアクセスを制限するようには機能しません。あなたがそれをする必要がないなら、それは素晴らしいです!
ジョーズ

また、mod_rewriteでリファラーチェックを追加します。
sanmai

1
ヘッダーを渡す前に認証できます。そうすれば、PHPのメモリを介して大量のものをポンプすることもありません。
ブレント

7
場所はまだすべてにアクセスできるように持っている...そして、それはクライアントから来ているので、チェックが全く保障されていない参照してください@UltimateBrent
ØyvindSkaar

@Jimbo確認するユーザートークン PHPでは?突然あなたの解決策は再帰的です。
Mark Amery 2014年

1

キャッシュサポート、カスタマイズされたhttpヘッダーを備えたより良い実装。

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

ここで説明したPHP Download関数が原因で、ファイルのダウンロードが実際に開始されるまでに遅延が発生していました。これがワニスキャッシュの使用によるものかどうかはわかりませんが、私にとってはsleep(1);完全に削除してに設定するのに役立ちまし$speed1024。今では問題なく動作し、地獄のように高速です。インターネット全体で使用されているのを見たので、その機能を変更することもできます。


0

PHPと自動MIMEタイプ検出を使用してファイルを提供する非常に単純な関数をコーディングしました。

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

使用法

serve_file("/no_apache/invoice243.pdf");
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.