Leider haben wir festgestellt, dass Downloads, die durch dieses Skript geleitet werden, vom Endbenutzer nicht fortgesetzt werden können.
Gibt es eine Möglichkeit, fortsetzbare Downloads mit einer solchen PHP-basierten Lösung zu unterstützen?
Das Ö
Das erste, was Sie tun müssen, ist, die zu senden Accept-Ranges: bytes Header in allen Antworten, um dem Client mitzuteilen, dass Sie partielle Inhalte unterstützen. Dann auf Wunsch mit a Range: bytes=x-y Header empfangen wird (mit x und y Zahlen sind) analysieren Sie den Bereich, den der Client anfordert, öffnen Sie die Datei wie gewohnt, suchen Sie x Bytes voraus und senden Sie die nächsten y – x Byte. 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) . "https://stackoverflow.com/" . $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 ich habe definitiv einige potenzielle Fehlerquellen ignoriert, aber es sollte ein Anfang sein.
Kleiner Fehler, Ihr regulärer Ausdruck sollte lauten: preg_match(‘/bytes=(\d+)-(\d+)?/’, $_SERVER[‘HTTP_RANGE’]$matches)
– Tiefbrunnen
13. Juli 2009 um 22:46 Uhr
Du hast Recht und ich habe es geändert. Allerdings ist es mir sowieso zu einfach, laut den Spezifikationen können Sie “bytes=xy”, “bytes=-x”, “bytes=x-“, “bytes=xy,ab” usw. tun, also den Fehler in der vorherigen Version war der fehlende Schrägstrich am Ende, nicht das Fehlen eines Fragezeichens.
– Das Ö
16. Juli 2009 um 8:09 Uhr
Sehr hilfreich, aber ich musste zwei kleine Änderungen vornehmen, damit es funktioniert: 1. Wenn der Client den Endpunkt nicht im Bereich sendet (da er implizit ist), $length wird negativ sein. $length = (($matches[2]) ? intval($matches[2]) : $filesize) - $offset; behebt das. 2. Content-Range behandelt das erste Byte als Byte 0also das letzte Byte ist $filesize - 1. Daher muss es sein ($offset + $length - 1).
– Denis
7. März 2012 um 13:48 Uhr
Oben funktioniert es bei großen Downloads nicht, Sie erhalten einen “Schwerwiegenden PHP-Fehler: Zulässige Speichergröße von XXXX Bytes erschöpft (versucht, XXX Bytes zuzuweisen) in “. In meinem Fall waren 100 MB zu groß. Sie speichern im Grunde alle Dateien in einer Variablen und spucken sie aus.
– sarah.ferguson
29. Januar 2016 um 16:56 Uhr
Sie können das Problem mit großen Dateien lösen, indem Sie sie in Stücken statt auf einmal auslesen.
BEARBEITEN 2016/02 – Code komplett umgeschrieben in eine Reihe von modularen Tools und eine beispielhafte Verwendung, anstatt eine monolithische Funktion. In den Kommentaren unten erwähnte Korrekturen wurden eingearbeitet.
Eine getestete, funktionierende Lösung (die stark auf Theos Antwort oben basiert), die sich mit fortsetzbaren Downloads befasst, in einer Reihe einiger eigenständiger Tools. 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) . "https://stackoverflow.com/" . $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);
}
}
Beispielnutzung:
<?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;
Ziemlich netter Code hier. Ich habe einen Fehler in der Zeile gefunden, in der $length gesetzt ist. Sollte lauten: $länge = $ende – $start + 1;
– Bobwienholt
11. Mai 2012 um 1:59 Uhr
Wie kann ich den Download pausieren?
– Prasanth Bendra
18. Februar 2013 um 11:32 Uhr
Sollte Content-Length auf die tatsächliche Dateigröße eingestellt werden oder nur auf die Anzahl der gesendeten Teilbytes? Diese Seite lässt es so aussehen, als ob es sich um die partiellen Bytes handeln sollte, aber das ist im obigen Beispielcode nicht der Fall. w3.org/Protocols/rfc2616/rfc2616-sec14.html
– Willen
21. Dezember 2013 um 15:18 Uhr
Noch ein kleiner Tippfehler: $start = $end - intval($range[0]); sollte sein range[1]
– BurninLeo
3. März 2014 um 14:29 Uhr
@sarah.ferguson Code komplett neu geschrieben und aktualisiert, siehe oben.
Es bedeutet im Grunde, dass Sie die lesen sollten Range -Header und starten Sie die Bereitstellung der Datei ab dem angegebenen Offset.
Das bedeutet, dass Sie readfile() nicht verwenden können, da dies die gesamte Datei bedient. Verwenden Sie stattdessen fopen() Zuerst, dann fseek() in die richtige Position bringen und dann verwenden fpassthru() um die Akte zu bedienen.
fpassthru ist keine gute Idee, wenn die Datei mehrere Megabyte groß ist, da Ihnen möglicherweise der Arbeitsspeicher ausgeht. Einfach fread() und print() in Stücken.
– Willem
12. Oktober 2008 um 22:58 Uhr
fpassthru funktioniert hier hervorragend mit Hunderten von Megabytes. echo file_get_contents(...) hat nicht funktioniert (OOM). Also ich denke nicht, dass das ein Problem ist. PHP5.3.
– Janus Troelsen
16. September 2011 um 12:52 Uhr
@JanusTroelsen Nein, 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 es vielleicht gut für Sie. Auf “schwache” Konfigurationen (wörtlich: Shared Hostings) verwenden fpassthru schlägt sogar bei Dateien mit 50 MB fehl. Sie sollten es definitiv nicht verwenden, wenn Sie große Dateien auf einer schwachen Serverkonfiguration bereitstellen. Wie @Wimmer richtig betont, fread + print ist alles, was Sie in diesem Fall brauchen.
– trejder
25. Februar 2014 um 8:04 Uhr
@trejder: Siehe die Notiz auf readfile(): readfile() wird selbst beim Senden großer Dateien keine Speicherprobleme verursachen. Wenn Sie auf einen Speichermangelfehler stoßen, stellen Sie sicher, dass die Ausgabepufferung mit ob_get_level() ausgeschaltet ist.
– Janus Troelsen
25. Februar 2014 um 9:05 Uhr
@trejder das Problem ist, dass Sie Ihre Ausgabepufferung nicht richtig konfiguriert haben. Es führt das Chunking automatisch durch, wenn Sie es anweisen: php.net/manual/de/… zB output_buffering=4096 (und wenn Ihr Framework dies nicht zulässt, ist Ihr Framework scheiße)
– ZJR
14. Oktober 2015 um 17:00 Uhr
Lebenslehrer
Das funktioniert zu 100% super, überprüfen Sie es, ich benutze es und keine Probleme mehr.
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.
Was ist, wenn Sie die Verknüpfung der Datei nach dem Senden aufheben möchten?
– Janus Troelsen
16. September 2011 um 12:54 Uhr
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
26. Juli 2016 um 11:38 Uhr
Mikko Rantalainen
Wenn Sie bereit sind, ein neues PECL-Modul zu installieren, wird die einfachste Weg, um fortsetzbare Downloads mit PHP zu unterstützen ist durch http_send_file()so was
Wir verwenden es, um in der Datenbank gespeicherte Inhalte bereitzustellen, und es funktioniert wie ein Zauber!
Was ist, wenn Sie die Verknüpfung der Datei nach dem Senden aufheben möchten?
– Janus Troelsen
16. September 2011 um 12:54 Uhr
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
26. Juli 2016 um 11:38 Uhr
Zsolt Szeberenyi
Ja, Sie können dafür den Range-Header verwenden. Für einen vollständigen Download müssen Sie dem Client 3 weitere Header geben: