Возобновляемые загрузки при использовании PHP для отправки файла?

104

Мы используем PHP-скрипт для туннелирования загрузок файлов, поскольку мы не хотим раскрывать абсолютный путь к загружаемому файлу:

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

К сожалению, мы заметили, что загрузки, прошедшие через этот скрипт, не могут быть возобновлены конечным пользователем.

Есть ли способ поддержать возобновляемые загрузки с помощью такого решения на основе PHP?

Марк Эмери
источник

Ответы:

102

Первое, что вам нужно сделать, это отправить Accept-Ranges: bytesзаголовок во всех ответах, чтобы сообщить клиенту, что вы поддерживаете частичное содержимое. Затем, если запрос с Range: bytes=x-y заголовком принимается (с xи yявляющихся числами) вы разбираете диапазон клиент запрашивает, откройте файл , как обычно, стремятся 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'], $
match
1
Вы правы, и я это изменил. Тем не менее, я в любом случае это слишком упрощенно, в соответствии со спецификациями вы можете использовать «bytes = xy», «bytes = -x», «bytes = x-», «bytes = xy, ab» и т. Д., Поэтому ошибка в в предыдущей версии отсутствовала косая черта на конце, а не отсутствовал вопросительный знак.
Тео
7
Очень полезно, но мне пришлось сделать две незначительные настройки, чтобы заставить его работать: 1. Если клиент не отправляет конечную точку в диапазоне (поскольку это неявно), $lengthбудет отрицательным. $length = (($matches[2]) ? intval($matches[2]) : $filesize) - $offset;исправляет это. 2. Content-Rangeобрабатывает первый байт как байт 0, следовательно, последний байт $filesize - 1. Следовательно, так и должно быть ($offset + $length - 1).
Деннис
1
Вышеупомянутое не работает для больших загрузок, вы получаете сообщение «Неустранимая ошибка PHP: допустимый размер памяти XXXX байтов исчерпан (попытка выделить XXX байтов) в». В моем случае 100 МБ было слишком большим. Вы в основном сохраняете весь файл в переменной и выкладываете его.
sarah.ferguson
1
Вы можете решить проблему с большим файлом, прочитав его по частям, а не все сразу.
dynamichael
71

EDIT 2017/01 - я написал библиотеку , чтобы сделать это в PHP> = 7,0 https://github.com/DaveRandom/Resume

РЕДАКТИРОВАТЬ 2016/02 - Код полностью переписан в набор модульных инструментов в качестве примера использования, а не монолитной функции. Исправления, упомянутые в комментариях ниже, были внесены.


Проверенное рабочее решение (в значительной степени основанное на ответе Тео выше), которое имеет дело с возобновляемыми загрузками в наборе из нескольких автономных инструментов. Для этого кода требуется 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;
DaveRandom
источник
Здесь довольно приятный код. Я нашел ошибку в строке, где установлена ​​длина $. Должно быть: $ length = $ end - $ start + 1;
bobwienholt
Как мне приостановить загрузку
Прасант Бендра
3
Должен ли Content-Length быть установлен на фактический размер файла или просто на количество отправляемых неполных байтов? Эта страница выглядит так, как будто это должны быть частичные байты, но это не то, что сделано в приведенном выше примере кода. w3.org/Protocols/rfc2616/rfc2616-sec14.html
willus
3
Еще одна маленькая опечатка: $start = $end - intval($range[0]);должно бытьrange[1]
BurninLeo 03
1
@ sarah.ferguson Код полностью переписан и обновлен, см. выше.
DaveRandom 01
15

Да. Поддержите изменения. См. RFC 2616, раздел 14.35 .

Это в основном означает, что вы должны прочитать Rangeзаголовок и начать обслуживание файла с указанного смещения.

Это означает, что вы не можете использовать readfile (), поскольку он обслуживает весь файл. Вместо этого сначала используйте fopen () , затем fseek () в правильную позицию, а затем используйте fpassthru () для обслуживания файла.

Sietse
источник
4
fpassthru - не лучшая идея, если размер файла составляет несколько мегабайт, у вас может закончиться память. Просто fread () и print () по частям.
Виллем,
3
Здесь отлично работает fpassthru с сотнями мегабайт. echo file_get_contents(...)не заработало (ООМ). Так что я не думаю, что это проблема. PHP 5.3.
Janus Troelsen
1
@JanusTroelsen Нет, это не так. Все зависит от конфигурации вашего сервера. Если у вас мощный сервер с большим объемом памяти, выделенным для PHP, возможно, он вам подходит. В «слабых» конфигурациях (буквально: общих хостингах) использование fpassthruне удастся даже для файлов размером 50 МБ. Вам определенно не следует использовать его, если вы обслуживаете большие файлы на слабой конфигурации сервера. Как правильно указывает @Wimmer, fread+ print- это все, что вам нужно в этом случае.
Trejder
2
@trejder: См. примечание к readfile () : readfile () не вызовет проблем с памятью, даже при отправке больших файлов, сам по себе. Если вы столкнулись с ошибкой нехватки памяти, убедитесь, что буферизация вывода отключена с помощью ob_get_level ().
Янус Троелсен
1
@trejder проблема в том, что вы неправильно настроили буферизацию вывода. Он выполняет фрагменты автоматически, если вы сообщите об этом: php.net/manual/en/… например, output_buffering = 4096 (и если ваш фреймворк не позволяет этого, вы фреймворк отстой)
ZJR
15

Это работает на 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);
user1524615
источник
1
Я проголосовал за, потому что ограничение скорости действительно полезно, однако проверка MD5 возобновленного файла (Firefox) показала несоответствие. Str_replace для $ range неверен, должно быть еще одно разнесение, результат должен быть числовым, а тире добавлено в заголовок Content-Range.
WhoIsRich
Как настроить его для поддержки удаленной загрузки файлов?
Siyamak Shahpasand
2
вы хотели заключить в двойные кавычки Content-Type: $ contentType;
Мэтт
set_time_limit (0); на мой взгляд, не совсем подходит. Может быть, более разумный лимит в 24 часа?
twojr
Спасибо за проверку моих опечаток :)!
user1524615 06
11

Действительно хороший способ решить эту проблему без необходимости «катить свой собственный» PHP-код - это использовать модуль Apache mod_xsendfile. Затем в PHP вы просто устанавливаете соответствующие заголовки. Apache делает свое дело.

header("X-Sendfile: /path/to/file");
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; file=\"filename\"");
Джонатан Хоукс
источник
2
Что делать, если вы хотите отключить файл после отправки?
Янус Троэльсен
1
Если вы хотите отключить файл после отправки, вам понадобится специальный флаг, чтобы указать это, см. XSendFilePath <absolute path> [AllowFileDelete]( Tn123.org/mod_xsendfile/beta ).
Jens A. Koch
9

Если вы хотите установить новый модуль 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

Мы используем его для обслуживания контента, хранящегося в базе данных, и он работает как шарм!

Джастин Т.
источник
3
Работает как шарм. Однако будьте осторожны, у вас не включена буферизация вывода (ob_start и т. Д.). Это приведет к буферизации всего запрошенного диапазона, особенно при отправке больших файлов.
Питер ван Гинкель
Когда это было добавлено в PHP? Всегда был там?
thomthom 08
1
Это Pecl, а не PHP. У меня нет этой функции.
Geo
4

В верхнем ответе есть различные ошибки.

  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');?
Mikko Rantalainen
1
@MikkoRantalainen Я забыл. Вы можете попробовать удалить его и посмотреть, что произойдет.
Mygod
1
К сожалению, вы вызовете ошибку в назначении $ end, если $ match [2] не установлен (например, с запросом «Range = 0-»). Вместо этого я использовал это:if(!isset($matches[2])) { $end=$fs-1; } else { $end = intval($matches[2]); }
Скайнет
3

Да, вы можете использовать для этого заголовок 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 () для поиска правильной позиции в файле.

Жолт Себереньи
источник
2
@ceejayoz: getallheaders () - это функция php, которую вы получаете, если используете apache uk2.php.net/getallheaders
Том Хэй
2

Небольшой класс с поддержкой композитора, который работает так же, как pecl http_send_file. Это означает поддержку возобновляемых загрузок и ограничения. https://github.com/diversen/http-send-file

Деннис
источник
1

Возобновление загрузки по протоколу HTTP осуществляется через Rangeзаголовок. Если запрос содержит Rangeзаголовок, и если другие индикаторы (например If-Match, If-Unmodified-Since) указывают на то, что содержимое не изменилось с момента начала загрузки, вы даете код ответа 206 (а не 200), указываете диапазон возвращаемых байтов. в Content-Rangeзаголовке, затем укажите этот диапазон в теле ответа.

Однако я не знаю, как это сделать в PHP.

Майк Диммик
источник
1

Спасибо, Тео! ваш метод не работал напрямую для потоковой передачи divx, потому что я обнаружил, что проигрыватель divx отправлял диапазоны, такие как bytes = 9932800-

но он показал мне, как это сделать, так что спасибо: D

if(isset($_SERVER['HTTP_RANGE']))
{
    file_put_contents('showrange.txt',$_SERVER['HTTP_RANGE']);
Barbatrux
источник
0

Вы можете использовать приведенный ниже код для поддержки запроса диапазона байтов в любом браузере.

    <?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);
?>
смурф
источник