Wiederaufnahme der Downloads bei Verwendung von PHP zum Senden der Datei?

104

Wir verwenden ein PHP-Skript zum Tunneln von Dateidownloads, da wir den absoluten Pfad der herunterladbaren Datei nicht offenlegen möchten:

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

Leider haben wir festgestellt, dass Downloads, die über dieses Skript gesendet wurden, vom Endbenutzer nicht fortgesetzt werden können.

Gibt es eine Möglichkeit, wiederaufnehmbare Downloads mit einer solchen PHP-basierten Lösung zu unterstützen?

Mark Amery
quelle

Antworten:

102

Das erste, was Sie tun müssen, ist, den Accept-Ranges: bytesHeader in allen Antworten zu senden , um dem Client mitzuteilen, dass Sie Teilinhalte unterstützen. Dann wird , wenn Anfrage mit einem Range: bytes=x-y Header empfangen wird (mit xund für yZahlen) analysieren Sie den Bereich der Client anfordert, öffnen Sie die Datei wie gewohnt, seek xBytes voraus und die nächsten senden y- xBytes. Stellen Sie auch die Antwort auf ein HTTP/1.0 206 Partial Content.

Ohne etwas getestet zu haben, könnte dies mehr oder weniger funktionieren:

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

Ich habe vielleicht etwas Offensichtliches übersehen und einige potenzielle Fehlerquellen definitiv ignoriert, aber es sollte ein Anfang sein.

Es gibt hier eine Beschreibung des Teilinhalts und ich habe einige Informationen zum Teilinhalt auf der Dokumentationsseite für fread gefunden .

Das Ö
quelle
3
Kleiner Fehler, Ihr regulärer Ausdruck sollte lauten: preg_match ('/ bytes = (\ d +) - (\ d +)? /', $ _SERVER ['HTTP_RANGE'], $
match
1
Du hast recht und ich habe es geändert. Allerdings ist es sowieso zu simpel, gemäß den Angaben kann man "bytes = xy", "bytes = -x", "bytes = x-", "bytes = xy, ab" usw. machen, so dass der Fehler in der Die vorherige Version war der fehlende Schrägstrich, nicht das Fehlen eines Fragezeichens.
Theo
7
Sehr hilfreich, aber ich musste zwei kleinere Änderungen vornehmen, damit es funktioniert: 1. Wenn der Client den Endpunkt nicht im Bereich sendet (da er implizit ist), $lengthist er negativ. $length = (($matches[2]) ? intval($matches[2]) : $filesize) - $offset;behebt das. 2. Content-Rangebehandelt das erste Byte als Byte 0, also das letzte Byte $filesize - 1. Deshalb muss es sein ($offset + $length - 1).
Dennis
1
Oben funktioniert nicht für große Downloads, es wird ein "Schwerwiegender PHP-Fehler: Zulässige Speichergröße von XXXX Bytes erschöpft (versucht, XXX Bytes zuzuweisen) in" angezeigt. In meinem Fall waren 100 MB zu groß. Grundsätzlich speichern Sie alle Dateien in einer Variablen und spucken sie aus.
sarah.ferguson
1
Sie können das Problem mit großen Dateien lösen, indem Sie es in Blöcken anstatt auf einmal auslesen.
Dynamichael
72

EDIT 2017/01 - Ich habe eine Bibliothek geschrieben, um dies in PHP> = 7.0 https://github.com/DaveRandom/Resume zu tun

EDIT 2016/02 - Code einer Reihe von modularen Werkzeugen ein Beispiel Nutzung, sondern als eine monolithische Funktion komplett neu geschrieben. Die in den Kommentaren unten erwähnten Korrekturen wurden übernommen.


Eine getestete, funktionierende Lösung (die stark auf der obigen Antwort von Theo basiert), die sich mit wiederaufnehmbaren Downloads in einigen eigenständigen Tools befasst. Dieser Code erfordert PHP 5.4 oder höher.

Diese Lösung kann immer noch nur einen Bereich pro Anfrage bewältigen, aber unter allen Umständen mit einem Standardbrowser, den ich mir vorstellen kann, sollte dies kein Problem verursachen.

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

Anwendungsbeispiel:

<?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
quelle
Ziemlich schöner Code hier. Ich habe einen Fehler in der Zeile gefunden, in der $ length festgelegt ist. Sollte sein: $ length = $ end - $ start + 1;
Bobwienholt
Wie werde ich das Herunterladen
unterbrechen
3
Sollte die Inhaltslänge auf die tatsächliche Dateigröße oder nur auf die Anzahl der gesendeten Teilbytes eingestellt werden? Auf dieser Seite sieht es so aus, als ob es sich um Teilbytes handeln sollte, aber das ist im obigen Beispielcode nicht der Fall. w3.org/Protocols/rfc2616/rfc2616-sec14.html
Willus
3
Ein weiterer kleiner Tippfehler: $start = $end - intval($range[0]);sollte seinrange[1]
BurninLeo
1
@ sarah.ferguson Code komplett neu geschrieben und aktualisiert, siehe oben.
Dave Random
16

Das funktioniert 100% super check it Ich benutze es und keine Probleme mehr.

        /* 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
quelle
1
Ich habe upvoted, weil das Tempolimit wirklich nützlich ist, aber eine MD5-Überprüfung einer wieder aufgenommenen Datei (Firefox) ergab eine Nichtübereinstimmung. Der str_replace für $ range ist falsch, sollte eine weitere Explosion sein, das Ergebnis numerisch gemacht und ein Strich zum Content-Range-Header hinzugefügt werden.
WhoIsRich
Wie kann ich es für das Herunterladen von Remote-Dateien anpassen?
Siyamak Shahpasand
1
Sie wollten 'Content-Type: $ contentType' doppelt zitieren.
Matt
set_time_limit (0); ist meiner Meinung nach nicht wirklich angemessen. Eine vernünftigere Grenze von 24 Stunden vielleicht?
zweimal am
Vielen Dank, dass Sie meine Tippfehler überprüft haben :)!
user1524615
15

Ja. Support byteranges. Siehe RFC 2616, Abschnitt 14.35 .

Dies bedeutet im Grunde, dass Sie den RangeHeader lesen und die Datei ab dem angegebenen Offset bereitstellen sollten.

Dies bedeutet, dass Sie readfile () nicht verwenden können, da dies die gesamte Datei bedient. Verwenden Sie stattdessen zuerst fopen () , dann fseek () an der richtigen Position und dann fpassthru () , um die Datei bereitzustellen .

Sietse
quelle
4
fpassthru ist keine gute Idee, wenn die Datei mehrere Megabyte groß ist und Ihnen möglicherweise der Speicher ausgeht. Einfach fread () und print () in Stücken.
Willem
3
fpassthru funktioniert hier hervorragend mit Hunderten von Megabyte. echo file_get_contents(...)hat nicht funktioniert (OOM). Ich denke also nicht, dass das ein Problem ist. PHP 5.3.
Janus Troelsen
1
@ JanusTroelsen Nein, das ist es nicht. Es hängt alles von der Konfiguration Ihres Servers ab. Wenn Sie einen starken Server mit viel Speicher für PHP haben, funktioniert dies möglicherweise einwandfrei für Sie. Bei "schwachen" Konfigurationen (wörtlich: gemeinsam genutzte Hostings) schlägt die Verwendung fpassthrusogar bei 50-MB-Dateien fehl. Sie sollten es definitiv nicht verwenden, wenn Sie große Dateien mit schwacher Serverkonfiguration bereitstellen. Wie @Wimmer richtig hervorhebt, ist fread+ printalles, was Sie in diesem Fall benötigen.
Trejder
2
@trejder: Siehe Hinweis zu readfile () : readfile () zeigt selbst beim Senden großer Dateien keine Speicherprobleme an. Wenn ein Speicherfehler auftritt, stellen Sie sicher, dass die Ausgabepufferung mit ob_get_level () deaktiviert ist.
Janus Troelsen
1
@trejder das Problem ist, dass Sie Ihre Ausgabepufferung nicht richtig konfiguriert haben. Es macht das Chunking automatisch, wenn Sie es anweisen : php.net/manual/en/… zB output_buffering = 4096 (und wenn Ihr Framework dies nicht zulässt, saugt Ihr Framework)
ZJR
11

Eine wirklich gute Möglichkeit, dies zu lösen, ohne Ihren eigenen PHP-Code "rollen" zu müssen, ist die Verwendung des Apache-Moduls mod_xsendfile. Dann setzen Sie in PHP einfach die entsprechenden Header. Apache darf sein Ding machen.

header("X-Sendfile: /path/to/file");
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; file=\"filename\"");
Jonathan Hawkes
quelle
2
Was ist, wenn Sie die Verknüpfung der Datei nach dem Senden aufheben möchten?
Janus Troelsen
1
Wenn Sie die Verknüpfung der Datei nach dem Senden aufheben möchten, benötigen Sie ein spezielles Flag, um dies anzuzeigen (siehe XSendFilePath <absolute path> [AllowFileDelete]( tn123.org/mod_xsendfile/beta )).
Jens A. Koch
9

Wenn Sie bereit sind , ein neues PECL - Modul zu installieren, der einfachste Weg , resumeable Downloads mit PHP zu unterstützen , ist durch http_send_file(), wie dies

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

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

Wir verwenden es, um in der Datenbank gespeicherte Inhalte bereitzustellen, und es funktioniert wie ein Zauber!

Justin T.
quelle
3
Klappt wunderbar. Achten Sie jedoch darauf, dass die Ausgabepufferung (ob_start usw.) nicht aktiviert ist. Insbesondere beim Senden großer Dateien wird dadurch der gesamte angeforderte Bereich gepuffert.
Pieter van Ginkel
Wann wurde dies zu PHP hinzugefügt? Warst du schon immer dort?
Thomthom
1
Das ist Pecl, nicht PHP. Ich habe diese Funktion nicht.
Geo
4

Die Top-Antwort hat verschiedene Fehler.

  1. Der Hauptfehler: Der Range-Header wird nicht richtig behandelt. bytes a-bbedeuten sollte [a, b]statt [a, b)und bytes a-wird nicht behandelt.
  2. Der kleine Fehler: Es wird kein Puffer verwendet, um die Ausgabe zu verarbeiten. Dies kann zu viel Speicher verbrauchen und bei großen Dateien zu geringer Geschwindigkeit führen.

Hier ist mein geänderter Code:

// 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);
Mein Gott
quelle
Warum braucht das ini_set('memory_limit', '-1');?
Mikko Rantalainen
1
@ MikkoRantalainen habe ich vergessen. Sie könnten versuchen, es zu entfernen und zu sehen, was passiert.
Mygod
1
Leider wird ein Fehler in der $ end-Zuweisung ausgegeben, falls $ match [2] nicht gesetzt ist (z. B. mit einer "Range = 0-" -Anforderung). Ich habe dies stattdessen verwendet:if(!isset($matches[2])) { $end=$fs-1; } else { $end = intval($matches[2]); }
Skynet
3

Ja, dafür können Sie den Range-Header verwenden. Sie müssen dem Client 3 weitere Header für einen vollständigen Download geben:

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

Für einen unterbrochenen Download müssen Sie den Header der Bereichsanforderung überprüfen, indem Sie:

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

Und vergessen Sie in diesem Fall nicht, den Inhalt mit dem Statuscode 206 bereitzustellen:

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

Sie erhalten die Variablen $ start und $ to aus dem Anforderungsheader und verwenden fseek (), um nach der richtigen Position in der Datei zu suchen.

Zsolt Szeberenyi
quelle
2
@ceejayoz: getallheaders () ist eine PHP-Funktion, die Sie erhalten, wenn Sie Apache verwenden uk2.php.net/getallheaders
Tom Haigh
2

Kleine Composer-fähige Klasse, die genauso funktioniert wie pecl http_send_file. Dies bedeutet Unterstützung für wiederaufnehmbare Downloads und Drosselung. https://github.com/diversen/http-send-file

Dennis
quelle
1

Das Fortsetzen von Downloads in HTTP erfolgt über den RangeHeader. Wenn die Anforderung enthält einen RangeHeader, und wenn andere Indikatoren (zB If-Match, If-Unmodified-Since) zeigen an, dass der Inhalt nicht , da der Download geändert hat begonnen, Sie einen 206 - Antwortcode geben (statt 200), geben Sie den Bereich von Bytes Sie Rückkehr Content-RangeGeben Sie dann im Header diesen Bereich im Antworttext an.

Ich weiß allerdings nicht, wie ich das in PHP machen soll.

Mike Dimmick
quelle
1

Danke Theo! Ihre Methode funktionierte nicht direkt für das Streaming von DivX, da ich feststellte, dass der DivX-Player Bereiche wie Bytes = 9932800- sendete.

aber es hat mir gezeigt, wie es geht, danke: D.

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

Sie können den folgenden Code für die Unterstützung von Bytebereichsanforderungen in jedem Browser verwenden

    <?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);
?>
Schlumpf
quelle