使用PHP发送文件时如何实现可恢复下载?

114

我们正在使用PHP脚本进行文件下载隧道,因为我们不想暴露可下载文件的绝对路径:

header("Content-Type: $ctype");
header("Content-Length: " . filesize($file));
header("Content-Disposition: attachment; filename=\"$fileName\"");
readfile($file);

不幸的是,我们注意到通过这个脚本下载的内容无法通过终端用户恢复下载。

有没有办法在基于PHP的解决方案中支持可恢复的下载?

14个回答

111
首先,您需要在所有响应中发送 Accept-Ranges: bytes 头,告诉客户端您支持部分内容。然后,如果收到带有 Range: bytes=x-y 头的请求(其中 xy 是数字),则解析客户端请求的范围,像往常一样打开文件,向前查找 x 字节并发送下一个 y - x 字节。还要将响应设置为 HTTP/1.0 206 Partial Content
没有经过任何测试,这可能会更或少起作用:
$filesize = filesize($file);

$offset = 0;
$length = $filesize;

if ( isset($_SERVER['HTTP_RANGE']) ) {
    // if the HTTP_RANGE header is set we're dealing with partial content

    $partialContent = true;

    // find the requested range
    // this might be too simplistic, apparently the client can request
    // multiple ranges, which can become pretty complex, so ignore it for now
    preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches);

    $offset = intval($matches[1]);
    $length = intval($matches[2]) - $offset;
} else {
    $partialContent = false;
}

$file = fopen($file, 'r');

// seek to the requested offset, this is 0 if it's not a partial content request
fseek($file, $offset);

$data = fread($file, $length);

fclose($file);

if ( $partialContent ) {
    // output the right headers for partial content

    header('HTTP/1.1 206 Partial Content');

    header('Content-Range: bytes ' . $offset . '-' . ($offset + $length) . '/' . $filesize);
}

// output the regular HTTP headers
header('Content-Type: ' . $ctype);
header('Content-Length: ' . $filesize);
header('Content-Disposition: attachment; filename="' . $fileName . '"');
header('Accept-Ranges: bytes');

// don't forget to send the data too
print($data);

我可能错过了一些显而易见的东西,肯定也忽略了一些潜在的错误来源,但这应该是一个开始。

这里有部分内容的描述,我在这里找到了一些关于部分内容的信息,并且在fread文档页面上也找到了一些相关信息。


3
小问题,你的正则表达式应该是: preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches) - deepwell
1
你是对的,我已经改了。然而,根据规格说明,你可以使用“bytes=x-y”、“bytes=-x”、“bytes=x-”、“bytes=x-y,a-b”等方式,所以上一个版本中的错误是缺少结尾斜杠,而不是缺少问号。 - Theo
8
非常有帮助,但我必须作出两个小调整才能使它正常工作:1. 如果客户端没有在范围内发送终点(因为它是隐式的),则 $length 将为负数。$length = (($matches[2]) ? intval($matches[2]) : $filesize) - $offset; 可以解决这个问题。2. Content-Range 将第一个字节视为字节 0,所以最后一个字节是 $filesize - 1。因此,它应该是 ($offset + $length - 1) - Dennis
1
以上方法无法处理大文件下载,会出现“PHP致命错误:内存大小已耗尽(尝试分配XXXX字节)”。在我的情况下,100MB的文件太大了。你需要将所有文件保存在一个变量中,然后再输出。 - sarah.ferguson
1
你可以通过分块读取而不是一次性读取来解决大文件问题。 - dynamichael
显示剩余7条评论

75

编辑 2017/01 - 我编写了一个库来实现PHP>=7.0的这个功能。 https://github.com/DaveRandom/Resume

编辑 2016/02 - 代码已完全重写为一组模块化工具和示例用法,而不是单片函数。评论中提到的更正已被纳入。


这是一个经过测试、可行的解决方案(在Theo的答案基础上进行了大量修改),可以处理可恢复下载,并使用几个独立的工具。此代码需要PHP 5.4或更高版本。

此解决方案仍然只能处理请求中的一个范围,但在我所能想到的任何使用标准浏览器的情况下,这都不应该成为问题。

<?php

/**
 * Get the value of a header in the current request context
 *
 * @param string $name Name of the header
 * @return string|null Returns null when the header was not sent or cannot be retrieved
 */
function get_request_header($name)
{
    $name = strtoupper($name);

    // IIS/Some Apache versions and configurations
    if (isset($_SERVER['HTTP_' . $name])) {
        return trim($_SERVER['HTTP_' . $name]);
    }

    // Various other SAPIs
    foreach (apache_request_headers() as $header_name => $value) {
        if (strtoupper($header_name) === $name) {
            return trim($value);
        }
    }

    return null;
}

class NonExistentFileException extends \RuntimeException {}
class UnreadableFileException extends \RuntimeException {}
class UnsatisfiableRangeException extends \RuntimeException {}
class InvalidRangeHeaderException extends \RuntimeException {}

class RangeHeader
{
    /**
     * The first byte in the file to send (0-indexed), a null value indicates the last
     * $end bytes
     *
     * @var int|null
     */
    private $firstByte;

    /**
     * The last byte in the file to send (0-indexed), a null value indicates $start to
     * EOF
     *
     * @var int|null
     */
    private $lastByte;

    /**
     * Create a new instance from a Range header string
     *
     * @param string $header
     * @return RangeHeader
     */
    public static function createFromHeaderString($header)
    {
        if ($header === null) {
            return null;
        }

        if (!preg_match('/^\s*(\S+)\s*(\d*)\s*-\s*(\d*)\s*(?:,|$)/', $header, $info)) {
            throw new InvalidRangeHeaderException('Invalid header format');
        } else if (strtolower($info[1]) !== 'bytes') {
            throw new InvalidRangeHeaderException('Unknown range unit: ' . $info[1]);
        }

        return new self(
            $info[2] === '' ? null : $info[2],
            $info[3] === '' ? null : $info[3]
        );
    }

    /**
     * @param int|null $firstByte
     * @param int|null $lastByte
     * @throws InvalidRangeHeaderException
     */
    public function __construct($firstByte, $lastByte)
    {
        $this->firstByte = $firstByte === null ? $firstByte : (int)$firstByte;
        $this->lastByte = $lastByte === null ? $lastByte : (int)$lastByte;

        if ($this->firstByte === null && $this->lastByte === null) {
            throw new InvalidRangeHeaderException(
                'Both start and end position specifiers empty'
            );
        } else if ($this->firstByte < 0 || $this->lastByte < 0) {
            throw new InvalidRangeHeaderException(
                'Position specifiers cannot be negative'
            );
        } else if ($this->lastByte !== null && $this->lastByte < $this->firstByte) {
            throw new InvalidRangeHeaderException(
                'Last byte cannot be less than first byte'
            );
        }
    }

    /**
     * Get the start position when this range is applied to a file of the specified size
     *
     * @param int $fileSize
     * @return int
     * @throws UnsatisfiableRangeException
     */
    public function getStartPosition($fileSize)
    {
        $size = (int)$fileSize;

        if ($this->firstByte === null) {
            return ($size - 1) - $this->lastByte;
        }

        if ($size <= $this->firstByte) {
            throw new UnsatisfiableRangeException(
                'Start position is after the end of the file'
            );
        }

        return $this->firstByte;
    }

    /**
     * Get the end position when this range is applied to a file of the specified size
     *
     * @param int $fileSize
     * @return int
     * @throws UnsatisfiableRangeException
     */
    public function getEndPosition($fileSize)
    {
        $size = (int)$fileSize;

        if ($this->lastByte === null) {
            return $size - 1;
        }

        if ($size <= $this->lastByte) {
            throw new UnsatisfiableRangeException(
                'End position is after the end of the file'
            );
        }

        return $this->lastByte;
    }

    /**
     * Get the length when this range is applied to a file of the specified size
     *
     * @param int $fileSize
     * @return int
     * @throws UnsatisfiableRangeException
     */
    public function getLength($fileSize)
    {
        $size = (int)$fileSize;

        return $this->getEndPosition($size) - $this->getStartPosition($size) + 1;
    }

    /**
     * Get a Content-Range header corresponding to this Range and the specified file
     * size
     *
     * @param int $fileSize
     * @return string
     */
    public function getContentRangeHeader($fileSize)
    {
        return 'bytes ' . $this->getStartPosition($fileSize) . '-'
             . $this->getEndPosition($fileSize) . '/' . $fileSize;
    }
}

class PartialFileServlet
{
    /**
     * The range header on which the data transmission will be based
     *
     * @var RangeHeader|null
     */
    private $range;

    /**
     * @param RangeHeader $range Range header on which the transmission will be based
     */
    public function __construct(RangeHeader $range = null)
    {
        $this->range = $range;
    }

    /**
     * Send part of the data in a seekable stream resource to the output buffer
     *
     * @param resource $fp Stream resource to read data from
     * @param int $start Position in the stream to start reading
     * @param int $length Number of bytes to read
     * @param int $chunkSize Maximum bytes to read from the file in a single operation
     */
    private function sendDataRange($fp, $start, $length, $chunkSize = 8192)
    {
        if ($start > 0) {
            fseek($fp, $start, SEEK_SET);
        }

        while ($length) {
            $read = ($length > $chunkSize) ? $chunkSize : $length;
            $length -= $read;
            echo fread($fp, $read);
        }
    }

    /**
     * Send the headers that are included regardless of whether a range was requested
     *
     * @param string $fileName
     * @param int $contentLength
     * @param string $contentType
     */
    private function sendDownloadHeaders($fileName, $contentLength, $contentType)
    {
        header('Content-Type: ' . $contentType);
        header('Content-Length: ' . $contentLength);
        header('Content-Disposition: attachment; filename="' . $fileName . '"');
        header('Accept-Ranges: bytes');
    }

    /**
     * Send data from a file based on the current Range header
     *
     * @param string $path Local file system path to serve
     * @param string $contentType MIME type of the data stream
     */
    public function sendFile($path, $contentType = 'application/octet-stream')
    {
        // Make sure the file exists and is a file, otherwise we are wasting our time
        $localPath = realpath($path);
        if ($localPath === false || !is_file($localPath)) {
            throw new NonExistentFileException(
                $path . ' does not exist or is not a file'
            );
        }

        // Make sure we can open the file for reading
        if (!$fp = fopen($localPath, 'r')) {
            throw new UnreadableFileException(
                'Failed to open ' . $localPath . ' for reading'
            );
        }

        $fileSize = filesize($localPath);

        if ($this->range == null) {
            // No range requested, just send the whole file
            header('HTTP/1.1 200 OK');
            $this->sendDownloadHeaders(basename($localPath), $fileSize, $contentType);

            fpassthru($fp);
        } else {
            // Send the request range
            header('HTTP/1.1 206 Partial Content');
            header('Content-Range: ' . $this->range->getContentRangeHeader($fileSize));
            $this->sendDownloadHeaders(
                basename($localPath),
                $this->range->getLength($fileSize),
                $contentType
            );

            $this->sendDataRange(
                $fp,
                $this->range->getStartPosition($fileSize),
                $this->range->getLength($fileSize)
            );
        }

        fclose($fp);
    }
}

示例用法:

<?php

$path = '/local/path/to/file.ext';
$contentType = 'application/octet-stream';

// Avoid sending unexpected errors to the client - we should be serving a file,
// we don't want to corrupt the data we send
ini_set('display_errors', '0');

try {
    $rangeHeader = RangeHeader::createFromHeaderString(get_request_header('Range'));
    (new PartialFileServlet($rangeHeader))->sendFile($path, $contentType);
} catch (InvalidRangeHeaderException $e) {
    header("HTTP/1.1 400 Bad Request");
} catch (UnsatisfiableRangeException $e) {
    header("HTTP/1.1 416 Range Not Satisfiable");
} catch (NonExistentFileException $e) {
    header("HTTP/1.1 404 Not Found");
} catch (UnreadableFileException $e) {
    header("HTTP/1.1 500 Internal Server Error");
}

// It's usually a good idea to explicitly exit after sending a file to avoid sending any
// extra data on the end that might corrupt the file
exit;

Dave - 谢谢你!我正在考虑使用这个代码来实现从 VPS 到客户端的可恢复下载,就是我最近发给你的那个代码 :) - Jimbo
5
Content-Length应该设置为实际文件大小,还是只设置正在发送的部分字节数?这个页面看起来应该是部分字节,但上面的示例代码并没有这样做。http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html - willus
3
还有一个小错误:$start = $end - intval($range[0]); 应改为 $range[1] - BurninLeo
1
@sarah.ferguson 代码已经完全重写和更新,请参见上文。 - DaveRandom
@madscientist159 它是MIT许可证。另请参阅:https://github.com/DaveRandom/Resume - DaveRandom
显示剩余10条评论

16

这个绝对有效,检查一下吧,我正在使用它,再也没有问题了。

        /* Function: download with resume/speed/stream options */


         /* List of File Types */
        function fileTypes($extension){
            $fileTypes['swf'] = 'application/x-shockwave-flash';
            $fileTypes['pdf'] = 'application/pdf';
            $fileTypes['exe'] = 'application/octet-stream';
            $fileTypes['zip'] = 'application/zip';
            $fileTypes['doc'] = 'application/msword';
            $fileTypes['xls'] = 'application/vnd.ms-excel';
            $fileTypes['ppt'] = 'application/vnd.ms-powerpoint';
            $fileTypes['gif'] = 'image/gif';
            $fileTypes['png'] = 'image/png';
            $fileTypes['jpeg'] = 'image/jpg';
            $fileTypes['jpg'] = 'image/jpg';
            $fileTypes['rar'] = 'application/rar';

            $fileTypes['ra'] = 'audio/x-pn-realaudio';
            $fileTypes['ram'] = 'audio/x-pn-realaudio';
            $fileTypes['ogg'] = 'audio/x-pn-realaudio';

            $fileTypes['wav'] = 'video/x-msvideo';
            $fileTypes['wmv'] = 'video/x-msvideo';
            $fileTypes['avi'] = 'video/x-msvideo';
            $fileTypes['asf'] = 'video/x-msvideo';
            $fileTypes['divx'] = 'video/x-msvideo';

            $fileTypes['mp3'] = 'audio/mpeg';
            $fileTypes['mp4'] = 'audio/mpeg';
            $fileTypes['mpeg'] = 'video/mpeg';
            $fileTypes['mpg'] = 'video/mpeg';
            $fileTypes['mpe'] = 'video/mpeg';
            $fileTypes['mov'] = 'video/quicktime';
            $fileTypes['swf'] = 'video/quicktime';
            $fileTypes['3gp'] = 'video/quicktime';
            $fileTypes['m4a'] = 'video/quicktime';
            $fileTypes['aac'] = 'video/quicktime';
            $fileTypes['m3u'] = 'video/quicktime';
            return $fileTypes[$extention];
        };

        /*
          Parameters: downloadFile(File Location, File Name,
          max speed, is streaming
          If streaming - videos will show as videos, images as images
          instead of download prompt
         */

        function downloadFile($fileLocation, $fileName, $maxSpeed = 100, $doStream = false) {
            if (connection_status() != 0)
                return(false);
        //    in some old versions this can be pereferable to get extention
        //    $extension = strtolower(end(explode('.', $fileName)));
            $extension = pathinfo($fileName, PATHINFO_EXTENSION);

            $contentType = fileTypes($extension);
            header("Cache-Control: public");
            header("Content-Transfer-Encoding: binary\n");
            header('Content-Type: $contentType');

            $contentDisposition = 'attachment';

            if ($doStream == true) {
                /* extensions to stream */
                $array_listen = array('mp3', 'm3u', 'm4a', 'mid', 'ogg', 'ra', 'ram', 'wm',
                    'wav', 'wma', 'aac', '3gp', 'avi', 'mov', 'mp4', 'mpeg', 'mpg', 'swf', 'wmv', 'divx', 'asf');
                if (in_array($extension, $array_listen)) {
                    $contentDisposition = 'inline';
                }
            }

            if (strstr($_SERVER['HTTP_USER_AGENT'], "MSIE")) {
                $fileName = preg_replace('/\./', '%2e', $fileName, substr_count($fileName, '.') - 1);
                header("Content-Disposition: $contentDisposition;
                    filename=\"$fileName\"");
            } else {
                header("Content-Disposition: $contentDisposition;
                    filename=\"$fileName\"");
            }

            header("Accept-Ranges: bytes");
            $range = 0;
            $size = filesize($fileLocation);

            if (isset($_SERVER['HTTP_RANGE'])) {
                list($a, $range) = explode("=", $_SERVER['HTTP_RANGE']);
                str_replace($range, "-", $range);
                $size2 = $size - 1;
                $new_length = $size - $range;
                header("HTTP/1.1 206 Partial Content");
                header("Content-Length: $new_length");
                header("Content-Range: bytes $range$size2/$size");
            } else {
                $size2 = $size - 1;
                header("Content-Range: bytes 0-$size2/$size");
                header("Content-Length: " . $size);
            }

            if ($size == 0) {
                die('Zero byte file! Aborting download');
            }
            set_magic_quotes_runtime(0);
            $fp = fopen("$fileLocation", "rb");

            fseek($fp, $range);

            while (!feof($fp) and ( connection_status() == 0)) {
                set_time_limit(0);
                print(fread($fp, 1024 * $maxSpeed));
                flush();
                ob_flush();
                sleep(1);
            }
            fclose($fp);

            return((connection_status() == 0) and ! connection_aborted());
        }

        /* Implementation */
        // downloadFile('path_to_file/1.mp3', '1.mp3', 1024, false);

1
我点赞了,因为速度限制真的非常有用,然而在恢复文件(Firefox)时进行 MD5 检查显示不匹配。$range 的 str_replace 是错误的,应该使用另一个 explode,将结果转换为数字,并将破折号添加到 Content-Range 标头中。 - WhoIsRich
如何自定义以支持远程文件下载? - Siyamak Shahpasand
3
你的意思是要用双引号引用 'Content-Type: $contentType'; - Matt
我认为 set_time_limit(0); 不是很合适。一个更合理的限制可能是24小时? - twicejr
1
这个程序完全不工作 :( 它无法将一个简单的 MP4 发送到客户端,但它可以发送一个 MP3,但不幸的是 Chrome 无法在这个 MP3 中进行搜索。 - HF_
显示剩余3条评论

16
是的。支持字节范围。请参阅RFC 2616第14.35节
基本上意味着您应该读取Range头,并从指定的偏移量开始提供文件。
这意味着您不能使用readfile(),因为它会提供整个文件。相反,首先使用fopen(),然后使用fseek()将文件定位到正确的位置,然后使用fpassthru()来提供文件。

4
如果文件大小超过几兆,fpassthru不是一个好的选择,因为你可能会用完内存。只需使用fread()和print()逐块输出即可。 - Willem
3
fpassthru在这里处理数百兆的数据非常好用。“echo file_get_contents(...)”不起作用(OOM)。因此,我不认为这是一个问题。PHP 5.3。 - Janus Troelsen
1
@JanusTroelsen 不是这样的。这完全取决于您服务器的配置。如果您拥有一台强大的服务器,为 PHP 分配了大量内存,那么它可能能够很好地工作。在“弱”配置(字面上:共享主机)上使用 fpassthru 甚至会使 50MB 的文件失败。如果您在弱服务器配置上提供大文件,则绝对不应使用它。正如 @Wimmer 正确指出的那样,在这种情况下,fread + print 就足够了。 - trejder
2
@trejder:请查看readfile()的注释readfile()本身不会出现任何内存问题,即使发送大文件。如果遇到内存不足错误,请确保使用ob_get_level()关闭输出缓冲区。 - Janus Troelsen
@JanusTroelsen 好的,我开启了输出缓冲,因为我必须这样做(我的框架使用它)。即使在 php.ini 中将最大内存设置为 1 GB(是其五倍),readfile() 在发送 200 MB 文件时也会失败。当我使用文件分块的简单解决方案(使用 fopenfread,但不使用 fpassthru)时,所有问题都消失了,我甚至成功发送了大小达 2 GB 的文件,而且没有任何问题,尽管 php.ini 配置没有改变。所以我的先前评论有点不正确,因为我没有像你建议的那样关闭输出缓冲来测试这个问题。 - trejder
1
@trejder问题在于你没有正确配置输出缓冲区。如果你告诉它这样做,它会自动进行分块处理:http://php.net/manual/en/outcontrol.configuration.php#ini.output-buffering 例:output_buffering=4096(如果你的框架不允许这样做,那你的框架就很糟糕)。 - ZJR

12

一个非常好的解决方法,无需编写自己的PHP代码,是使用mod_xsendfile Apache模块。然后在PHP中,您只需设置适当的标头即可。Apache将继续处理。

header("X-Sendfile: /path/to/file");
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; file=\"filename\"");

3
如果您想在发送文件后取消链接,该怎么办? - Janus Troelsen
1
如果您想在发送文件后取消链接,您需要一个特殊的标志来指示,参见 XSendFilePath <绝对路径> [AllowFileDelete] (https://tn123.org/mod_xsendfile/beta/)。 - Jens A. Koch

9

如果您想安装新的PECL模块,通过使用http_send_file()实现PHP支持可恢复下载是最简单的方式,示例如下:

<?php
http_send_content_disposition("document.pdf", true);
http_send_content_type("application/pdf");
http_throttle(0.1, 2048);
http_send_file("../report.pdf");
?>

来源: http://www.php.net/manual/en/function.http-send-file.php

我们使用它来提供数据库存储的内容,效果非常好!


3
运作得很好。但是要注意不要开启输出缓冲(例如ob_start),特别是在发送大文件时,这会缓存完整的请求范围。 - Pieter van Ginkel
这个是什么时候加入 PHP 的?一直存在吗? - thomthom
2
那是Pecl,不是PHP。我没有这个函数。 - Geo
此函数已被弃用,不再起作用。 - Benyamin Limanto

4

是的,您可以使用范围标头来实现。您需要向客户端提供三个额外的标头以进行完整的下载:

header ("Accept-Ranges: bytes");
header ("Content-Length: " . $fileSize);
header ("Content-Range: bytes 0-" . $fileSize - 1 . "/" . $fileSize . ";");

如果下载中断,您需要通过以下方式检查范围请求头:

$headers = getAllHeaders ();
$range = substr ($headers['Range'], '6');

在这种情况下,不要忘记使用206状态码提供内容:

header ("HTTP/1.1 206 Partial content");
header ("Accept-Ranges: bytes");
header ("Content-Length: " . $remaining_length);
header ("Content-Range: bytes " . $start . "-" . $to . "/" . $fileSize . ";");

您将从请求头中获取$start和$to变量,并使用fseek()在文件中寻找正确的位置。

2
@ceejayoz:getallheaders()是一个PHP函数,如果你正在使用Apache http://uk2.php.net/getallheaders,你就可以使用它。 - Tom Haigh

4

最佳答案存在多个缺陷。

  1. 主要缺陷:它不能正确地处理范围标头。 bytes a-b 应该表示 [a, b] 而不是 [a, b),而且 bytes a- 也未被处理。
  2. 次要缺陷:它没有使用缓冲区来处理输出。这可能会消耗过多的内存,并导致处理大文件时速度变慢。

以下是我修改后的代码:

// TODO: configurations here
$fileName = "File Name";
$file = "File Path";
$bufferSize = 2097152;

$filesize = filesize($file);
$offset = 0;
$length = $filesize;
if (isset($_SERVER['HTTP_RANGE'])) {
    // if the HTTP_RANGE header is set we're dealing with partial content
    // find the requested range
    // this might be too simplistic, apparently the client can request
    // multiple ranges, which can become pretty complex, so ignore it for now
    preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches);
    $offset = intval($matches[1]);
    $end = $matches[2] || $matches[2] === '0' ? intval($matches[2]) : $filesize - 1;
    $length = $end + 1 - $offset;
    // output the right headers for partial content
    header('HTTP/1.1 206 Partial Content');
    header("Content-Range: bytes $offset-$end/$filesize");
}
// output the regular HTTP headers
header('Content-Type: ' . mime_content_type($file));
header("Content-Length: $filesize");
header("Content-Disposition: attachment; filename=\"$fileName\"");
header('Accept-Ranges: bytes');

$file = fopen($file, 'r');
// seek to the requested offset, this is 0 if it's not a partial content request
fseek($file, $offset);
// don't forget to send the data too
ini_set('memory_limit', '-1');
while ($length >= $bufferSize)
{
    print(fread($file, $bufferSize));
    $length -= $bufferSize;
}
if ($length) print(fread($file, $length));
fclose($file);

为什么需要使用 ini_set('memory_limit', '-1'); - Mikko Rantalainen
1
@MikkoRantalainen 我忘了。你可以尝试将其删除,看看会发生什么。 - Mygod
2
如果$matches [2]未设置(例如使用“Range = 0-”请求),则在$end分配中将抛出错误。我改用了以下代码: if(!isset($matches[2])) { $end=$fs-1; } else { $end = intval($matches[2]); } - Skynet

3

3

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接