Cách nhanh nhất để cung cấp tệp bằng PHP


98

Tôi đang cố gắng tập hợp một hàm nhận đường dẫn tệp, xác định nó là gì, đặt tiêu đề thích hợp và phân phát nó giống như Apache.

Lý do tôi đang làm điều này là vì tôi cần sử dụng PHP để xử lý một số thông tin về yêu cầu trước khi phân phát tệp.

Tốc độ là rất quan trọng

virtual () không phải là một tùy chọn

Phải làm việc trong môi trường lưu trữ được chia sẻ nơi người dùng không có quyền kiểm soát máy chủ web (Apache / nginx, v.v.)

Đây là những gì tôi có cho đến nay:

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
Tại sao bạn không để Apache làm điều này? Nó luôn luôn sẽ nhanh hơn đáng kể hơn so với khởi động thông dịch PHP ...
Billy Oneal

4
Tôi cần xử lý yêu cầu và lưu trữ một số thông tin trong cơ sở dữ liệu trước khi xuất tệp.
Kirk Ouimet

3
Tôi có thể đề nghị một cách để có được phần mở rộng mà không có biểu thức thông thường đắt hơn: $extension = end(explode(".", $pathToFile)), hoặc bạn có thể làm điều đó với substr và strrpos: $extension = substr($pathToFile, strrpos($pathToFile, '.')). Ngoài ra, để dự phòng cho mime_content_type()bạn, bạn có thể thử gọi hệ thống:$mimetype = exec("file -bi '$pathToFile'", $output);
Fanis Hatzidakis 17/09/10

Ý bạn là gì nhanh nhất ? Thời gian tải xuống nhanh nhất?
Alix Axel

Câu trả lời:


140

Câu trả lời trước đây của tôi là một phần và không được ghi chép đầy đủ, đây là bản cập nhật với bản tóm tắt các giải pháp từ nó và từ những người khác trong cuộc thảo luận.

Các giải pháp được sắp xếp từ giải pháp tốt nhất đến kém nhất nhưng cũng từ giải pháp cần kiểm soát nhiều nhất đối với máy chủ web đến giải pháp cần ít hơn. Dường như không có cách nào dễ dàng để có một giải pháp vừa nhanh vừa hiệu quả ở mọi nơi.


Sử dụng tiêu đề X-SendFile

Theo tài liệu của những người khác, đó thực sự là cách tốt nhất. Cơ sở là bạn thực hiện kiểm soát truy cập của mình trong php và sau đó thay vì tự gửi tệp, bạn yêu cầu máy chủ web làm điều đó.

Mã php cơ bản là:

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

Trong trường hợp $file_namelà đường dẫn đầy đủ về hệ thống tập tin.

Vấn đề chính với giải pháp này là nó cần được máy chủ web cho phép và không được cài đặt theo mặc định (apache), không hoạt động theo mặc định (lighttpd) hoặc cần một cấu hình cụ thể (nginx).

Apache

Trong apache nếu bạn sử dụng mod_php, bạn cần cài đặt một mô-đun có tên là mod_xsendfile sau đó định cấu hình nó (trong cấu hình apache hoặc .htaccess nếu bạn cho phép)

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

Với mô-đun này, đường dẫn tệp có thể là tuyệt đối hoặc tương đối với chỉ định XSendFilePath.

Lighttpd

Mod_fastcgi hỗ trợ điều này khi được định cấu hình với

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

Tài liệu cho tính năng này có trên wiki lighttpd, họ ghi lại X-LIGHTTPD-send-filetiêu đề nhưng X-Sendfiletên cũng hoạt động

Nginx

Trên Nginx, bạn không thể sử dụng X-Sendfiletiêu đề, bạn phải sử dụng tiêu đề riêng của chúng được đặt tên X-Accel-Redirect. Nó được bật theo mặc định và sự khác biệt thực sự duy nhất là đối số của nó phải là một URI chứ không phải một hệ thống tệp. Kết quả là bạn phải xác định một vị trí được đánh dấu là nội bộ trong cấu hình của mình để tránh khách hàng tìm thấy url tệp thực và truy cập trực tiếp vào nó, wiki của họ có giải thích rõ về điều này.

Tiêu đề liên kết tượng trưng và vị trí

Bạn có thể sử dụng các liên kết tượng trưng và chuyển hướng đến chúng, chỉ cần tạo các liên kết tượng trưng đến tệp của bạn với các tên ngẫu nhiên khi người dùng được phép truy cập tệp và chuyển hướng người dùng đến tệp đó bằng cách sử dụng:

header("Location: " . $url_of_symlink);

Rõ ràng là bạn sẽ cần một cách để cắt chúng khi tập lệnh để tạo chúng được gọi hoặc qua cron (trên máy nếu bạn có quyền truy cập hoặc thông qua một số dịch vụ webcron)

Trong apache, bạn cần có thể kích hoạt FollowSymLinkstrong một .htaccesshoặc trong cấu hình apache.

Kiểm soát truy cập theo IP và tiêu đề Vị trí

Một thủ thuật khác là tạo tệp truy cập apache từ php cho phép IP người dùng rõ ràng. Theo apache, nó có nghĩa là sử dụng các lệnh mod_authz_host( mod_access) Allow from.

Vấn đề là việc khóa quyền truy cập vào tệp (vì nhiều người dùng có thể muốn thực hiện việc này cùng lúc) là không nhỏ và có thể dẫn đến việc một số người dùng phải chờ đợi lâu. Và bạn vẫn cần phải cắt bớt tệp.

Rõ ràng là một vấn đề khác là nhiều người đứng sau cùng một IP có thể có khả năng truy cập tệp.

Khi mọi thứ khác thất bại

Nếu bạn thực sự không có cách nào để nhờ máy chủ web giúp bạn, giải pháp duy nhất còn lại là readfile, nó có sẵn trong tất cả các phiên bản php hiện đang được sử dụng và hoạt động khá tốt (nhưng không thực sự hiệu quả).


Kết hợp các giải pháp

Tốt nhất, cách tốt nhất để gửi một tệp thực sự nhanh nếu bạn muốn mã php của mình có thể sử dụng được ở mọi nơi là có một tùy chọn có thể cấu hình ở đâu đó, với hướng dẫn về cách kích hoạt nó tùy thuộc vào máy chủ web và có thể tự động phát hiện trong cài đặt của bạn. kịch bản.

Nó khá giống với những gì được thực hiện trong nhiều phần mềm

  • Làm sạch các url ( mod_rewritetrên apache)
  • Chức năng tiền điện tử ( mcryptmô-đun php)
  • Hỗ trợ chuỗi multibyte ( mbstringmô-đun php)

Có vấn đề gì khi thực hiện một số hoạt động PHP (kiểm tra cookie / tham số GET / POST khác so với cơ sở dữ liệu) trước khi thực hiện header("Location: " . $path);không?
Afriza N. Arief

2
Không có vấn đề gì đối với hành động như vậy, điều bạn cần phải cẩn thận là gửi nội dung (print, echo) vì tiêu đề phải đứng trước bất kỳ nội dung nào và thực hiện những việc sau khi gửi tiêu đề này, nó không phải là chuyển hướng ngay lập tức và mã sau đó sẽ được thực thi hầu hết thời gian nhưng bạn không có gì đảm bảo rằng trình duyệt sẽ không cắt kết nối.
Julien Roncaglia

Jords: Tôi không biết rằng apache cũng hỗ trợ điều này, tôi sẽ thêm điều này vào câu trả lời của mình khi có thời gian. Vấn đề duy nhất với nó là tôi không hợp nhất (ví dụ: X-Accel-Redirect nginx) vì vậy cần có giải pháp thứ hai nếu máy chủ không hỗ trợ nó. Nhưng tôi nên thêm nó vào câu trả lời của mình.
Julien Roncaglia

Tôi có thể cho phép .htaccess điều khiển XSendFilePath ở đâu?
Keyne Viana

1
@Keyne Tôi không nghĩ bạn có thể. tn123.org/mod_xsendfile không liệt kê .htaccess trong ngữ cảnh cho tùy chọn XSendFilePath
cheshirekow

33

Cách nhanh nhất: Đừng. Nhìn vào tiêu đề x-sendfile cho nginx , cũng có những thứ tương tự đối với các máy chủ web khác. Điều này có nghĩa là bạn vẫn có thể thực hiện kiểm soát truy cập, v.v. trong php nhưng ủy quyền việc gửi tệp thực sự đến máy chủ web được thiết kế cho điều đó.

PS: Tôi cảm thấy ớn lạnh khi nghĩ đến việc sử dụng điều này với nginx hiệu quả hơn bao nhiêu so với việc đọc và gửi tệp bằng php. Chỉ cần nghĩ nếu 100 người đang tải xuống một tệp: Với php + apache, hào phóng, có thể là 100 * 15mb = 1,5GB (ước chừng, bắn tôi), ram ngay tại đó. Nginx sẽ chỉ việc gửi tệp đến hạt nhân, và sau đó nó được tải trực tiếp từ đĩa vào bộ đệm mạng. Nhanh chóng!

PPS: Và, với phương pháp này, bạn vẫn có thể thực hiện tất cả các điều khiển truy cập, cơ sở dữ liệu mà bạn muốn.


4
Hãy để tôi chỉ thêm rằng điều này cũng tồn tại cho Apache: jasny.net/articles/how-i-php-x-sendfile . Bạn có thể làm cho tập lệnh phát hiện ra máy chủ và gửi các tiêu đề thích hợp. Nếu không tồn tại (và người dùng không có quyền kiểm soát máy chủ theo các câu hỏi), rơi trở lại bình thườngreadfile()
Fanis HATZIDAKIS

Bây giờ điều này thật tuyệt vời - tôi luôn ghét tăng giới hạn bộ nhớ trong các máy chủ ảo của mình chỉ để PHP cung cấp một tệp và với điều này thì tôi không cần phải làm vậy. Tôi sẽ sớm dùng thử.
Greg W

1
Và đối với tín dụng mà tín dụng là do, Lighttpd là máy chủ web đầu tiên để thực hiện điều này (và phần còn lại sao chép nó, mà là tốt vì nó là một ý tưởng tuyệt vời Nhưng cung cấp tín dụng mà tín dụng là do.) ...
ircmaxell

1
Câu trả lời này tiếp tục được ủng hộ, nhưng nó sẽ không hoạt động trong môi trường mà máy chủ web và cài đặt của nó nằm ngoài tầm kiểm soát của người dùng.
Kirk Ouimet

Bạn thực sự đã thêm điều đó vào câu hỏi của mình sau khi tôi đăng câu trả lời này. Và nếu hiệu suất là một vấn đề, thì máy chủ web phải nằm trong tầm kiểm soát của bạn.
Jords

23

Đây là một giải pháp PHP thuần túy. Tôi đã điều chỉnh chức năng sau từ khuôn khổ cá nhân của mình :

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

Mã hiệu quả nhất có thể, nó đóng trình xử lý phiên để các tập lệnh PHP khác có thể chạy đồng thời cho cùng một người dùng / phiên. Nó cũng hỗ trợ phân phát tải xuống trong phạm vi (đó cũng là những gì Apache làm theo mặc định mà tôi nghi ngờ), để mọi người có thể tạm dừng / tiếp tục tải xuống và cũng được hưởng lợi từ tốc độ tải xuống cao hơn với trình tăng tốc tải xuống. Nó cũng cho phép bạn chỉ định tốc độ tối đa (tính bằng Kbps) mà tại đó (phần) tải xuống sẽ được phân phát thông qua $speedđối số.


2
Rõ ràng đây chỉ là một ý tưởng hay nếu bạn không thể sử dụng X-Sendfile hoặc một trong các biến thể của nó để yêu cầu hạt nhân gửi tệp. Bạn có thể thay thế vòng lặp feof () / fread () ở trên bằng [ php.net/manual/en/ Chức năng.eio-sendfile.php]( PHP 's eio_sendfile ()], thực hiện điều tương tự trong PHP. Điều này không nhanh bằng thực hiện trực tiếp trong hạt nhân, vì bất kỳ đầu ra nào được tạo bằng PHP vẫn phải quay trở lại thông qua quy trình máy chủ web, nhưng nó sẽ nhanh hơn rất nhiều so với thực hiện trong mã PHP.
Brian C

@BrianC: Chắc chắn rồi, nhưng bạn không thể giới hạn tốc độ hoặc khả năng đa phần với X-Sendfile (có thể không khả dụng) và eiocũng không phải lúc nào cũng có. Tuy nhiên, +1, không biết về tiện ích mở rộng pecl đó. =)
Alix Axel

Nó sẽ hữu ích khi hỗ trợ mã hóa truyền: chunked và mã hóa nội dung: gzip?
skibulk

Tại sao $size = sprintf('%u', filesize($path))?
Svish

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

Hãy để Apache làm công việc cho bạn.


12
Điều đó đơn giản hơn phương pháp x-sendfile, nhưng sẽ không hoạt động để hạn chế quyền truy cập vào tệp, tức là chỉ những người đã đăng nhập. Nếu bạn không cần phải làm điều đó thì thật tuyệt!
Jords

Đồng thời thêm kiểm tra liên kết giới thiệu với mod_rewrite.
sanmai

1
Bạn có thể xác thực trước khi chuyển tiêu đề. Bằng cách đó, bạn cũng không phải bơm hàng tấn nội dung qua bộ nhớ của PHP.
Brent

7
@UltimateBrent Vị trí vẫn phải có thể truy cập đến tất cả .. Và một tham khảo kiểm tra là không có an ninh tại tất cả vì nó xuất phát từ khách hàng
Øyvind Skaar

@Jimbo Một mã thông báo người dùng mà bạn sẽ kiểm tra như thế nào? Với PHP? Đột nhiên giải pháp của bạn được lặp lại.
Mark Amery

1

Triển khai tốt hơn, với hỗ trợ bộ nhớ cache, tiêu đề http tùy chỉnh.

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

nếu bạn có thể thêm phần mở rộng PECL vào php của mình, bạn có thể chỉ cần sử dụng các chức năng từ gói Fileinfo để xác định loại nội dung và sau đó gửi các tiêu đề thích hợp ...


/ va, bạn đã đề cập đến khả năng này chưa? :)
Andreas Linden

0

Hàm PHP Downloadđược đề cập ở đây đã gây ra một số chậm trễ trước khi tệp thực sự bắt đầu tải xuống. Tôi không biết liệu điều này có phải do sử dụng bộ đệm véc ni hay gì không, nhưng đối với tôi, nó đã giúp xóa sleep(1);hoàn toàn và đặt $speedthành 1024. Bây giờ nó hoạt động mà không có bất kỳ vấn đề nhanh như địa ngục. Có lẽ bạn cũng có thể sửa đổi chức năng đó, vì tôi thấy nó được sử dụng khắp nơi trên internet.


0

Tôi đã viết mã một hàm rất đơn giản để cung cấp các tệp có PHP và tự động phát hiện kiểu 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);
}

Sử dụng

serve_file("/no_apache/invoice243.pdf");
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.