105

我正在尝试组合一个函数来接收文件路径,识别它是什么,设置适当的标题,并像 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], '.');
    }
}
}
?>
4

8 回答 8

147

我之前的回答是部分的并且没有很好的记录,这里是一个更新,其中包含来自它和讨论中其他人的解决方案的摘要。

这些解决方案的顺序是从最好的解决方案到最差的解决方案,也从需要对 Web 服务器进行最多控制的解决方案到需要较少的解决方案。似乎没有一种简单的方法来拥有一个既快速又无处不在的解决方案。


使用 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 下,如果您使用 mod_php,您需要安装一个名为mod_xsendfile的模块,然后对其进行配置(如果您允许,可以在 apache config 或 .htaccess 中)

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

使用此模块,文件路径可以是绝对的或相对于指定的XSendFilePath.

轻量级

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对此有很好的解释

符号链接和位置标头

您可以使用符号链接并重定向到它们,只需在用户被授权访问文件并使用以下命令将用户重定向到该文件时,使用随机名称创建指向文件的符号链接:

header("Location: " . $url_of_symlink);

显然,您需要一种方法来修剪它们,无论是在调用创建它们的脚本时还是通过 cron(如果您有权访问,则在机器上,否则通过某些 webcron 服务)

在 apache 下,您需要能够FollowSymLinks在 a.htaccess或 apache 配置中启用。

通过 IP 和 Location 标头进行访问控制

另一个 hack 是从允许显式用户 IP 的 php 生成 apache 访问文件。在 apache 下,它意味着使用mod_authz_host( mod_access)Allow from命令。

问题是锁定对文件的访问(因为多个用户可能希望同时执行此操作)并非易事,并且可能导致一些用户等待很长时间。无论如何,您仍然需要修剪文件。

显然,另一个问题是同一 IP 后面的多个人可能会访问该文件。

当其他一切都失败时

如果你真的没有办法让你的网络服务器来帮助你,剩下的唯一解决方案是readfile它在​​当前使用的所有 php 版本中都可用并且工作得很好(但不是很有效)。


组合解决方案

好吧,如果您希望您的 php 代码在任何地方都可用,那么快速发送文件的最佳方法是在某处有一个可配置的选项,其中包含有关如何根据 Web 服务器激活它的说明,并且可能在您的安装中进行自动检测脚本。

它与许多软件中所做的非常相似

  • 清理网址(mod_rewrite在 apache 上)
  • 加密函数(mcryptphp 模块)
  • 多字节字符串支持(mbstringphp 模块)
于 2010-09-16T23:29:09.850 回答
34

最快的方法:不要。查看nginx 的 x-sendfile 标头,其他 Web 服务器也有类似的东西。这意味着您仍然可以在 php 中进行访问控制等,但将文件的实际发送委托给为此设计的 Web 服务器。

PS:与在 php 中读取和发送文件相比,与 nginx 一起使用它的效率高出多少,我就感到不寒而栗。试想一下,如果有 100 个人正在下载一个文件:使用 php + apache,很慷慨,那可能是 100*15mb = 1.5GB(大约,射击我),内存就在那里。Nginx 只会将文件发送到内核,然后直接从磁盘加载到网络缓冲区中。迅速!

PPS:而且,使用这种方法,您仍然可以进行所有访问控制,您想要的数据库内容。

于 2010-09-13T03:52:50.700 回答
26

这是一个纯 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 为单位)。

于 2011-09-29T00:18:34.733 回答
14
header('Location: ' . $path);
exit(0);

让 Apache 为您完成工作。

于 2010-09-13T04:18:23.877 回答
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;
    }
}
于 2015-04-06T09:56:55.183 回答
0

如果您有可能将 PECL 扩展添加到您的 php,您可以简单地使用Fileinfo 包中的函数来确定内容类型,然后发送正确的标头...

于 2010-09-23T18:30:34.757 回答
0

此处提到的 PHPDownload函数在文件实际开始下载之前造成了一些延迟。我不知道这是否是由使用清漆缓存或什么引起的,但对我来说,它有助于sleep(1);完全删除并设置$speed1024. 现在它可以毫无问题地运行,速度非常快。也许您也可以修改该功能,因为我看到它在整个互联网上都使用过。

于 2013-12-12T10:03:28.820 回答
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");
于 2020-02-28T16:33:17.563 回答