Téléchargements avec reprise lors de l'utilisation de PHP pour envoyer le fichier?

104

Nous utilisons un script PHP pour les téléchargements de fichiers en tunnel, car nous ne voulons pas exposer le chemin absolu du fichier téléchargeable:

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

Malheureusement, nous avons remarqué que les téléchargements passés via ce script ne peuvent pas être repris par l'utilisateur final.

Existe-t-il un moyen de prendre en charge les téléchargements avec reprise avec une telle solution PHP?

Mark Amery
la source

Réponses:

102

La première chose à faire est d'envoyer l'en- Accept-Ranges: bytestête dans toutes les réponses, pour indiquer au client que vous prenez en charge le contenu partiel. Ensuite, si la demande avec un en- Range: bytes=x-y tête est reçu (avec xet yétant des nombres) vous analysez la plage le client demande, ouvrez le fichier comme d' habitude, Seek xoctets avant et d' envoyer les prochains y- xoctets. Définissez également la réponse sur HTTP/1.0 206 Partial Content.

Sans avoir testé quoi que ce soit, cela pourrait fonctionner, plus ou moins:

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

J'ai peut-être manqué quelque chose d'évident, et j'ai très certainement ignoré certaines sources potentielles d'erreurs, mais cela devrait être un début.

Il y a une description du contenu partiel ici et j'ai trouvé des informations sur le contenu partiel sur la page de documentation de fread .

Théo
la source
3
Petit bogue, votre expression régulière devrait être: preg_match ('/ bytes = (\ d +) - (\ d +)? /', $ _SERVER ['HTTP_RANGE'], $ matches)
deepwell
1
Vous avez raison et je l'ai changé. Cependant, je suis de toute façon trop simpliste, selon les spécifications, vous pouvez faire "bytes = xy", "bytes = -x", "bytes = x-", "bytes = xy, ab", etc. donc le bogue dans le la version précédente était la barre oblique manquante, pas l'absence d'un point d'interrogation.
Theo
7
Très utile, mais j'ai dû faire deux ajustements mineurs pour que cela fonctionne: 1. Si le client n'envoie pas le point de terminaison dans la plage (car il est implicite), $lengthsera négatif. $length = (($matches[2]) ? intval($matches[2]) : $filesize) - $offset;corrige cela. 2. Content-Rangetraite le premier octet comme un octet 0, donc le dernier octet l'est $filesize - 1. Par conséquent, il doit l'être ($offset + $length - 1).
Dennis
1
Ci-dessus ne fonctionne pas pour les gros téléchargements, vous obtenez une "Erreur fatale PHP: taille de mémoire autorisée de XXXX octets épuisée (essayé d'allouer XXX octets) dans". Dans mon cas, 100 Mo était trop gros. Vous enregistrez essentiellement tous les fichiers dans une variable et les crachez.
sarah.ferguson
1
Vous pouvez résoudre le problème des fichiers volumineux en les lisant par blocs au lieu de tous en même temps.
dynamichael
71

EDIT 2017/01 - J'ai écrit une bibliothèque pour faire cela en PHP> = 7.0 https://github.com/DaveRandom/Resume

EDIT 2016/02 - Code complètement réécrit en un ensemble d'outils modulaires, un exemple d'utilisation, plutôt qu'une fonction monolithique. Les corrections mentionnées dans les commentaires ci-dessous ont été intégrées.


Une solution testée et fonctionnelle (largement basée sur la réponse de Theo ci-dessus) qui traite des téléchargements avec reprise, dans un ensemble de quelques outils autonomes. Ce code nécessite PHP 5.4 ou version ultérieure.

Cette solution ne peut toujours faire face qu'à une plage par requête, mais en toute circonstance avec un navigateur standard auquel je pense, cela ne devrait pas poser de problème.

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

Exemple d'utilisation:

<?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
la source
Code assez sympa ici. J'ai trouvé un bug sur la ligne où $ length est défini. Doit être: $ length = $ end - $ start + 1;
bobwienholt
Comment puis-je suspendre le téléchargement
Prasanth Bendra
3
Content-Length doit-il être défini sur la taille réelle du fichier ou simplement sur le nombre d'octets partiels envoyés? Cette page donne l'impression que ce devrait être les octets partiels, mais ce n'est pas ce qui est fait dans l'exemple de code ci-dessus. w3.org/Protocols/rfc2616/rfc2616-sec14.html
willus
3
Une autre petite faute de frappe: $start = $end - intval($range[0]);devrait êtrerange[1]
BurninLeo
1
@ sarah.ferguson Code entièrement réécrit et mis à jour, voir ci-dessus.
DaveRandom
16

Cela fonctionne à 100% super vérifiez que je l'utilise et plus de problèmes.

        /* 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);
utilisateur1524615
la source
1
J'ai voté pour parce que la limite de vitesse est vraiment utile, mais une vérification MD5 sur un fichier de reprise (Firefox) a montré une discordance. Le str_replace pour $ range est erroné, devrait être une autre explosion, le résultat rendu numérique et un tiret ajouté à l'en-tête Content-Range.
WhoIsRich
Comment le personnaliser pour prendre en charge le téléchargement de fichiers à distance?
Siyamak Shahpasand
1
vous vouliez double-citer «Content-Type: $ contentType»;
Matt
set_time_limit (0); n'est pas vraiment approprié à mon avis. Une limite plus raisonnable de 24 heures peut-être?
deux fois le
Merci d'avoir vérifié mes fautes de frappe :)!
user1524615
15

Oui. Prend en charge les byteranges. Voir la section 14.35 de la RFC 2616 .

Cela signifie essentiellement que vous devez lire l'en- Rangetête et commencer à servir le fichier à partir du décalage spécifié.

Cela signifie que vous ne pouvez pas utiliser readfile (), car cela sert l'ensemble du fichier. À la place, utilisez d'abord fopen () , puis fseek () à la bonne position, puis utilisez fpassthru () pour servir le fichier.

Sietse
la source
4
fpassthru n'est pas une bonne idée si le fichier fait plusieurs mégaoctets, vous risquez de manquer de mémoire. Fread () et print () en morceaux.
Willem
3
fpassthru fonctionne très bien ici avec des centaines de mégaoctets. echo file_get_contents(...)n'a pas fonctionné (MOO). Je ne pense donc pas que ce soit un problème. PHP 5.3.
Janus Troelsen le
1
@JanusTroelsen Non, ce n'est pas le cas. Tout dépend de la configuration de votre serveur. Si vous avez un serveur puissant, avec beaucoup de mémoire allouée à PHP, alors peut-être que cela fonctionne bien pour vous. Sur les configurations «faibles» (littéralement: hébergements partagés), l'utilisation fpassthruéchouera même sur des fichiers de 50 Mo. Vous ne devriez certainement pas l'utiliser si vous servez des fichiers volumineux sur une configuration de serveur faible. Comme @Wimmer le souligne correctement, fread+ printest tout ce dont vous avez besoin dans ce cas.
trejder
2
@trejder: Voir la note sur readfile () : readfile () ne présentera aucun problème de mémoire, même lors de l'envoi de gros fichiers, seul. Si vous rencontrez une erreur de mémoire insuffisante, assurez-vous que la mémoire tampon de sortie est désactivée avec ob_get_level ().
Janus Troelsen
1
@trejder le problème est que vous n'avez pas correctement configuré votre tampon de sortie. Il effectue la segmentation automatiquement, si vous le dites à: php.net/manual/en / ... par exemple output_buffering = 4096 (et si votre framework ne le permet pas, votre framework est nul)
ZJR
11

Un très bon moyen de résoudre ce problème sans avoir à "lancer votre propre" code PHP est d'utiliser le module Apache mod_xsendfile. Ensuite, en PHP, il vous suffit de définir les en-têtes appropriés. Apache doit faire son travail.

header("X-Sendfile: /path/to/file");
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; file=\"filename\"");
Jonathan Hawkes
la source
2
Que faire si vous souhaitez dissocier le fichier après l'envoi?
Janus Troelsen le
1
Si vous souhaitez dissocier le fichier après l'envoi, vous avez besoin d'un indicateur spécial pour l'indiquer, voir XSendFilePath <absolute path> [AllowFileDelete]( tn123.org/mod_xsendfile/beta ).
Jens A. Koch
9

Si vous êtes prêt à installer un nouveau module PECL, le moyen le plus simple de prendre en charge les téléchargements avec reprise avec PHP consiste à utiliserhttp_send_file() , comme ceci

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

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

Nous l'utilisons pour servir du contenu stocké dans une base de données et cela fonctionne comme un charme!

Justin T.
la source
3
Fonctionne comme un charme. Cependant, veillez à ne pas activer la mise en mémoire tampon de sortie (ob_start, etc.). Surtout lors de l'envoi de gros fichiers, cela mettra en mémoire tampon la plage demandée complète.
Pieter van Ginkel
Quand cela a-t-il été ajouté à PHP? Vous avez toujours été là?
thomthom
1
C'est Pecl, pas PHP. Je n'ai pas cette fonction.
Geo
4

La première réponse a plusieurs bugs.

  1. Le bogue majeur: il ne gère pas correctement l'en-tête Range. bytes a-bdevrait signifier [a, b]au lieu de [a, b), et bytes a-n'est pas gérée.
  2. Le bogue mineur: il n'utilise pas de tampon pour gérer la sortie. Cela peut consommer trop de mémoire et entraîner une faible vitesse pour les fichiers volumineux.

Voici mon code modifié:

// 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);
Mon Dieu
la source
Pourquoi ce besoin ini_set('memory_limit', '-1');?
Mikko Rantalainen
1
@MikkoRantalainen j'ai oublié. Vous pouvez essayer de le supprimer et voir ce qui se passe.
Mygod
1
Malheureusement, vous lancerez une erreur dans l'affectation $ end au cas où $ correspond [2] n'est pas défini (par exemple avec une requête "Range = 0-"). J'ai utilisé ceci à la place:if(!isset($matches[2])) { $end=$fs-1; } else { $end = intval($matches[2]); }
Skynet
3

Oui, vous pouvez utiliser l'en-tête Range pour cela. Vous devez donner 3 autres en-têtes au client pour un téléchargement complet:

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

Que pour un téléchargement interrompu, vous devez vérifier l'en-tête de la requête Range en:

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

Et dans ce cas, n'oubliez pas de servir le contenu avec le code d'état 206:

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

Vous obtiendrez les variables $ start et $ to à partir de l'en-tête de la requête, et utiliserez fseek () pour rechercher la position correcte dans le fichier.

Zsolt Szeberenyi
la source
2
@ceejayoz: getallheaders () est une fonction php que vous obtenez si vous utilisez apache uk2.php.net/getallheaders
Tom Haigh
2

Petite classe activée pour composer qui fonctionne de la même manière que pecl http_send_file. Cela signifie la prise en charge des téléchargements avec reprise et de l'accélération. https://github.com/diversen/http-send-file

Dennis
la source
1

La reprise des téléchargements en HTTP se fait via l'en- Rangetête. Si la requête contient un en- Rangetête, et si d'autres indicateurs (par exemple If-Match, If-Unmodified-Since) indiquent que le contenu n'a pas changé depuis le début du téléchargement, vous donnez un code de réponse 206 (au lieu de 200), indiquez la plage d'octets que vous renvoyez dans l'en- Content-Rangetête, puis indiquez cette plage dans le corps de la réponse.

Je ne sais pas comment faire ça en PHP, cependant.

Mike Dimmick
la source
1

Merci Théo! votre méthode ne fonctionnait pas directement pour le streaming divx car j'ai trouvé que le lecteur divx envoyait des plages telles que bytes = 9932800-

mais il m'a montré comment le faire alors merci: D

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

Vous pouvez utiliser le code ci-dessous pour la prise en charge des demandes de plage d'octets dans n'importe quel navigateur

    <?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);
?>
schtroumpf
la source