Có thể tiếp tục tải xuống khi sử dụng PHP để gửi tệp?


104

Chúng tôi đang sử dụng tập lệnh PHP để tải tệp xuống đường hầm, vì chúng tôi không muốn để lộ đường dẫn tuyệt đối của tệp có thể tải xuống:

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

Rất tiếc, chúng tôi nhận thấy rằng người dùng cuối không thể tiếp tục tải xuống thông qua tập lệnh này.

Có cách nào để hỗ trợ tải xuống có thể tiếp tục với giải pháp dựa trên PHP như vậy không?

Câu trả lời:


102

Điều đầu tiên bạn cần làm là gửi Accept-Ranges: bytestiêu đề trong tất cả các câu trả lời, để cho khách hàng biết rằng bạn hỗ trợ một phần nội dung. Sau đó, nếu yêu cầu với một Range: bytes=x-y tiêu đề nhận được (với xylà con số), bạn phân tích phạm vi khách hàng được yêu cầu, mở tập tin như bình thường, tìm kiếm xbyte trước và gửi tiếp theo y- xbyte. Đồng thời đặt phản hồi thành HTTP/1.0 206 Partial Content.

Nếu không thử nghiệm bất cứ điều gì, điều này có thể hoạt động, ít nhiều:

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

Tôi có thể đã bỏ qua một điều gì đó rõ ràng và chắc chắn tôi đã bỏ qua một số nguồn lỗi tiềm ẩn, nhưng đó nên là một sự khởi đầu.

Có một mô tả về một phần nội dung ở đây và tôi đã tìm thấy một số thông tin về một phần nội dung trên trang tài liệu cho fread .


3
Một lỗi nhỏ, biểu thức chính quy của bạn phải là: preg_match ('/ byte = (\ d +) - (\ d +)? /', $ _SERVER ['HTTP_RANGE'], $ trận đấu)
deepwell

1
Bạn đúng và tôi đã thay đổi nó. Tuy nhiên, dù sao thì nó cũng quá đơn giản, theo thông số kỹ thuật bạn có thể làm "bytes = xy", "bytes = -x", "bytes = x-", "bytes = xy, ab", v.v. nên lỗi trong phiên bản trước thiếu dấu gạch chéo cuối chứ không phải thiếu dấu chấm hỏi.
Theo

7
Rất hữu ích, nhưng tôi đã phải thực hiện hai điều chỉnh nhỏ để làm cho nó hoạt động: 1. Nếu khách hàng không gửi điểm cuối trong phạm vi (vì nó là ẩn), $lengthsẽ là tiêu cực. $length = (($matches[2]) ? intval($matches[2]) : $filesize) - $offset;sửa lỗi đó. 2. Content-Rangexử lý byte đầu tiên là byte 0, vì vậy byte cuối cùng là $filesize - 1. Do đó, nó phải được ($offset + $length - 1).
Dennis

1
Ở trên không hoạt động đối với các bản tải xuống lớn, bạn gặp phải "Lỗi nghiêm trọng PHP: Đã hết kích thước bộ nhớ được phép XXXX byte (đã cố gắng phân bổ XXX byte) trong". Trong trường hợp của tôi, 100MB là quá lớn. Về cơ bản, bạn lưu tất cả các tệp trong một biến và loại bỏ nó.
sarah.ferguson

1
Bạn có thể giải quyết vấn đề tệp lớn bằng cách đọc thành nhiều phần thay vì tất cả cùng một lúc.
dynamichael

71

EDIT 2017/01 - Tôi đã viết một thư viện để thực hiện việc này bằng PHP> = 7.0 https://github.com/DaveRandom/Resume

EDIT 2016/02 - Mã được viết lại hoàn toàn thành một bộ công cụ mô-đun, một ví dụ sử dụng, thay vì một hàm nguyên khối. Các chỉnh sửa được đề cập trong các bình luận bên dưới đã được kết hợp.


Một giải pháp đã được thử nghiệm, đang hoạt động (dựa nhiều vào câu trả lời của Theo ở trên) giải quyết việc tải xuống có thể tiếp tục, trong một tập hợp một vài công cụ độc lập. Mã này yêu cầu PHP 5.4 trở lên.

Giải pháp này vẫn chỉ có thể đối phó với một phạm vi cho mỗi yêu cầu, nhưng trong bất kỳ trường hợp nào với trình duyệt tiêu chuẩn mà tôi có thể nghĩ ra, điều này sẽ không gây ra sự cố.

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

Ví dụ sử dụng:

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

Mã khá đẹp ở đây. Tôi đã tìm thấy một lỗi trên dòng nơi đặt $ length. Nên là: $ length = $ end - $ start + 1;
bobwienholt

Tôi sẽ tạm dừng tải xuống như thế nào
Prasanth Bendra

3
Nội dung-Độ dài có nên được đặt thành kích thước tệp thực hay chỉ là số byte một phần được gửi đi? Trang này làm cho nó trông giống như nó phải là từng byte, nhưng đó không phải là những gì được thực hiện trong đoạn mã ví dụ ở trên. w3.org/Protocols/rfc2616/rfc2616-sec14.html
willus

3
Một lỗi đánh máy nhỏ khác: $start = $end - intval($range[0]);nên đượcrange[1]
BurninLeo

1
@ sarah.ferguson Mã được viết lại và cập nhật hoàn toàn, xem ở trên.
DaveRandom

16

Điều này hoạt động 100% siêu kiểm tra nó tôi đang sử dụng nó và không có vấn đề gì nữa.

        /* 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
Tôi đã ủng hộ vì giới hạn tốc độ thực sự hữu ích, tuy nhiên, kiểm tra MD5 trên tệp được tiếp tục (Firefox) cho thấy sự không khớp. Str_replace cho phạm vi $ là sai, phải là một khối nổ khác, kết quả là số và một dấu gạch ngang được thêm vào tiêu đề Phạm vi nội dung.
WhoIsRich

Làm thế nào để tùy chỉnh nó để hỗ trợ tải xuống tệp từ xa?
Siyamak Shahpasand

1
ý bạn muốn trích dẫn kép 'Content-Type: $ contentType';
Matt

set_time_limit (0); không thực sự thích hợp theo quan điểm của tôi. Có thể giới hạn hợp lý hơn là 24 giờ?
twojr

Cảm ơn bạn đã kiểm tra lỗi chính tả của tôi :)!
dùng1524615

15

Đúng. Hỗ trợ byteranges. Xem RFC 2616 phần 14.35 .

Về cơ bản, nó có nghĩa là bạn nên đọc Range tiêu đề và bắt đầu phân phát tệp từ độ lệch được chỉ định.

Điều này có nghĩa là bạn không thể sử dụng readfile (), vì nó phục vụ toàn bộ tệp. Thay vào đó, hãy sử dụng fopen () trước, sau đó fseek () đến vị trí chính xác, rồi sử dụng fpassthru () để cung cấp tệp.


4
fpassthru không phải là ý kiến ​​hay nếu tệp có nhiều megabyte, bạn có thể hết bộ nhớ. Chỉ cần fread () và print () theo từng phần.
Willem

3
fpassthru hoạt động tốt ở đây với hàng trăm megabyte. echo file_get_contents(...)không hoạt động (OOM). Vì vậy, tôi không nghĩ đó là một vấn đề. PHP 5.3.
Janus Troelsen

1
@JanusTroelsen Không, không phải. Tất cả phụ thuộc vào cấu hình máy chủ của bạn. Nếu bạn có một máy chủ mạnh, với nhiều bộ nhớ được phân bổ cho PHP, thì có lẽ nó hoạt động tốt với bạn. Trên các cấu hình "yếu" (nghĩa đen: lưu trữ được chia sẻ), việc sử dụng fpassthrusẽ không thành công trên các tệp thậm chí 50 MB. Bạn chắc chắn không nên sử dụng nó, nếu bạn đang cung cấp các tệp lớn trên cấu hình máy chủ yếu. Như @Wimmer đã chỉ ra một cách chính xác, fread+ printlà tất cả những gì bạn cần trong trường hợp này.
trejder

2
@trejder: Hãy xem ghi chú trên readfile () : readfile () sẽ không xuất hiện bất kỳ vấn đề bộ nhớ nào, ngay cả khi tự gửi các tệp lớn. Nếu bạn gặp lỗi hết bộ nhớ, hãy đảm bảo rằng bộ đệm đầu ra được tắt với ob_get_level ().
Janus Troelsen

1
@trejder vấn đề là bạn không định cấu hình bộ đệm đầu ra của mình đúng. Nó thực hiện các chunking tự động, nếu bạn yêu cầu: php.net/manual/en/... ví dụ output_buffering = 4096 (và nếu khuôn khổ của bạn không cho phép điều này, bạn có khuôn khổ sucks)
ZJR

11

Một cách thực sự hay để giải quyết vấn đề này mà không cần phải "cuộn" mã PHP của riêng bạn là sử dụng mô-đun mod_xsendfile Apache. Sau đó, trong PHP, bạn chỉ cần đặt các tiêu đề thích hợp. Apache phải làm việc của mình.

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

2
Điều gì xảy ra nếu bạn muốn hủy liên kết tệp sau khi gửi?
Janus Troelsen,

1
Nếu bạn muốn hủy liên kết tệp sau khi gửi, bạn cần một cờ đặc biệt để biểu thị điều đó, hãy xem XSendFilePath <absolute path> [AllowFileDelete]( tn123.org/mod_xsendfile/beta ).
Jens A. Koch,

9

Nếu bạn sẵn sàng cài đặt mô-đun PECL mới, cách dễ nhất để hỗ trợ tải xuống có thể tiếp tục với PHP là thông qua http_send_file(), như thế này

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

nguồn: http://www.php.net/manual/en/ Chức năng.http-send-file.php

Chúng tôi sử dụng nó để phục vụ nội dung được lưu trữ trong cơ sở dữ liệu và nó hoạt động như một sự quyến rũ!


3
Hoạt động như một sự quyến rũ. Tuy nhiên, hãy cẩn thận khi bạn chưa bật bộ đệm đầu ra (ob_start, v.v.). Đặc biệt khi gửi các tệp lớn, điều này sẽ đệm toàn bộ phạm vi được yêu cầu.
Pieter van Ginkel

Khi nào điều này được thêm vào PHP? Luôn ở đó?
thomthom

1
Đó là Pecl, không phải PHP. Tôi không có chức năng này.
Geo

4

Câu trả lời hàng đầu có nhiều lỗi khác nhau.

  1. Lỗi chính: Nó không xử lý chính xác tiêu đề Phạm vi. bytes a-bnên có nghĩa là [a, b]thay vì [a, b), vàbytes a- không được xử lý.
  2. Lỗi nhỏ: Nó không sử dụng bộ đệm để xử lý đầu ra. Điều này có thể tiêu tốn quá nhiều bộ nhớ và gây ra tốc độ thấp cho các tệp lớn.

Đây là mã đã sửa đổi của tôi:

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

Tại sao điều này cần ini_set('memory_limit', '-1');?
Mikko Rantalainen

1
@MikkoRantalainen Tôi quên mất. Bạn có thể thử xóa nó và xem điều gì sẽ xảy ra.
Mygod

1
Thật không may, bạn sẽ gặp lỗi trong phép gán $ end trong trường hợp $ đối sánh [2] không được đặt (ví dụ: với yêu cầu "Phạm vi = 0-"). Tôi đã sử dụng cái này thay thế:if(!isset($matches[2])) { $end=$fs-1; } else { $end = intval($matches[2]); }
Skynet

3

Có, bạn có thể sử dụng tiêu đề Phạm vi cho điều đó. Bạn cần cung cấp thêm 3 tiêu đề cho máy khách để tải xuống đầy đủ:

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

Đối với quá trình tải xuống bị gián đoạn, bạn cần kiểm tra tiêu đề Yêu cầu phạm vi bằng cách:

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

Và trong trường hợp này, đừng quên cung cấp nội dung có mã trạng thái 206:

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

Bạn sẽ nhận được các biến $ start và $ to từ tiêu đề yêu cầu và sử dụng fseek () để tìm kiếm vị trí chính xác trong tệp.


2
@ceejayoz: getallheaders () là một hàm php mà bạn nhận được nếu bạn đang sử dụng apache uk2.php.net/getallheaders
Tom Haigh



1

Tiếp tục tải xuống trong HTTP được thực hiện thông qua Rangetiêu đề. Nếu yêu cầu có chứa một Rangeheader, và nếu các chỉ số khác (ví dụ If-Match, If-Unmodified-Since) chỉ ra rằng nội dung vẫn không thay đổi kể từ khi việc download đã bắt đầu, bạn cho một mã số 206 phản ứng (chứ không phải 200), chỉ ra hàng loạt các byte bạn đang trở về bên trongContent-Range tiêu đề, sau đó cung cấp phạm vi đó trong nội dung phản hồi.

Tôi không biết làm thế nào để làm điều đó trong PHP.


1

Cảm ơn Theo! phương pháp của bạn không trực tiếp hoạt động để phát trực tuyến divx vì tôi thấy trình phát divx đang gửi các phạm vi như byte = 9932800-

nhưng nó đã chỉ cho tôi cách làm điều đó nên cảm ơn: D

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

0

Bạn có thể sử dụng mã dưới đây để hỗ trợ yêu cầu phạm vi byte trên bất kỳ trình duyệt nào

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