PHP Kullanarak Dosya Sunmanın En Hızlı Yolu


99

Bir dosya yolunu alan, ne olduğunu tanımlayan, uygun başlıkları ayarlayan ve onu Apache'nin yapacağı gibi sunan bir işlev oluşturmaya çalışıyorum.

Bunu yapmamın nedeni, dosyayı sunmadan önce istek hakkındaki bazı bilgileri işlemek için PHP'yi kullanmam gerektiğidir.

Hız kritiktir

sanal () bir seçenek değil

Kullanıcının web sunucusu üzerinde hiçbir kontrolünün olmadığı paylaşılan bir barındırma ortamında çalışmalıdır (Apache / nginx, vb.)

Şimdiye kadar sahip olduğum şeyler:

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
Apache'nin bunu yapmasına neden izin vermiyorsun? Her zaman PHP yorumlayıcısını başlatmaktan çok daha hızlı olacaktır ...
Billy ONeal

4
Dosyayı çıkarmadan önce isteği işlemem ve bazı bilgileri veritabanında depolamam gerekiyor.
Kirk Ouimet

3
Uzantıyı daha pahalı normal ifadeler olmadan elde etmenin bir yolunu önerebilir miyim: $extension = end(explode(".", $pathToFile))veya bunu substr ve strrpos: ile yapabilirsiniz $extension = substr($pathToFile, strrpos($pathToFile, '.')). Ayrıca, geri dönüş olarak mime_content_type(), bir sistem çağrısını deneyebilirsiniz:$mimetype = exec("file -bi '$pathToFile'", $output);
Fanis Hatzidakis

En hızlı derken neyi kastediyorsunuz ? En hızlı indirme süresi?
Alix Axel

Yanıtlar:


140

Önceki cevabım kısmi idi ve iyi belgelenmemişti, burada ondan ve tartışmadaki diğerlerinden çözümlerin bir özetini içeren bir güncelleme var.

Çözümler, en iyi çözümden en kötüsüne, aynı zamanda web sunucusu üzerinde en fazla kontrole ihtiyaç duyan çözümden daha azına ihtiyaç duyana kadar sıralanır. Hem hızlı hem de her yerde çalışan tek bir çözüme sahip olmanın kolay bir yolu yok gibi görünüyor.


X-SendFile başlığını kullanma

Başkaları tarafından belgelendiği gibi, aslında en iyi yoldur. Bunun temeli, erişim kontrolünüzü php'de yapmanız ve ardından dosyayı kendiniz göndermek yerine web sunucusuna yapmasını söylemenizdir.

Temel php kodu:

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

$file_nameDosya sistemindeki tam yol nerede .

Bu çözümle ilgili temel sorun, web sunucusu tarafından izin verilmesi gerekmesi ve varsayılan olarak yüklenmemesi (apache), varsayılan olarak etkin olmaması (lighttpd) veya belirli bir yapılandırmaya (nginx) ihtiyaç duymasıdır.

Apaçi

Apache altında, mod_php kullanıyorsanız mod_xsendfile adlı bir modül kurmanız ve ardından onu yapılandırmanız gerekir (izin verirseniz apache config veya .htaccess içinde)

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

Bu modülle dosya yolu mutlak veya belirtilene göre olabilir XSendFilePath.

Lighttpd

Mod_fastcgi, ile yapılandırıldığında bunu destekler

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

Özelliğin belgeleri lighttpd wiki üzerindedir , X-LIGHTTPD-send-filebaşlığı belgelerler ancak X-Sendfilead da çalışır

Nginx

Nginx'te X-Sendfilebaşlığı kullanamazsınız, adlandırılmış kendi başlıklarını kullanmanız gerekir X-Accel-Redirect. Varsayılan olarak etkindir ve tek gerçek farkı, bağımsız değişkeninin dosya sistemi değil URI olması gerektiğidir. Sonuç olarak, istemcilerin gerçek dosya url'sini bulmasını ve doğrudan ona gitmesini önlemek için yapılandırmanızda dahili olarak işaretlenmiş bir konum tanımlamanız gerekir, wiki'leri bunun iyi bir açıklamasını içerir.

Sembolik Bağlantılar ve Konum başlığı

Sembolik bağları kullanabilir ve bunlara yeniden yönlendirebilirsiniz, bir kullanıcı bir dosyaya erişme yetkisi verildiğinde dosyanıza rastgele adlarla sembolik bağlantılar oluşturabilir ve aşağıdakileri kullanarak kullanıcıyı yeniden yönlendirebilirsiniz:

header("Location: " . $url_of_symlink);

Açıkçası, onları oluşturmak için komut dosyası çağrıldığında veya cron aracılığıyla (erişiminiz varsa makinede veya başka türlü bir webcron hizmeti aracılığıyla) bunları budamanın bir yoluna ihtiyacınız olacak.

Apache altında FollowSymLinks, bir .htaccessveya apache yapılandırmasında etkinleştirebilmeniz gerekir .

IP ve Konum başlığına göre erişim kontrolü

Diğer bir hack, açık kullanıcı IP'sine izin veren php'den apache erişim dosyaları oluşturmaktır. Apache altında, mod_authz_host( mod_access) Allow fromkomutlarının kullanılması anlamına gelir .

Sorun, dosyaya erişimi kilitlemenin (birden fazla kullanıcının aynı anda yapmak isteyebileceği için) önemsiz olmaması ve bazı kullanıcıların uzun süre beklemesine neden olabilmesidir. Ve yine de dosyayı budamanız gerekiyor.

Açıkçası başka bir sorun, aynı IP'nin arkasındaki birden fazla kişinin potansiyel olarak dosyaya erişebilmesidir.

Her şey başarısız olduğunda

Web sunucunuzun size yardımcı olmasını sağlamak için gerçekten herhangi bir yolunuz yoksa, kalan tek çözüm, şu anda kullanımda olan tüm php sürümlerinde mevcut olan ve oldukça iyi çalışan (ama gerçekten verimli değil) okuma dosyasıdır .


Çözümleri birleştirmek

Aslında, php kodunuzun her yerde kullanılabilir olmasını istiyorsanız, bir dosyayı gerçekten hızlı göndermenin en iyi yolu, web sunucusuna bağlı olarak nasıl etkinleştirileceğine dair talimatlar ve belki de kurulumunuzda otomatik algılama ile yapılandırılabilir bir seçeneğe sahip olmaktır. senaryo.

Birçok yazılımda yapılana oldukça benzer.

  • Url'leri temizle ( mod_rewriteapache'de)
  • Kripto fonksiyonları ( mcryptphp modülü)
  • Çok baytlı dizi desteği ( mbstringphp modülü)

Yapmadan önce bazı PHP işlerini yapmakla ilgili herhangi bir sorun var mı (veritabanına karşı çerez / diğer GET / POST parametrelerini kontrol edin) header("Location: " . $path);?
Afriza N. Arief

2
Bu tür bir eylem için sorun değil, dikkat etmeniz gereken şey içerik göndermektir (yazdırma, yankı) çünkü başlık herhangi bir içerikten önce gelmelidir ve bu başlığı gönderdikten sonra bir şeyler yapmalıdır, bu hemen bir yönlendirme ve ondan sonraki kod değildir. çoğu zaman çalıştırılır, ancak tarayıcının bağlantıyı kesmeyeceğine dair hiçbir garantiniz yoktur.
Julien Roncaglia

Jords: Apache'nin de bunu desteklediğini bilmiyordum, bunu zamanım olduğunda cevabıma ekleyeceğim. Bununla ilgili tek sorun, birleşik olmamam (örneğin X-Accel-Redirect nginx), bu nedenle sunucu ya desteklemiyorsa ikinci bir çözüme ihtiyaç var. Ama cevabıma eklemeliyim.
Julien Roncaglia

.Htaccess'in XSendFilePath'i kontrol etmesine nerede izin verebilirim?
Keyne Viana

1
@Keyne yapabileceğini sanmıyorum. tn123.org/mod_xsendfile , XSendFilePath seçeneği bağlamında .htaccess'i listelemiyor
cheshirekow

33

En hızlı yol: Yapma. Nginx için x-sendfile başlığına bakın , diğer web sunucuları için de benzer şeyler var. Bu, php'de hala erişim kontrolü vb. Yapabileceğiniz anlamına gelir, ancak dosyanın gerçek gönderimini bunun için tasarlanmış bir web sunucusuna delege edebilirsiniz.

Not: Dosyayı php olarak okumak ve göndermekle karşılaştırıldığında, nginx ile bunu kullanmanın ne kadar verimli olduğunu düşünürken ürperiyorum. Sadece 100 kişinin bir dosya indirdiğini düşünün: php + apache ile cömert olmak, bu muhtemelen 100 * 15mb = 1.5GB (yaklaşık, beni vur), tam orada ram. Nginx, dosyayı çekirdeğe göndermeyi bırakacak ve ardından doğrudan diskten ağ arabelleklerine yükleyecektir. Hızlı!

PPS: Ve bu yöntemle, istediğiniz tüm erişim kontrolü, veritabanı işlerini yine de yapabilirsiniz.


4
Bunun Apache için de mevcut olduğunu eklememe izin verin: jasny.net/articles/how-i-php-x-sendfile . Komut dosyasının sunucuyu koklamasını ve uygun başlıkları göndermesini sağlayabilirsiniz. Hiçbiri exist (ve kullanıcı söz gereği sunucuya üzerinde hiçbir kontrole sahip) ise normal geri düşmekreadfile()
Fanis Hacıdakis

Şimdi bu harika - sanal konaklarımdaki bellek sınırını, PHP'nin bir dosya sunması için yükseltmekten her zaman nefret etmişimdir ve bununla bunu yapmak zorunda kalmamalıyım. Çok yakında deneyeceğim.
Greg W

1
Ve kredinin ödenmesi gereken yerde, Lighttpd bunu uygulayan ilk web sunucusuydu (Ve geri kalanı kopyaladı, bu harika bir fikir olduğu için iyi. Ama kredinin ödenmesi gereken yere kredi verin) ...
ircmaxell

1
Bu cevap oylanmaya devam ediyor, ancak web sunucusunun ve ayarlarının kullanıcının kontrolü dışında olduğu bir ortamda işe yaramayacak.
Kirk Ouimet

Ben bu yanıtı gönderdikten sonra bunu aslında sorunuza eklediniz. Ve performans bir sorunsa, web sunucusunun sizin kontrolünüzde olması gerekir.
Jords

23

İşte saf bir PHP çözümü. Aşağıdaki işlevi kişisel çerçevemden uyarladım :

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

Kod olabildiğince etkilidir, oturum işleyicisini kapatır, böylece diğer PHP betikleri aynı kullanıcı / oturum için aynı anda çalışabilir. Ayrıca, indirmelerin aralıklarda sunulmasını da destekler (ki bu, Apache'nin varsayılan olarak yaptığından şüpheleniyorum), böylece insanlar indirmeleri duraklatabilir / devam ettirebilir ve ayrıca indirme hızlandırıcılarıyla daha yüksek indirme hızlarından yararlanabilir. Ayrıca, indirmenin (bölümün) $speedbağımsız değişken aracılığıyla sunulması gereken maksimum hızı (Kbps cinsinden) belirlemenize olanak tanır .


2
Açıkçası, bu yalnızca X-Sendfile veya türevlerinden birini kernelin dosyayı göndermesi için kullanamıyorsanız iyi bir fikirdir. Yukarıdaki feof () / fread () döngüsünü , PHP'de aynı şeyi yapan [ php.net/manual/en/function.eio-sendfile.php](PHP'nin eio_sendfile ()] çağrısıyla değiştirebilmelisiniz. PHP'de üretilen herhangi bir çıktının hala web sunucusu sürecinden geri dönmesi gerektiğinden, bunu doğrudan çekirdekte yapmak kadar hızlı değil, ancak PHP kodunda yapmaktan çok daha hızlı olacak.
Brian C

@BrianC: Elbette, ancak hızı veya çok parçalı yeteneği X-Sendfile ile sınırlayamazsınız (mevcut olmayabilir) ve eioher zaman mevcut değildir. Yine de +1, o pecl uzantısını bilmiyordu. =)
Alix Axel

Aktarım kodlamasını desteklemek yararlı olur mu: parçalı ve içerik kodlama: gzip?
skibulk

Neden $size = sprintf('%u', filesize($path))?
Svish

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

Bırakın Apache işi sizin için yapsın.


12
Bu, x-sendfile yönteminden daha basittir, ancak bir dosyaya erişimi kısıtlamak için, sadece oturum açmış kişiler demek için işe yaramayacaktır. Bunu yapmana gerek yoksa harika!
Jords

Ayrıca mod_rewrite ile bir yönlendirme denetimi ekleyin.
sanmai

1
Başlığı geçmeden önce yetkilendirebilirsiniz. Bu şekilde, PHP'nin belleğinden tonlarca şey pompalamıyorsunuz.
Brent

7
@UltimateBrent Konum hala herkes tarafından erişilebilir olmalıdır .. Ve bir referans kontrolü istemciden geldiği için hiçbir güvenlik sağlamaz
Øyvind Skaar

@Jimbo Nasıl kontrol edeceğiniz bir kullanıcı jetonu? PHP ile mi? Aniden çözümünüz yineleniyor.
Mark Amery

1

Önbellek desteği, özelleştirilmiş http başlıkları ile daha iyi bir uygulama.

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

DownloadBurada bahsedilen PHP işlevi, dosya gerçekten indirilmeye başlamadan önce biraz gecikmeye neden oluyordu. Bu vernik önbelleği ya da ne kullanarak neden oldu eğer bilmiyorum, ama benim için kaldırmak için yardımcı oldu sleep(1);tamamen ve seti $speediçin 1024. Şimdi cehennem kadar hızlı olduğu kadar sorunsuz çalışıyor. Belki bu işlevi de değiştirebilirsiniz, çünkü tüm internette kullanıldığını gördüm.


0

Dosyaları PHP ve otomatik MIME türü algılamayla sunmak için çok basit bir işlev kodladım:

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

Kullanım

serve_file("/no_apache/invoice243.pdf");
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.