Laden Sie vom Laravel-Speicher herunter, ohne die gesamte Datei in den Speicher zu laden
Lesezeit: 6 Minuten
Džuris
Ich verwende Laravel Storage und möchte Benutzern einige Dateien (größer als das Speicherlimit) bereitstellen. Mein Code wurde von einem Beitrag in SO inspiriert und geht so:
Es scheint, dass es versucht, die gesamte Datei in den Speicher zu laden. Ich hatte erwartet, dass die Verwendung von Stream und Passthru dies nicht tun würde … Fehlt etwas in meinem Code? Muss ich irgendwie die Chunk-Größe angeben oder was?
Die Versionen, die ich verwende, sind Laravel 5.1 und PHP 5.6.
Das einzige Szenario, das mir einfällt, wo fpassthru in den Speicher allokiert wird, wenn die Ausgabepufferung verwendet wird. Sie können daher eine Schleife anprobieren fread mit einem echo.
– Bischof
2. Mai 2016 um 15:23 Uhr
Christian
Es scheint, dass die Ausgabepufferung immer noch viel im Speicher aufbaut.
Versuchen Sie, ob zu deaktivieren, bevor Sie fpassthru ausführen:
Es könnte sein, dass mehrere Ausgabepuffer aktiv sind, weshalb das While benötigt wird.
Diese Antwort spricht das eigentliche Problem an, das bei meiner versuchten Implementierung Probleme verursacht hat, daher akzeptiere ich das Kopfgeld und spreche es Ihnen zu. Vielen Dank an alle für die anderen Antworten, die auch wertvolle Informationen sind!
– Džuris
9. Mai 2016 um 12:03 Uhr
Kevin
Anstatt die gesamte Datei auf einmal in den Speicher zu laden, versuchen Sie, fread zu verwenden, um sie Chunk für Chunk zu lesen und zu senden.
<?php
//disable execution time limit when downloading a big file.
set_time_limit(0);
/** @var \League\Flysystem\Filesystem $fs */
$fs = Storage::disk('local')->getDriver();
$fileName="bigfile";
$metaData = $fs->getMetadata($fileName);
$handle = $fs->readStream($fileName);
header('Pragma: public');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Cache-Control: private', false);
header('Content-Transfer-Encoding: binary');
header('Content-Disposition: attachment; filename="' . $metaData['path'] . '";');
header('Content-Type: ' . $metaData['type']);
/*
I've commented the following line out.
Because \League\Flysystem\Filesystem uses int for file size
For file size larger than PHP_INT_MAX (2147483647) bytes
It may return 0, which results in:
Content-Length: 0
and it stops the browser from downloading the file.
Try to figure out a way to get the file size represented by a string.
(e.g. using shell command/3rd party plugin?)
*/
//header('Content-Length: ' . $metaData['size']);
$chunkSize = 1024 * 1024;
while (!feof($handle)) {
$buffer = fread($handle, $chunkSize);
echo $buffer;
ob_flush();
flush();
}
fclose($handle);
exit;
?>
Aktualisieren
Einfacher geht es: Einfach anrufen
if (ob_get_level()) ob_end_clean();
bevor Sie eine Antwort zurücksenden.
Gutschrift an @Christiaan
//disable execution time limit when downloading a big file.
set_time_limit(0);
/** @var \League\Flysystem\Filesystem $fs */
$fs = Storage::disk('local')->getDriver();
$fileName="bigfile";
$metaData = $fs->getMetadata($fileName);
$stream = $fs->readStream($fileName);
if (ob_get_level()) ob_end_clean();
return response()->stream(
function () use ($stream) {
fpassthru($stream);
},
200,
[
'Content-Type' => $metaData['type'],
'Content-disposition' => 'attachment; filename="' . $metaData['path'] . '"',
]);
Genau dafür ist fpasstru da, keine Notwendigkeit, die Dinge zu komplizieren.
– Christian
8. Mai 2016 um 5:38 Uhr
Ich glaube nicht. Ich habe ein Experiment gemacht, fpassthru führte zu genau dem gleichen Fehler. Mit dieser Methode kann ich die Datei herunterladen.
– Kevin
8. Mai 2016 um 6:42 Uhr
@Christiaan Ich habe den Code in meiner Antwort aktualisiert und Sie können dieses Experiment auf Ihrem Computer durchführen. (Generieren Sie einfach eine 20 GB große Datei)
– Kevin
8. Mai 2016 um 6:44 Uhr
Haben Sie mit fpasstru sichergestellt, dass Sie die Ausgabepufferung deaktiviert haben? Denn das ist es, was Ihr Beispiel stirbt, indem es jedes Mal Flush ruft.
– Christian
8. Mai 2016 um 7:40 Uhr
@Christiaan Du hast recht. Danke für den Hinweis. Ja, es ist eigentlich ein sehr einfaches Problem, wie konnte ich den Punkt verpasst haben. Ruf einfach an if (ob_get_level()) ob_end_clean(); bevor Sie eine Antwort zurücksenden. Ich werde die Antwort aktualisieren und Ihnen Anerkennung zollen
– Kevin
8. Mai 2016 um 8:00 Uhr
X-Send-File.
X-Send-File ist eine interne Direktive, die Varianten für Apache, nginx und lighthttpd hat. Es erlaubt Ihnen komplett überspringen Verteilen einer Datei über PHP und ist eine Anweisung, die dem Webserver mitteilt, was er als Antwort anstelle der eigentlichen Antwort von FastCGI senden soll.
Dies wurde auf stark frequentierten Websites getestet. Tun nicht Puffern Sie Medien über einen PHP-Daemon, es sei denn, Ihre Website hat so gut wie keinen Traffic oder Sie verbrauchen Ressourcen.
Ich würde das wirklich gerne implementieren, aber ich bin mir nicht sicher über die Sicherheit. Können Sie erklären, ob Sie verwenden X-Send-File das Risiko hinzufügt, dass die Datei nicht autorisierten Clients zugänglich gemacht wird?
– Džuris
3. Mai 2016 um 18:33 Uhr
Sie können damit Controller-Richtlinien verwenden, weshalb ich die Lösung so liebe. Sie sollten sich jedoch bewusst sein, dass nginx und möglicherweise CDNs wie Cloudflare die Datei zwischenspeichern und an jeden verteilen können, der die URL hat.
– Josch
3. Mai 2016 um 18:38 Uhr
Sie könnten versuchen, die StreamedResponse-Komponente direkt anstelle des Laravel-Wrappers dafür zu verwenden. Gestreamte Antwort
Das einzige Szenario, das mir einfällt, wo
fpassthru
in den Speicher allokiert wird, wenn die Ausgabepufferung verwendet wird. Sie können daher eine Schleife anprobierenfread
mit einemecho
.– Bischof
2. Mai 2016 um 15:23 Uhr