Download per readfile()

TchiboMann

Erfahrenes Mitglied
Hallo Kollegen ;)

Heut hab ich mal ein (kleines :D) Problem. Bin momentan dabei einen Downloader zu schreiben, der die echte URL der Downloaddatei versteckt.

Ich hab es erst per readfile(); versucht, kleinere Dateien gehen ohne Probleme. Ich erwarte aber eine Dateigröße von ca 1GB, habe daher mal versucht mit dem Downloader ein 700MB großes File herunterzuladen. Tja leider klappr das gar nicht! Der Browser fängt an zu ziehen und beendet den Download 2-5KB später.

Das Ganze läuft auf meinem Linux-Server (2GB RAM). Ich hab den Downloader auf meinem Heimrechner ausprobiert - hier funktionieren 500MB lockerflockig ohne Probleme (Windows Vista 3,25GB RAM).

Nu hab ich etwas recherchiert und hab herausgefunden, dass das wohl am PHP memory_limit liegt, das bei mir auf 32M gesetzt ist. Guuuuut, ich könnts natürlich auf 1.5G erhöhen, allerdings wird dann nur jeweils einer, vielleich zwei Leute die Datei downloaden. Ist natürlich etwas, nunja, bekloppt, weil ich einen recht großen Andrang erwarte :D

Nun hatte ich dann auch folgenden Code gefunden, der eigentlich abhilfe schaffen soll:
PHP:
function readfile_chunked($filename) {
  $chunksize = 1*(1024*1024); // how many bytes per chunk
  $buffer = '';
  $handle = fopen($filename, 'rb');
  if ($handle === false) {
    return false;
  }

  while (!feof($handle)) {
    $buffer = fread($handle, $chunksize);
    print $buffer;

  }
  return fclose($handle);
}

Problem: Es funktioniert nicht :D nach 30MB wird der Download gestoppt... Ich denke mal, dass es auch hier am PHP memory_limit liegt, blos versteh ich da nicht warum, denn nach dem print wird die Variable doch wieder überschrieben, also dürften da doch nur 1024x1024 bytes vom memory genutzt werden? unset($buffer); direkt hinter print $buffer; bringt garnix, weil da wieder das Problem mit dem sofortigen Downloadstopp auftritt...

Bin da im Moment etwas überfragt und am Ende meines Wissens, mir fällt da grad nichts zu ein...

Hat jemand von Euch das Problem schonmal umgehen können oder eine andere, effiziente Art gefunden die wahre URL des Downloads vor allen zu verbergen?


// code den ich nutze, nur der wichtigste Teil davon:
PHP:
<?php
        # Sessionmanagement und alles für diesen Thread unwichtige rausgenommen
	$file = "demo.exe";
	$dlpath = "6kmdkjjsdfi98788nbsdfjn7/";
	$mime = array(
			'exe'	=> 'application/octet-stream',
			'rar'	=> 'application/x-rar-compressed',
			'zip'	=> 'application/zip',
		);

	$extget = explode(".", $file);
	$extension = $extget[count($extget)-1];
	$sendmime = $mime[$extension];
	header ("HTTP/1.1 200 OK");
	header("Content-type: $sendmime");
	header("Content-Length: ".filesize($dlpath.$file)); // Dateigröße
	header("Content-Disposition: attachment; filename=\"$file\"");
	readfile_chunked($dlpath.$file); 
?>
 
Zuletzt bearbeitet:
Weiss da keiner was?

Interessiert mich ungemein! Ich hab jetzt zwar etwas mit .htaccess und mod_rewrite hinbekommen, aber problem ist, dass refererlose Browser ausgeschlossen werden. Nun gut, ich selbst kann drauf verzichten, weil ich es für *räusper* halte den Referer zu verstecken, aber leider killen ja auch Adblocker (dessen Einsatz ich auch bescheiden finde...) den referer raus... Also werden auch "ehrliche" Surfer vom Download ausgeschlossen...

Von daher: Gibt es eine andere sinnvolle methode readfile() oder ähnliches zu nutzen, OHNE dass das PHP memory-limit genutzt wird?
 
Hast du eventuell schon mal versucht, zur Scriptlaufzeit die maximale Größe zu setzen? Das hätte den Vorteil, dass es nur für dieses eine Script gelten würde:

PHP:
ini_set('memory_limit', '-1');

Muss ehrlich gestehen, dass bei mir bisher - trotz Memory Limit & Execution Timout, etc - kein solches Problem aufgetreten ist.

Funktioniert das hier ebenfalls nicht?
PHP:
$fh = fopen($path, 'rb');
fpassthru($fh);
Wäre eigentlich eine ineffiziente Version von readfile, aber vielleicht wirkt es auch als eine Art Workaround...
 
Also mit dem Memory-Limit hat das sicher nichts zu tun.
Wenn ich <?php readfile( "2.5gb-datei.iso" ); ?>
ausführe, dann hab ich fast 50mb/s und das bricht nicht ab.
(Unter Linux)
 
Was du machen könntest währe:

1.) mit einem normlaem fopen die Datei Sequentiell auszulesen und an den Browser gleich zu schicken. So bläht sich das ganze wohl nicht so auf.

2.) Es garnicht per readfile zu machen sondern mit temporären .htaccess zugriffen die beim verwenden wieder entfernt werden. Oder seine IP bis zum ersten zugriff Erlauben zB. Bringt halt teilweise andere Unschönheiten mit sich.

Generell denke ich nicht das php dafür gedacht ist gigabyte weise daten durchzuschleusen.
 
Probier mal folgende Funktion:
PHP:
function readfile_chunked( $filename, $chunksize=1024 )
{
	if( ($handle = fopen($filename, 'rb')) === false ) {
		return false;
	}
	$old = ini_set('implicit_flush', true);
	while( !feof($handle) ) {
		echo fread($handle, $chunksize);
	}
	ini_set('implicit_flush', $old);
	return fclose($handle);
}
 
sorry, konnte 2 Tage hier nich reinschauen :(

So, mal von unten hochgearbeitet :D
@Gumbo

Nette idee, wozu iss dieses "implicid_flush"? hatte leider nicht funktioniert, gab weder fehler noch sonst ne wirkung...

@Michael Engel
Funktionierte ja leider NICHT mit dem sequentiellem Ausgeben, was dieses readfile_chunked(); macht, wenn ich mich nicht irre...

@maeTimmae
Du bist mein Held^^ :D deine erste Lösung funktionierte! Ich hab nie -1 ausprobiert, sondern hochgesetzt auf 1000, was eigentlich für den Download gereicht hätte. tja, so kanns gehen, scheint wohl doch an dem memory_limit gelegen zu haben, denn seitdem ich das nutze funktionierts einwandfrei. Viel lustiger find ich noch, dass ich so tatsächlich mit 700kb/s ziehen kann, nutze ich den direkten download ziehe ich nur mit 450-500kb/s :D

Stellt sich einzig nur noch die Frage: Was passiert, wenn 10 leute gleichzeitig auf das File zugreifen?

@All:
Vielen Dank für eure Hilfe 8)


// EDIT
HAH!

Ich habs rausgefunden, wie man richtig buffert^^ Vielen Dank an euch für die tollen tipps! mit ein wenig Recherche bekommt mans dann auch hin :D

PHP:
// Fuktion readfile_chunked();
function readfile_chunked($filename) {
  $chunksize = 1*(1024*1024); // how many bytes per chunk
  $buffer = '';
  $handle = fopen($filename, 'rb');
  if ($handle === false) {
    return false;
  }
  while (!feof($handle)) {
    $buffer = fread($handle, $chunksize);
    print $buffer;
    ob_flush();
    flush();
  }
  return fclose($handle);
}

PHP:
// Mimetypes definieren, headerdaten ausgeben, readfile_chunked(filepath); ausführen
	$mime = array(
				'doc'	=> 'application/msword',
				'bin'	=> 'application/octet-stream',
				'exe'	=> 'application/octet-stream',
				'rar'	=> 'application/x-rar-compressed',
				'so'	=> 'application/octet-stream',
				'pdf'	=> 'application/pdf',
				'swf'	=> 'application/x-shockwave-flash',
				'tar'	=> 'application/x-tar',
				'zip'	=> 'application/zip',
				'mpga'	=> 'audio/mpeg',
				'mp2'	=> 'audio/mpeg',
				'mp3'	=> 'audio/mpeg',
				'aif'	=> 'audio/x-aiff',
				'aiff'	=> 'audio/x-aiff',
				'aifc'	=> 'audio/x-aiff',
				'm3u'	=> 'audio/x-mpegurl',
				'ram'	=> 'audio/x-pn-realaudio',
				'rm'	=> 'audio/x-pn-realaudio',
				'rpm'	=> 'audio/x-pn-realaudio-plugin',
				'ra'	=> 'audio/x-realaudio',
				'wav'	=> 'audio/x-wav',
				'bmp'	=> 'image/bmp',
				'gif'	=> 'image/gif',
				'jpeg'	=> 'image/jpeg',
				'jpg'	=> 'image/jpeg',
				'jpe'	=> 'image/jpeg',
				'png'	=> 'image/png',
				'tiff'	=> 'image/tiff',
				'tif'	=> 'image/tiff',
				'mpeg'	=> 'video/mpeg',
				'mpg'	=> 'video/mpeg',
				'mpe'	=> 'video/mpeg',
				'qt'	=> 'video/quicktime',
				'mov'	=> 'video/quicktime',
				'avi'	=> 'video/x-msvideo'
			);
		//ini_set('memory_limit', '-1');
	        $extget = explode(".", $file);
	        $extension = $extget[count($extget)-1];
	        $sendmime = $mime[$extension];
	        header ("HTTP/1.1 200 OK");
	        header("Content-type: $sendmime");
	        header("Content-Length: ".filesize($dlpath.$file)); // Dateigröße
	        header("Content-Disposition: attachment; filename=\"$file\"");
	        readfile_chunked($dlpath.$file);

Bei readfile_chunked(); ist folgendes wichtig:
PHP:
  while (!feof($handle)) {
    $buffer = fread($handle, $chunksize);
    print $buffer;
    ob_flush();
    flush();
  }

Hinter print $buffer; ein ob_flush(); und dann ein flush(); setzen. Jetzt funktioniert der downloader bei mir einwandfrei ohne RAM und CPU zu belasten und ohne das memory_limit anfassen zu müssen, einzig die max_execution_time muss man glaub ich höher setzen, das hab ich bei mir allerdings eh schon sehr hoch eingestellt...

Mensch, geil Jungs! Vielen Dank!
 
Zuletzt bearbeitet:
einzig die max_execution_time muss man glaub ich höher setzen

Danke für die Chunk-Lösung. Habe die zur Sicherheit gleich mal in eines meiner Scripte eingebaut ^^
Für die maximale Scriptlaufzeit habe ich folgenden Ansatz gewählt:

PHP:
// setze Timeout-Wert
$timeout = ini_get('max_execution_time');

while (!@feof($fh)) {
    // setze Timeout-Interval neu
    set_time_limit($timeout);
    $buffer = fread($fh, $chunksize);
    echo $buffer;
    ob_flush();
    flush();
}

Einzig und allein Apache oder ein Modul könnte noch dazwischenfunken (Standard-Timeout ist 300 Sekunden) - Sollte aber nicht, da ständig Daten übertragen werden. Ansonsten könnte die Config dahingehend angepasst werden.
 
yo, gerne :D hab das gestern mer durch zufall gefunden, aufgrund deines Tipps mit dem fpassthru();.

Also, was das set_time_limit angeht, kann man das tatsächlich in einer while-Schleife ändern? ich mein, das wär ja eigentlich schon recht übel wenn das geht, so kannste ja ein Script praktisch bis in alle Ewigkeit laufen lassen. Wenns geht, wär das geil für ein Statistikmodul... Besuchen die User eine HP werden sämtliche Daten ohne konkrete auswertung schlicht gesammelt und erst dann ausgewertet, wenn der Admin den Adminbereich besucht, und das ebend in dieser Schleife, da wärs ja vollkommen egal, wie viel ausgewertet werden müsste...

Auf jeden ein interessanter Ansatz, danke dir!
 
Wollte einfach nur nochmal nachschlagen, ob es Sinn macht,
PHP:
ini_set('max_execution_time', '0')
zu setzen und bin dabei auf [phpf]set_time_limit[/phpf] gestoßen. Die Doku besagt
Wenn set_time_limit() aufgerufen wird, dann startet der Zähler neu. Das heisst, wenn die Standardeinstellung 30 Sekunden beträgt und nach 25 Sekunden durch ein Script ein Aufruf wie z.B. set_time_limit(20) erfolgt, darf das Script insgesamt 45 Sekunden laufen, bevor eine Fehlermeldung ausgegeben wird.
(Funktioniert jedoch nur bei SafeMode = 'off')

Um ein Totlaufen bei schlechter Konnektivität oder was auch immer zu vermeiden, wollte ich nicht den Weg über set_time_limit(0) gehen - Wäre aber noch ne Möglichkeit. Alternativ wäre natürlich auch ein on-client-abort-exit-script möglich, würde aber eventuell mehr Last zur Laufzeit erzeugen, da der Zustand in einem engeren Intervall geprüft wird.

Interessant find ich übrigens die Verwendung von [phpf]set_time_limit[/phpf] und [phpf]sleep[/phpf] unter Unix-Systemen (wäre übrigens auch was für dein Logging-Tool zur kontrollierten Ausführung): Die Ruhezeit zählt nicht zur Exec-Zeit und eignet sich somit ziemlich gut zur kontrollierten Cron-Emulierung. Geht zwar sicherlich eleganter und ist auf Linux über Cronjobs generell simpler, aber ist dennoch mal ne Spielerei wert ^^

Interessant für emulierte Cronjobs in einem Intervall x wäre auch Folgendes:
PHP:
function shutdown()
{
    // Script erneut aufrufen über http, refresh oder sonstige Methode
}

register_shutdown_function('shutdown');

while (true) {
    $time = microtime(true); // jetzt
    $interval = 120; // 2 Minuten
    $timeout = 30; // maximal erwarteter Schleifenlaufzeitswert, ansonsten Script abbrechen
    set_time_limit($timeout);

    // tue dies und das

    time_sleep_until($time + $interval);
}
 
Zurück