Downloads retomáveis ​​ao usar PHP para enviar o arquivo?

104

Estamos usando um script PHP para fazer o tunelamento de downloads de arquivos, pois não queremos expor o caminho absoluto do arquivo para download:

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

Infelizmente, notamos que os downloads que passaram por esse script não podem ser retomados pelo usuário final.

Existe alguma maneira de oferecer suporte a downloads recuperáveis ​​com uma solução baseada em PHP?

Mark Amery
fonte

Respostas:

102

A primeira coisa que você precisa fazer é enviar o Accept-Ranges: bytescabeçalho em todas as respostas, para informar ao cliente que você oferece suporte a conteúdo parcial. Então, se pedido com um Range: bytes=x-y cabeçalho é recebido (com xe ysendo números) você analisar o intervalo o cliente está solicitando, abra o arquivo como de costume, procure xbytes em frente e enviar os próximos y- xbytes. Defina também a resposta como HTTP/1.0 206 Partial Content.

Sem ter testado nada, isso poderia funcionar, mais ou menos:

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

Posso ter perdido algo óbvio e definitivamente ignorei algumas fontes potenciais de erros, mas deve ser um começo.

Há uma descrição do conteúdo parcial aqui e eu encontrei algumas informações sobre o conteúdo parcial na página de documentação do fread .

Theo
fonte
3
Pequeno bug, sua expressão regular deve ser: preg_match ('/ bytes = (\ d +) - (\ d +)? /', $ _SERVER ['HTTP_RANGE'], $ corresponde)
deepwell
1
Você está certo e eu mudei isso. No entanto, é muito simplista de qualquer maneira, de acordo com as especificações, você pode fazer "bytes = xy", "bytes = -x", "bytes = x-", "bytes = xy, ab", etc. então o bug no a versão anterior era a barra final que faltava, não a falta de um ponto de interrogação.
Theo
7
Muito útil, mas tive que fazer dois pequenos ajustes para que funcionasse: 1. Se o cliente não enviar o endpoint no intervalo (já que está implícito), $lengthserá negativo. $length = (($matches[2]) ? intval($matches[2]) : $filesize) - $offset;corrige isso. 2. Content-Rangetrata o primeiro byte como byte 0, então o último byte é $filesize - 1. Portanto, tem que ser ($offset + $length - 1).
Dennis,
1
Acima não funciona para downloads grandes, você obtém um "Erro fatal do PHP: Tamanho de memória permitido de XXXX bytes esgotados (tentativa de alocar XXX bytes) em". No meu caso, 100 MB era muito grande. Basicamente, você salva todo o arquivo em uma variável e o despeja.
sarah.ferguson
1
Você pode resolver o problema de arquivos grandes lendo-os em partes, em vez de todos de uma vez.
dynamichael
71

EDIT 2017/01 - escrevi uma biblioteca para fazer isso em PHP> = 7.0 https://github.com/DaveRandom/Resume

EDIT 2016/02 - Código completamente reescrito para um conjunto de ferramentas modulares um exemplo de uso, ao invés de uma função monolítica. As correções mencionadas nos comentários abaixo foram incorporadas.


Uma solução testada e funcional (baseada fortemente na resposta de Theo acima) que lida com downloads recuperáveis, em um conjunto de algumas ferramentas autônomas. Este código requer PHP 5.4 ou posterior.

Essa solução ainda pode lidar com apenas um intervalo por solicitação, mas em qualquer circunstância com um navegador padrão que eu possa imaginar, isso não deve causar problemas.

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

Exemplo de uso:

<?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
fonte
Um código muito bom aqui. Eu encontrei um bug na linha onde $ length está definido. Deve ser: $ length = $ end - $ start + 1;
bobwienholt
Como vou pausar o download
Prasanth Bendra
3
Deve Content-Length ser definido para o tamanho real do arquivo ou apenas o número de bytes parciais sendo enviados? Esta página faz com que pareça que deveriam ser bytes parciais, mas não é isso que é feito no código de exemplo acima. w3.org/Protocols/rfc2616/rfc2616-sec14.html
willus
3
Outro pequeno erro de digitação: $start = $end - intval($range[0]);deveria serrange[1]
BurninLeo
1
@ sarah.ferguson Código completamente reescrito e atualizado, veja acima.
DaveRandom
16

Isso funciona 100% super, verifique se estou usando e sem problemas mais.

        /* 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
fonte
1
Eu votei a favor porque o limite de velocidade é realmente útil, no entanto, uma verificação MD5 em um arquivo retomado (Firefox) mostrou uma incompatibilidade. O str_replace para $ range está errado, deveria ser outra explosão, o resultado tornado numérico e um traço adicionado ao cabeçalho Content-Range.
WhoIsRich
Como personalizá-lo para suportar o download remoto de arquivos?
Siyamak Shahpasand
1
você quis dizer aspas 'Content-Type: $ contentType';
Matt
set_time_limit (0); não é realmente apropriado na minha opinião. Um limite mais razoável de 24 horas, talvez?
duas vezesjr
Obrigado por verificar meus erros de digitação :)!
user1524615
15

Sim. Byteranges de suporte. Consulte a seção 14.35 do RFC 2616 .

Basicamente, significa que você deve ler o Rangecabeçalho e começar a servir o arquivo a partir do deslocamento especificado.

Isso significa que você não pode usar readfile (), uma vez que serve o arquivo inteiro. Em vez disso, use fopen () primeiro, depois fseek () para a posição correta e, em seguida, use fpassthru () para servir o arquivo.

Sietse
fonte
4
fpassthru não é uma boa ideia se o arquivo tiver vários megabytes, você pode ficar sem memória. Apenas fread () e print () em pedaços.
Willem
3
fpassthru funciona muito bem aqui com centenas de megabytes. echo file_get_contents(...)não funcionou (OOM). Portanto, não acho que isso seja um problema. PHP 5.3.
Janus Troelsen de
1
@JanusTroelsen Não, não é. Tudo depende da configuração do seu servidor. Se você tiver um servidor forte, com muita memória alocada para o PHP, talvez funcione bem para você. Em configurações "fracas" (literalmente: hospedagens compartilhadas), o uso fpassthrufalhará até em arquivos de 50 MB. Definitivamente, você não deve usá-lo se estiver servindo arquivos grandes em uma configuração de servidor fraca. Como @Wimmer corretamente aponta, fread+ printé tudo o que você precisa neste caso.
trejder
2
@trejder: Veja a nota sobre readfile () : readfile () não apresentará nenhum problema de memória, mesmo ao enviar arquivos grandes, por conta própria. Se você encontrar um erro de falta de memória, certifique-se de que o buffer de saída esteja desativado com ob_get_level ().
Janus Troelsen
1
@trejder, o problema é que você não configurou o buffer de saída corretamente. Ele faz a fragmentação automaticamente, se você disser para: php.net/manual/en/… por exemplo, output_buffering = 4096 (e se seu framework não permitir isso, seu framework é uma merda)
ZJR
11

Uma maneira muito boa de resolver isso sem ter que "lançar seu próprio" código PHP é usar o módulo mod_xsendfile do Apache. Então, no PHP, você apenas define os cabeçalhos apropriados. O Apache faz seu trabalho.

header("X-Sendfile: /path/to/file");
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; file=\"filename\"");
Jonathan Hawkes
fonte
2
E se você quiser desvincular o arquivo após o envio?
Janus Troelsen de
1
Se você deseja desvincular o arquivo após o envio, você precisa de um sinalizador especial para indicar isso, consulte XSendFilePath <absolute path> [AllowFileDelete]( tn123.org/mod_xsendfile/beta ).
Jens A. Koch
9

Se você estiver disposto a instalar um novo módulo PECL, a maneira mais fácil de suportar downloads retomáveis ​​com PHP é através http_send_file(), como este

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

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

Nós o usamos para servir conteúdo armazenado em banco de dados e funciona perfeitamente!

Justin T.
fonte
3
Funciona como um encanto. No entanto, tome cuidado para não ter o buffer de saída (ob_start etc) ativado. Especialmente ao enviar arquivos grandes, isso armazenará em buffer todo o intervalo solicitado.
Pieter van Ginkel
Quando isso foi adicionado ao PHP? Sempre esteve lá?
thomthom
1
Isso é Pecl, não PHP. Eu não tenho essa função.
Geo
4

A resposta principal contém vários bugs.

  1. O principal bug: ele não controla o cabeçalho Range corretamente. bytes a-bdeve significar em [a, b]vez de [a, b)e bytes a-não é manipulado.
  2. O pequeno bug: ele não usa buffer para lidar com a saída. Isso pode consumir muita memória e causar baixa velocidade para arquivos grandes.

Aqui está meu código modificado:

// 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);
Meu Deus
fonte
Por que isso precisa ini_set('memory_limit', '-1');?
Mikko Rantalainen
1
@MikkoRantalainen eu esqueci. Você pode tentar removê-lo e ver o que acontece.
Mygod
1
Infelizmente, você lançará um erro na atribuição $ end caso $ coincida [2] não seja definido (por exemplo, com uma solicitação "Faixa = 0-"). Em vez disso, usei:if(!isset($matches[2])) { $end=$fs-1; } else { $end = intval($matches[2]); }
Skynet
3

Sim, você pode usar o cabeçalho Range para isso. Você precisa fornecer mais 3 cabeçalhos ao cliente para um download completo:

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

Em seguida, para um download interrompido, você precisa verificar o cabeçalho da solicitação Range:

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

E, neste caso, não se esqueça de servir o conteúdo com código de status 206:

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

Você obterá as variáveis ​​$ start e $ to do cabeçalho da solicitação e usará fseek () para buscar a posição correta no arquivo.

Zsolt Szeberenyi
fonte
2
@ceejayoz: getallheaders () é uma função php que você obtém se estiver usando apache uk2.php.net/getallheaders
Tom Haigh
2

Classe habilitada para compositor pequeno que funciona da mesma maneira que pecl http_send_file. Isso significa suporte para downloads recuperáveis ​​e aceleração. https://github.com/diversen/http-send-file

dennis
fonte
1

A retomada dos downloads em HTTP é feita por meio do Rangecabeçalho. Se a solicitação contém um Rangecabeçalho e se outros indicadores (por exemplo If-Match, If-Unmodified-Since) indicam que o conteúdo não mudou desde o início do download, você fornece um código de resposta 206 (em vez de 200), indica o intervalo de bytes que está retornando noContent-Range cabeçalho e, em seguida, forneça esse intervalo no corpo da resposta.

Não sei como fazer isso em PHP, no entanto.

Mike Dimmick
fonte
1

Obrigado Theo! seu método não funcionou diretamente para streaming divx porque descobri que o player divx estava enviando intervalos como bytes = 9932800-

mas me mostrou como fazer isso, obrigado: D

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

Você pode usar o código abaixo para suporte de solicitação de intervalo de bytes em qualquer navegador

    <?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);
?>
smurf
fonte