Resumable downloads when using PHP to send the file?

后端 未结 13 883
梦毁少年i
梦毁少年i 2020-11-22 12:22

We are using a PHP scripting for tunnelling file downloads, since we don\'t want to expose the absolute path of downloadable file:

header(\"Content-Type: $ct         


        
相关标签:
13条回答
  • 2020-11-22 13:02

    The first thing you need to do is to send the Accept-Ranges: bytes header in all responses, to tell the client that you support partial content. Then, if request with a Range: bytes=x-y header is received (with x and y being numbers) you parse the range the client is requesting, open the file as usual, seek x bytes ahead and send the next y - x bytes. Also set the response to HTTP/1.0 206 Partial Content.

    Without having tested anything, this could work, more or less:

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

    I may have missed something obvious, and I have most definitely ignored some potential sources of errors, but it should be a start.

    There's a description of partial content here and I found some info on partial content on the documentation page for fread.

    0 讨论(0)
  • 2020-11-22 13:02

    This works 100% super check it I am using it and no problems any more.

            /* 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);
    
    0 讨论(0)
  • 2020-11-22 13:03

    EDIT 2017/01 - I wrote a library to do this in PHP >=7.0 https://github.com/DaveRandom/Resume

    EDIT 2016/02 - Code completely rewritten to a set of modular tools an an example usage, rather than a monolithic function. Corrections mentioned in comments below have been incorporated.


    A tested, working solution (based heavily on Theo's answer above) which deals with resumable downloads, in a set of a few standalone tools. This code requires PHP 5.4 or later.

    This solution can still only cope with one range per request, but under any circumstance with a standard browser that I can think of, this should not cause a problem.

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

    Example usage:

    <?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;
    
    0 讨论(0)
  • 2020-11-22 13:04

    A really nice way to solve this without having to "roll your own" PHP code is to use the mod_xsendfile Apache module. Then in PHP, you just set the appropriate headers. Apache gets to do its thing.

    header("X-Sendfile: /path/to/file");
    header("Content-Type: application/octet-stream");
    header("Content-Disposition: attachment; file=\"filename\"");
    
    0 讨论(0)
  • 2020-11-22 13:05

    Thanks Theo! your method did not directly work for streaming divx because i found the divx player was sending ranges like bytes=9932800-

    but it showed me how to do it so thanks :D

    if(isset($_SERVER['HTTP_RANGE']))
    {
        file_put_contents('showrange.txt',$_SERVER['HTTP_RANGE']);
    
    0 讨论(0)
  • 2020-11-22 13:08

    The top answer has various bugs.

    1. The major bug: It doesn't handle Range header correctly. bytes a-b should mean [a, b] instead of [a, b), and bytes a- is not handled.
    2. The minor bug: It doesn't use buffer to handle output. This may consume too much memory and cause low speed for large files.

    Here's my modified code:

    // 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);
    
    0 讨论(0)
提交回复
热议问题