¿Descargas reanudables al usar PHP para enviar el archivo?

104

Estamos usando un script PHP para tunelizar las descargas de archivos, ya que no queremos exponer la ruta absoluta del archivo descargable:

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

Lamentablemente, notamos que el usuario final no puede reanudar las descargas pasadas a través de este script.

¿Hay alguna forma de admitir descargas reanudables con una solución basada en PHP?

Mark Amery
fuente

Respuestas:

102

Lo primero que debe hacer es enviar el Accept-Ranges: bytesencabezado en todas las respuestas, para decirle al cliente que admite contenido parcial. Entonces, si la solicitud con una Range: bytes=x-y se recibe la cabecera (con xy yson números) que analizar el rango del cliente está solicitando, abre el archivo como de costumbre, busque xbytes adelante y enviar el siguiente y- xbytes. También configure la respuesta en HTTP/1.0 206 Partial Content.

Sin haber probado nada, esto podría funcionar, más o 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);

Es posible que me haya perdido algo obvio y definitivamente he ignorado algunas fuentes potenciales de errores, pero debería ser un comienzo.

Aquí hay una descripción del contenido parcial y encontré información sobre el contenido parcial en la página de documentación de fread .

Theo
fuente
3
Pequeño error, su expresión regular debería ser: preg_match ('/ bytes = (\ d +) - (\ d +)? /', $ _SERVER ['HTTP_RANGE'], $ matches)
deepwell
1
Tienes razón y lo he cambiado. Sin embargo, es demasiado simplista de todos modos, de acuerdo con las especificaciones, puede hacer "bytes = xy", "bytes = -x", "bytes = x-", "bytes = xy, ab", etc., por lo que el error en el la versión anterior era la barra que faltaba al final, no la falta de un signo de interrogación.
Theo
7
Muy útil, pero tuve que hacer dos pequeños ajustes para que funcionara: 1. Si el cliente no envía el punto final en el rango (ya que está implícito), $lengthserá negativo. $length = (($matches[2]) ? intval($matches[2]) : $filesize) - $offset;arregla eso. 2. Content-Rangetrata el primer byte como byte 0, por lo que el último byte es $filesize - 1. Por lo tanto, tiene que serlo ($offset + $length - 1).
Dennis
1
Lo anterior no funciona para descargas grandes, aparece un "Error fatal de PHP: Se agotó el tamaño de memoria permitido de XXXX bytes (se intentó asignar XXX bytes) en". En mi caso, 100 MB era demasiado grande. Básicamente, guardas todos los archivos en una variable y los escupes.
sarah.ferguson
1
Puede resolver el problema de los archivos grandes leyéndolos en partes en lugar de todos a la vez.
dynamichael
71

EDITAR 2017/01: escribí una biblioteca para hacer esto en PHP> = 7.0 https://github.com/DaveRandom/Resume

EDITAR 2016/02: código reescrito completamente en un conjunto de herramientas modulares y un uso de ejemplo, en lugar de una función monolítica. Se han incorporado las correcciones mencionadas en los comentarios a continuación.


Una solución de trabajo probada (basada en gran medida en la respuesta de Theo anterior) que se ocupa de las descargas reanudables, en un conjunto de algunas herramientas independientes. Este código requiere PHP 5.4 o posterior.

Esta solución solo puede hacer frente a un rango por solicitud, pero bajo cualquier circunstancia con un navegador estándar en el que pueda pensar, esto no debería causar un problema.

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

Uso de ejemplo:

<?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
fuente
Código bastante bonito aquí. Encontré un error en la línea donde se establece $ length. Debe ser: $ longitud = $ final - $ inicio + 1;
bobwienholt
¿Cómo
pausaré la
3
¿Se debe establecer Content-Length en el tamaño real del archivo o solo en el número de bytes parciales que se envían? Esta página hace que parezca que deberían ser los bytes parciales, pero eso no es lo que se hace en el código de ejemplo anterior. w3.org/Protocols/rfc2616/rfc2616-sec14.html
willus
3
Otro pequeño error tipográfico: $start = $end - intval($range[0]);debería serrange[1]
BurninLeo
1
@ sarah.ferguson Código completamente reescrito y actualizado, ver arriba.
DaveRandom
16

Esto funciona 100% super compruébalo, lo estoy usando y ya no hay problemas.

        /* 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);
usuario1524615
fuente
1
Elegí mi voto porque el límite de velocidad es realmente útil, sin embargo, una verificación MD5 en un archivo reanudado (Firefox) mostró una falta de coincidencia. El str_replace para $ range es incorrecto, debería ser otro explosión, el resultado numérico y un guión agregado al encabezado Content-Range.
WhoIsRich
¿Cómo personalizarlo para admitir la descarga remota de archivos?
Siyamak Shahpa y
1
quiso decir entre comillas dobles 'Content-Type: $ contentType';
Matt
set_time_limit (0); no es realmente apropiado en mi opinión. ¿Quizás un límite más razonable de 24 horas?
twojr
¡Gracias por revisar mis errores tipográficos :)!
user1524615
15

Si. Soporte de rangos de bytes. Consulte la sección 14.35 de RFC 2616 .

Básicamente significa que debe leer el Rangeencabezado y comenzar a servir el archivo desde el desplazamiento especificado.

Esto significa que no puede usar readfile (), ya que sirve para todo el archivo. En su lugar, use fopen () primero, luego fseek () a la posición correcta, y luego use fpassthru () para servir el archivo.

Sietse
fuente
4
fpassthru no es una buena idea si el archivo tiene varios megabytes, es posible que se quede sin memoria. Simplemente fread () e imprima () en trozos.
Willem
3
fpassthru funciona muy bien aquí con cientos de megabytes. echo file_get_contents(...)no funcionó (OOM). Así que no creo que eso sea un problema. PHP 5.3.
Janus Troelsen
1
@JanusTroelsen No, no lo es. Todo depende de la configuración de su servidor. Si tiene un servidor fuerte, con mucha memoria asignada a PHP, entonces tal vez funcione bien para usted. En configuraciones "débiles" (literalmente: alojamientos compartidos), el uso fpassthrufallará incluso en archivos de 50 MB. Definitivamente no debería usarlo si está sirviendo archivos grandes en una configuración de servidor débil. Como @Wimmer señala correctamente, fread+ printes todo lo que necesita en este caso.
trejder
2
@trejder: Vea la nota sobre readfile () : readfile () no presentará ningún problema de memoria, incluso al enviar archivos grandes, por sí solo. Si encuentra un error de memoria insuficiente, asegúrese de que el búfer de salida esté desactivado con ob_get_level ().
Janus Troelsen
1
@trejder, el problema es que no configuró correctamente el búfer de salida. Hace la fragmentación automáticamente, si le dice a: php.net/manual/en/… por ejemplo, output_buffering = 4096 (y si su marco no lo permite, su marco apesta)
ZJR
11

Una manera realmente agradable de resolver esto sin tener que "rodar su propio" código PHP es usar el módulo mod_xsendfile Apache. Luego, en PHP, simplemente establece los encabezados apropiados. Apache consigue hacer lo suyo.

header("X-Sendfile: /path/to/file");
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; file=\"filename\"");
Jonathan Hawkes
fuente
2
¿Qué sucede si desea desvincular el archivo después de enviarlo?
Janus Troelsen
1
Si desea desvincular el archivo después de enviarlo, necesita una bandera especial para indicarlo, consulte XSendFilePath <absolute path> [AllowFileDelete]( tn123.org/mod_xsendfile/beta ).
Jens A. Koch
9

Si está dispuesto a instalar un nuevo módulo PECL, la forma más sencilla de admitir descargas reanudables con PHP es a través de http_send_file()esta.

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

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

Lo usamos para servir contenido almacenado en bases de datos y funciona como un encanto.

Justin T.
fuente
3
Funciona de maravilla. Sin embargo, tenga cuidado de no tener activado el búfer de salida (ob_start, etc.). Especialmente cuando se envían archivos grandes, esto almacenará en búfer el rango solicitado completo.
Pieter van Ginkel
¿Cuándo se agregó esto a PHP? ¿Siempre ha estado ahí?
thomthom
1
Eso es Pecl, no PHP. No tengo esta función.
Geo
4

La respuesta principal tiene varios errores.

  1. El error principal: no maneja el encabezado Range correctamente. bytes a-bdebería significar en [a, b]lugar de [a, b), y bytes a-no se maneja.
  2. El error menor: no usa búfer para manejar la salida. Esto puede consumir demasiada memoria y causar baja velocidad para archivos grandes.

Aquí está mi 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);
Dios mío
fuente
¿Por qué esto necesita ini_set('memory_limit', '-1');?
Mikko Rantalainen
1
@MikkoRantalainen Lo olvidé. Podría intentar eliminarlo y ver qué sucede.
Mygod
1
Desafortunadamente, arrojará un error en la asignación de $ end en caso de que $ matches [2] no estén configurados (por ejemplo, con una solicitud "Range = 0-"). Usé esto en su lugar:if(!isset($matches[2])) { $end=$fs-1; } else { $end = intval($matches[2]); }
Skynet
3

Sí, puede usar el encabezado Range para eso. Debe proporcionar 3 encabezados más al cliente para una descarga completa:

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

En el caso de una descarga interrumpida, debe verificar el encabezado de solicitud de Rango de la siguiente manera:

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

Y en este caso, no olvide entregar el contenido con el código de estado 206:

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

Obtendrá las variables $ start y $ to del encabezado de la solicitud, y usará fseek () para buscar la posición correcta en el archivo.

Zsolt Szeberenyi
fuente
2
@ceejayoz: getallheaders () es una función php que obtiene si está usando apache uk2.php.net/getallheaders
Tom Haigh
1

La reanudación de las descargas en HTTP se realiza a través del Rangeencabezado. Si la solicitud contiene un Rangeencabezado y otros indicadores (p If-Match. Ej. , If-Unmodified-Since) Indican que el contenido no ha cambiado desde que se inició la descarga, proporcione un código de respuesta 206 (en lugar de 200), indique el rango de bytes que está devolviendo. en el Content-Rangeencabezado, luego proporcione ese rango en el cuerpo de la respuesta.

Sin embargo, no sé cómo hacer eso en PHP.

Mike Dimmick
fuente
1

¡Gracias Theo! su método no funcionó directamente para la transmisión de divx porque descubrí que el reproductor de divx estaba enviando rangos como bytes = 9932800-

pero me mostró cómo hacerlo así que gracias: D

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

Puede usar el siguiente código para el soporte de solicitudes de rango de bytes en cualquier 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);
?>
pitufo
fuente