Cara Tercepat untuk Menyajikan File Menggunakan PHP

98

Saya mencoba untuk mengumpulkan fungsi yang menerima jalur file, mengidentifikasi apa itu, menetapkan header yang sesuai, dan melayani seperti yang dilakukan Apache.

Alasan saya melakukan ini adalah karena saya perlu menggunakan PHP untuk memproses beberapa informasi tentang permintaan sebelum menyajikan file.

Kecepatan sangat penting

virtual () bukanlah pilihan

Harus bekerja di lingkungan hosting bersama di mana pengguna tidak memiliki kendali atas server web (Apache / nginx, dll)

Inilah yang saya dapatkan sejauh ini:

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
sumber
10
Mengapa Anda tidak membiarkan Apache melakukan ini? Ini akan selalu jauh lebih cepat daripada memulai penerjemah PHP ...
Billy ONeal
4
Saya perlu memproses permintaan dan menyimpan beberapa informasi dalam database sebelum mengeluarkan file.
Kirk Ouimet
3
Mungkin saya menyarankan cara untuk mendapatkan ekstensi tanpa ekspresi reguler lebih mahal: $extension = end(explode(".", $pathToFile)), atau Anda dapat melakukannya dengan substr dan strrpos: $extension = substr($pathToFile, strrpos($pathToFile, '.')). Selain itu, sebagai pengganti mime_content_type(), Anda dapat mencoba panggilan sistem:$mimetype = exec("file -bi '$pathToFile'", $output);
Fanis Hatzidakis
Apa yang Anda maksud dengan tercepat ? Waktu unduh tercepat?
Alix Axel

Jawaban:

140

Jawaban saya sebelumnya sebagian dan tidak terdokumentasi dengan baik, berikut adalah pembaruan dengan ringkasan solusi darinya dan dari orang lain dalam diskusi.

Solusi tersebut disusun dari solusi terbaik hingga solusi terburuk, tetapi juga dari solusi yang membutuhkan kontrol paling besar atas server web ke solusi yang membutuhkan lebih sedikit. Tampaknya tidak ada cara mudah untuk memiliki satu solusi yang cepat dan berfungsi di mana saja.


Menggunakan header X-SendFile

Seperti yang didokumentasikan oleh orang lain, ini sebenarnya cara terbaik. Dasarnya adalah Anda melakukan kontrol akses di php dan alih-alih mengirim file sendiri, Anda memberi tahu server web untuk melakukannya.

Kode php dasarnya adalah:

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

Di mana $file_namejalur lengkap pada sistem file.

Masalah utama dengan solusi ini adalah ia harus diizinkan oleh server web dan tidak diinstal secara default (apache), tidak aktif secara default (lighttpd) atau memerlukan konfigurasi khusus (nginx).

Apache

Di bawah apache jika Anda menggunakan mod_php Anda perlu menginstal modul bernama mod_xsendfile kemudian mengkonfigurasinya (baik di konfigurasi apache atau .htaccess jika Anda mengizinkannya)

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

Dengan modul ini, jalur file bisa absolut atau relatif terhadap yang ditentukan XSendFilePath.

Lighttpd

Mod_fastcgi mendukung ini ketika dikonfigurasi dengan

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

Dokumentasi untuk fitur ini ada di wiki lighttpd, mereka mendokumentasikan X-LIGHTTPD-send-fileheader tetapi X-Sendfilenama juga berfungsi

Nginx

Di Nginx Anda tidak dapat menggunakan X-Sendfileheader Anda harus menggunakan header mereka sendiri yang diberi nama X-Accel-Redirect. Ini diaktifkan secara default dan satu-satunya perbedaan nyata adalah argumennya harus berupa URI, bukan sistem file. Konsekuensinya adalah Anda harus menentukan lokasi yang ditandai sebagai internal dalam konfigurasi Anda untuk menghindari klien menemukan url file yang sebenarnya dan langsung pergi ke sana, wiki mereka berisi penjelasan yang baik tentang ini.

Symlinks dan tajuk Lokasi

Anda dapat menggunakan symlink dan mengarahkan ke mereka, cukup buat symlink ke file Anda dengan nama acak ketika pengguna diberi otorisasi untuk mengakses file dan mengarahkan pengguna ke sana menggunakan:

header("Location: " . $url_of_symlink);

Jelas Anda memerlukan cara untuk memangkasnya baik ketika skrip untuk membuatnya dipanggil atau melalui cron (di mesin jika Anda memiliki akses atau melalui beberapa layanan webcron)

Di bawah apache, Anda harus dapat mengaktifkannya FollowSymLinksdi .htaccessatau di konfigurasi apache.

Kontrol akses dengan IP dan header Lokasi

Hack lain adalah menghasilkan file akses apache dari php yang memungkinkan IP pengguna eksplisit. Di bawah apache itu berarti menggunakan mod_authz_host( mod_access) Allow fromperintah.

Masalahnya adalah bahwa mengunci akses ke file (karena beberapa pengguna mungkin ingin melakukan ini pada saat yang sama) tidak sepele dan dapat menyebabkan beberapa pengguna menunggu lama. Dan Anda tetap perlu memangkas file tersebut.

Jelas masalah lain adalah bahwa banyak orang di belakang IP yang sama berpotensi mengakses file tersebut.

Ketika semuanya gagal

Jika Anda benar-benar tidak memiliki cara untuk mendapatkan server web Anda untuk membantu Anda, satu-satunya solusi yang tersisa adalah readfile yang tersedia di semua versi php yang saat ini digunakan dan bekerja dengan cukup baik (tetapi tidak terlalu efisien).


Menggabungkan solusi

Baik-baik saja, cara terbaik untuk mengirim file dengan sangat cepat jika Anda ingin kode php Anda dapat digunakan di mana-mana adalah dengan memiliki opsi yang dapat dikonfigurasi di suatu tempat, dengan instruksi tentang cara mengaktifkannya tergantung pada server web dan mungkin deteksi otomatis di instalasi Anda naskah.

Ini sangat mirip dengan apa yang dilakukan di banyak perangkat lunak untuk

  • Bersihkan url ( mod_rewritedi apache)
  • Fungsi Crypto ( mcryptmodul php)
  • Dukungan string multibyte ( mbstringmodul php)
Julien Roncaglia
sumber
Apakah ada masalah dengan melakukan beberapa pekerjaan PHP (periksa cookie / parameter GET / POST lain terhadap database) sebelum melakukannya header("Location: " . $path);?
Afriza N. Arief
2
Tidak masalah untuk tindakan seperti itu, hal yang perlu Anda perhatikan adalah mengirim konten (cetak, gema) karena header harus ada sebelum konten apa pun dan melakukan sesuatu setelah mengirim header ini, ini bukan pengalihan langsung dan kode setelah itu akan dilakukan. dieksekusi hampir sepanjang waktu tetapi Anda tidak memiliki jaminan bahwa browser tidak akan memutus koneksi.
Julien Roncaglia
Jords: Saya tidak tahu bahwa apache juga mendukung ini, saya akan menambahkan ini ke jawaban saya ketika saya punya waktu. Satu-satunya masalah dengan itu adalah saya tidak bersatu (X-Accel-Redirect nginx misalnya) jadi solusi kedua diperlukan jika server tidak mendukungnya. Tetapi saya harus menambahkannya ke jawaban saya.
Julien Roncaglia
Di mana saya dapat mengizinkan .htaccess untuk mengontrol XSendFilePath?
Keyne Viana
1
@ Saya rasa Anda tidak bisa. tn123.org/mod_xsendfile tidak mencantumkan .htaccess dalam konteks opsi XSendFilePath
cheshirekow
33

Cara tercepat: Jangan. Lihat header x-sendfile untuk nginx , ada juga hal serupa untuk server web lain. Ini berarti bahwa Anda masih dapat melakukan kontrol akses dll di php tetapi mendelegasikan pengiriman sebenarnya dari file tersebut ke server web yang dirancang untuk itu.

PS: Saya merasa merinding hanya memikirkan seberapa efisien menggunakan ini dengan nginx, dibandingkan dengan membaca dan mengirim file dalam php. Bayangkan jika 100 orang mengunduh file: Dengan php + apache, menjadi murah hati, itu mungkin 100 * 15mb = 1,5GB (kira-kira, tembak saya), dari ram di sana. Nginx hanya akan mengirimkan file ke kernel, dan kemudian dimuat langsung dari disk ke buffer jaringan. Cepat!

PPS: Dan, dengan metode ini Anda masih dapat melakukan semua kontrol akses, database yang Anda inginkan.

Jords
sumber
4
Izinkan saya menambahkan bahwa ini juga ada untuk Apache: jasny.net/articles/how-i-php-x-sendfile . Anda dapat membuat skrip mengendus server dan mengirim header yang sesuai. Jika tidak ada (dan pengguna tidak memiliki kendali atas server sesuai pertanyaan), kembali ke normalreadfile()
Fanis Hatzidakis
Sekarang ini luar biasa - saya selalu benci menaikkan batas memori di host virtual saya hanya agar PHP akan menyajikan file, dan dengan ini saya tidak perlu melakukannya. Saya akan segera mencobanya.
Greg W
1
Dan untuk kredit di mana kredit jatuh tempo, Lighttpd adalah server web pertama yang menerapkan ini (Dan sisanya menyalinnya, yang bagus karena itu ide yang bagus. Tapi berikan kredit jika kredit jatuh tempo) ...
ircmaxell
1
Jawaban ini terus mendapat suara positif, tetapi tidak akan berfungsi di lingkungan di mana server web dan pengaturannya berada di luar kendali pengguna.
Kirk Ouimet
Anda benar-benar menambahkannya ke pertanyaan Anda setelah saya memposting jawaban ini. Dan jika kinerja menjadi masalah, maka server web harus berada dalam kendali Anda.
Jords
23

Ini dia solusi PHP murni. Saya telah mengadaptasi fungsi berikut dari kerangka pribadi saya :

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

Kode ini seefisien mungkin, ia menutup penangan sesi sehingga skrip PHP lain dapat berjalan secara bersamaan untuk pengguna / sesi yang sama. Ini juga mendukung penyajian unduhan dalam rentang (yang juga secara default dilakukan Apache), sehingga orang dapat menjeda / melanjutkan unduhan dan juga mendapatkan keuntungan dari kecepatan unduh yang lebih tinggi dengan akselerator unduhan. Ini juga memungkinkan Anda untuk menentukan kecepatan maksimum (dalam Kbps) di mana unduhan (bagian) harus disajikan melalui $speedargumen.

Alix Axel
sumber
2
Jelas ini hanya ide yang bagus jika Anda tidak dapat menggunakan X-Sendfile atau salah satu variasinya untuk meminta kernel mengirim file. Anda seharusnya bisa mengganti loop feof () / fread () di atas dengan [ php.net/manual/en/function.eio-sendfile.php](PHP's eio_sendfile ()] panggilan, yang menyelesaikan hal yang sama di PHP. Ini tidak secepat melakukannya secara langsung di kernel, karena output apa pun yang dihasilkan di PHP masih harus keluar melalui proses server web, tetapi ini akan menjadi jauh lebih cepat daripada melakukannya dalam kode PHP.
Brian C
@ BrianC: Tentu, tetapi Anda tidak dapat membatasi kecepatan atau kemampuan multi bagian dengan X-Sendfile (yang mungkin tidak tersedia) dan eiojuga tidak selalu tersedia. Tetap saja, +1, tidak tahu tentang ekstensi pecl itu. =)
Alix Axel
Apakah akan berguna untuk mendukung transfer-encoding: chunked dan content-encoding: gzip?
skibulk
Kenapa $size = sprintf('%u', filesize($path))?
Svish
14
header('Location: ' . $path);
exit(0);

Biarkan Apache yang bekerja untuk Anda.

amfetamachine
sumber
12
Itu lebih sederhana daripada metode x-sendfile, tetapi tidak akan berfungsi untuk membatasi akses ke file, katakanlah hanya orang yang masuk. Jika Anda tidak perlu melakukan itu maka itu bagus!
Jords
Tambahkan juga cek pengarah dengan mod_rewrite.
sanmai
1
Anda dapat mengautentikasi sebelum meneruskan header. Dengan cara itu Anda juga tidak memompa banyak barang melalui memori PHP.
Brent
7
@UltimateBrent Lokasi masih harus dapat diakses oleh semua .. Dan pemeriksaan referensi tidak ada keamanan sama sekali karena berasal dari klien
Øyvind Skaar
@Jimbo Token pengguna yang akan Anda periksa bagaimana caranya? Dengan PHP? Tiba-tiba solusi Anda berulang.
Mark Amery
1

Implementasi yang lebih baik, dengan dukungan cache, header http yang disesuaikan.

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
sumber
0

jika Anda memiliki kemungkinan untuk menambahkan ekstensi PECL ke php Anda, Anda cukup menggunakan fungsi dari paket Fileinfo untuk menentukan tipe konten dan kemudian mengirim header yang tepat ...

Andreas Linden
sumber
/ bump, sudahkah Anda menyebutkan kemungkinan ini? :)
Andreas Linden
0

Fungsi PHP yang Downloaddisebutkan di sini menyebabkan beberapa penundaan sebelum file benar-benar mulai diunduh. Saya tidak tahu apakah ini disebabkan dengan menggunakan pernis cache atau apa, tapi bagi saya itu membantu untuk menghapus sleep(1);sepenuhnya dan set $speedke 1024. Sekarang bekerja tanpa masalah secepat neraka. Mungkin Anda bisa memodifikasi fungsi itu juga, karena saya melihatnya digunakan di seluruh internet.

pengguna1601422
sumber
0

Saya membuat kode fungsi yang sangat sederhana untuk melayani file dengan PHP dan deteksi tipe MIME otomatis:

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

Pemakaian

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