【问题标题】:Resumable downloads when using PHP to send the file?使用 PHP 发送文件时可恢复下载?
【发布时间】:2010-09-14 12:18:50
【问题描述】:

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

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

很遗憾,我们注意到最终用户无法恢复通过此脚本进行的下载。

有没有办法通过这种基于 PHP 的解决方案来支持可恢复下载?

【问题讨论】:

    标签: php download resume-download


    【解决方案1】:

    您需要做的第一件事是在所有响应中发送Accept-Ranges: bytes 标头,告诉客户端您支持部分内容。然后,如果收到带有Range: bytes=x-y 标头的请求(xy 是数字),您解析客户端请求的范围,照常打开文件,提前查找x 字节并发送下一个@ 987654328@ - 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);
    

    我可能遗漏了一些明显的东西,而且我肯定忽略了一些潜在的错误来源,但这应该是一个开始。

    有一个description of partial content here,我在fread 的文档页面上找到了一些关于部分内容的信息。

    【讨论】:

    • 小bug,你的正则表达式应该是:preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches)
    • 你是对的,我已经改变了它。但是,无论如何我都太简单了,根据规范,您可以执行“bytes=xy”、“bytes=-x”、“bytes=x-”、“bytes=xy,ab”等,所以在以前的版本是缺少结束斜线,而不是缺少问号。
    • 非常有帮助,但我必须做两个小调整才能使其正常工作: 1. 如果客户端没有发送范围内的端点(因为它是隐式的),$length 将是否定的。 $length = (($matches[2]) ? intval($matches[2]) : $filesize) - $offset; 解决了这个问题。 2.Content-Range将第一个字节视为0,所以最后一个字节为$filesize - 1。因此,它必须是($offset + $length - 1)
    • 以上不适用于大下载,您会收到“PHP 致命错误:允许的 XXXX 字节的内存大小已用尽(尝试分配 XXX 字节)”。就我而言,100MB 太大了。您基本上将所有文件保存在一个变量中并将其吐出。
    • 您可以通过分块读取而不是一次全部读取来解决大文件问题。
    【解决方案2】:

    编辑 2017/01 - 我写了一个库来用 PHP >=7.0 https://github.com/DaveRandom/Resume

    编辑 2016/02 - 代码完全重写为一组模块化工具作为示例用法,而不是单一功能。已合并以下 cmets 中提到的更正。


    一个经过测试的、有效的解决方案(很大程度上基于上面 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;
    

    【讨论】:

    • 这里的代码很不错。我确实在设置 $length 的行上发现了一个错误。应该是:$length = $end - $start + 1;
    • 我将如何暂停下载
    • 应该将 Content-Length 设置为实际文件大小,还是只设置正在发送的部分字节数?这个页面使它看起来应该是部分字节,但这不是上面示例代码中所做的。 w3.org/Protocols/rfc2616/rfc2616-sec14.html
    • 另一个小错字:$start = $end - intval($range[0]); 应该是 range[1]
    • @sarah.ferguson 代码完全重写和更新,见上文。
    【解决方案3】:

    是的。支持字节范围。见RFC 2616 section 14.35

    这基本上意味着您应该读取Range 标头,并从指定的偏移量开始提供文件。

    这意味着您不能使用 readfile(),因为它服务于整个文件。而是先使用fopen(),然后fseek() 到正确的位置,然后使用fpassthru() 提供文件。

    【讨论】:

    • fpassthru 不是一个好主意,如果文件是多兆字节,您可能会耗尽内存。只是 fread() 和 print() 分块。
    • fpassthru 在数百兆字节的情况下工作得很好。 echo file_get_contents(...) 不起作用(OOM)。所以我不认为这是一个问题。 PHP 5.3。
    • @JanusTroelsen 不,不是。这一切都取决于您的服务器的配置。如果您有一个强大的服务器,并为 PHP 分配了大量内存,那么它可能对您来说工作得很好。在使用fpassthru 的“弱”配置(字面意思是:共享主机)上,即使是 50 MB 文件也会失败。如果您在弱服务器配置上提供大文件,则绝对不应该使用它。正如@Wimmer 正确指出的那样,fread + print 就是你在这种情况下所需要的。
    • @trejder:参见注释on readfile()readfile() 不会出现任何内存问题,即使在发送大文件时也是如此。如果遇到内存不足错误,请确保使用 ob_get_level() 关闭输出缓冲。
    • @trejder 问题是您没有正确配置输出缓冲。如果您告诉它,它会自动进行分块:php.net/manual/en/… 例如output_buffering=4096(如果你的框架不允许这样做,你的框架很烂)
    【解决方案4】:

    这工作 100% 超级检查它 我正在使用它,没有问题了。

            /* 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);
    

    【讨论】:

    • 我投了赞成票,因为速度限制真的很有用,但是对恢复文件 (Firefox) 的 MD5 检查显示不匹配。 $range 的 str_replace 错误,应该是另一个爆炸,结果变成数字,并且在 Content-Range 标题中添加了一个破折号。
    • 如何自定义支持远程文件下载?
    • 你的意思是双引号 'Content-Type: $contentType';
    • set_time_limit(0);在我看来不是很合适。也许更合理的 24 小时限制?
    • 这有效 0% :( 它不能向客户端发送一个简单的MP4,但它会发送一个MP3,但不幸的是Chrome,无法在这个MP3 中寻找。
    【解决方案5】:

    解决这个问题的一个非常好的方法是使用 mod_xsendfile Apache 模块,而无需“滚动你自己的”PHP 代码。然后在 PHP 中,您只需设置适当的标头。 Apache 开始做自己的事了。

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

    【讨论】:

    • 如果你想在发送后取消链接文件怎么办?
    • 如果你想在发送后取消链接文件,你需要一个特殊的标志来表示,请参阅XSendFilePath &lt;absolute path&gt; [AllowFileDelete] (tn123.org/mod_xsendfile/beta)。
    【解决方案6】:

    如果您愿意安装新的 PECL 模块,使用 PHP 支持可恢复下载的最简单方法是通过http_send_file(),就像这样

    <?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

    我们用它来提供数据库存储的内容,它就像一个魅力!

    【讨论】:

    • 像魅力一样工作。但是请注意您没有打开输出缓冲(ob_start 等)。特别是在发送大文件时,这将缓冲完整的请求范围。
    • 这是什么时候添加到 PHP 中的?一直都在吗?
    • 那是 Pecl,不是 PHP。我没有这个功能。
    • 此功能已弃用。不再工作了。
    【解决方案7】:

    是的,您可以为此使用 Range 标头。您需要再向客户端提供 3 个标头才能完整下载:

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

    对于中断的下载,您需要通过以下方式检查 Range 请求标头:

    $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() 查找文件中的正确位置。

    【讨论】:

    【解决方案8】:

    最佳答案有各种错误。

    1. 主要错误:它不能正确处理 Range 标头。 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');
    • @MikkoRantalainen 我忘了。您可以尝试删除它,看看会发生什么。
    • 不幸的是,如果 $matches[2] 未设置(例如,使用“Range=0-”请求),您将在 $end 分配中抛出错误。我改用这个:if(!isset($matches[2])) { $end=$fs-1; } else { $end = intval($matches[2]); }
    【解决方案9】:

    这对我来说效果很好:https://github.com/pomle/php-serveFilePartial

    【讨论】:

      【解决方案10】:

      启用了小型作曲家的类,其工作方式与 pecl http_send_file 相同。这意味着支持可恢复下载和限制。 https://github.com/diversen/http-send-file

      【讨论】:

        【解决方案11】:

        您可以使用以下代码在任何浏览器中支持字节范围请求

            <?php
        $file = 'YouTube360p.mp4';
        $fileLoc = $file;
        $filesize = filesize($file);
        $offset = 0;
        $fileLength = $filesize;
        $length = $filesize - 1;
        
        if ( isset($_SERVER['HTTP_RANGE']) ) {
            // if the HTTP_RANGE header is set we're dealing with partial content
        
            $partialContent = true;
            preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches);
        
            $offset = intval($matches[1]);
            $tempLength = intval($matches[2]) - 0;
            if($tempLength != 0)
            {
                $length = $tempLength;
            }
            $fileLength = ($length - $offset) + 1;
        } else {
            $partialContent = false;
            $offset = $length;
        }
        
        $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');
        }
        
        // output the regular HTTP headers
        header('Content-Type: ' . mime_content_type($fileLoc));
        header('Content-Length: ' . $fileLength);
        header('Content-Disposition: inline; filename="' . $file . '"');
        header('Accept-Ranges: bytes');
        header('Content-Range: bytes ' . $offset . '-' . $length . '/' . $filesize);
        
        // don't forget to send the data too
        print($data);
        ?>
        

        【讨论】:

          【解决方案12】:

          在 HTTP 中恢复下载是通过 Range 标头完成的。如果请求包含Range 标头,并且如果其他指示符(例如If-MatchIf-Unmodified-Since)表明自下载开始后内容没有更改,则给出206 响应码(而不是200),在 Content-Range 标头中指明您要返回的字节范围,然后在响应正文中提供该范围。

          不过,我不知道如何在 PHP 中做到这一点。

          【讨论】:

            【解决方案13】:

            感谢西奥!你的方法不能直接用于流媒体 divx,因为我发现 divx 播放器发送的范围像 bytes=9932800-

            但它向我展示了如何做到这一点,谢谢:D

            if(isset($_SERVER['HTTP_RANGE']))
            {
                file_put_contents('showrange.txt',$_SERVER['HTTP_RANGE']);
            

            【讨论】:

              【解决方案14】:

              我创建了一个用于提供文件的库,支持条件(除非文件已更改,否则不要再次下载文件)和范围(暂停和恢复下载)请求。它甚至适用于虚拟文件系统,例如Flysystem

              在这里查看:FileWaiter

              示例用法:

              use Stadly\FileWaiter\Adapter\Local;
              use Stadly\FileWaiter\File;
              use Stadly\FileWaiter\Waiter;
              
              $streamFactory = new \GuzzleHttp\Psr7\HttpFactory();                // Any PSR-17 compatible stream factory.
              $file = new File(new Local('filename.txt', $streamFactory));        // Or another file adapter. See below.
              $responseFactory = new \GuzzleHttp\Psr7\HttpFactory();              // Any PSR-17 compatible response factory.
              
              $waiter = new Waiter($file, $responseFactory);
              
              $request = \GuzzleHttp\Psr7\ServerRequest::fromGlobals();           // Any PSR-7 compatible server request.
              
              $response = $waiter->handle($request);                              // The response is created by the response factory.
              
              $emitter = new \Laminas\HttpHandlerRunner\Emitter\SapiEmitter();    // Any way of emitting PSR-7 responses.
              $emitter->emit($response);
              die();
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2013-07-21
                • 1970-01-01
                • 2012-10-19
                • 2022-01-04
                相关资源
                最近更新 更多