PHPGangsta - Der praktische PHP Blog

PHP Blog von PHPGangsta


Archive for the ‘PHP Speed Limit’ tag

Dateidownload via PHP mit Speedlimit und Resume

with 19 comments

Wenn man statische Dateien zum Download anbieten möchte lässt man das meistens den Webserver erledigen. Dann sind auch häufig Funktionalitäten wie ein Speedlimit oder das Wiederaufnehmen des Downloads (Resume) möglich.

Doch was macht man, wenn etwas mehr Kontrolle nötig ist, beispielsweise eine Authentifizierung in PHP, die Datei erst in Echtzeit berechnet werden muss oder die Datei aus einer Datenbank kommt (böse!). Oder man hat keinen Zugriff auf die Konfiguration des Webservers. Dann kommt man nicht um ein kleines PHP-Script drumherum, muss sich dann aber um die Speedlimit-/Resume Funktionalitäten selbst kümmern. Beispielsweise kann man Premium-Usern mehr Downloadgeschwindigkeit geben als normalen Benutzern, oder pro Benutzer ein tägliches Gesamtlimit festlegen.  Und so schwer ist das garnicht.

Es wird die Extension FileInfo benötigt, seit PHP 5.3.0 Standard, für ältere Versionen kann sie aus der PECL installiert werden. Man kann die entsprechende Stelle auch umgehen und anders lösen falls man FileInfo nicht nutzen möchte/kann.

Es gibt dafür zwar das PEAR-Paket HTTP_Download, aber das ist veraltet (PHP4-Code) und wirft Notices und E_STRICT Meldungen.

Hier mein Script (auf das wesentliche gekürzt, damit es hier in den Blog passt):

<?
class RatedSender
{
	/**
	 * Send a file as download to the browser (maybe limited in speed)
	 *
	 * @param string $filePath
	 * @param int $rate speedlimit in KB/s
	 * @return void
	 */
	public function send($filePath, $rate = 0)
	{
		// Check if file exists
		if (!is_file($filePath)) {
			throw new Exception('File not found.');
		}

		// get more information about the file
		$filename = basename($filePath);
		$size = filesize($filePath);
		$finfo = finfo_open(FILEINFO_MIME);
		$mimetype = finfo_file($finfo, realpath($filePath));
		finfo_close($finfo);

		// Create file handle
		$fp = fopen($filePath, 'rb');

		$seekStart = 0;
		$seekEnd = $size;

		// Check if only a specific part should be sent
		if(isset($_SERVER['HTTP_RANGE'])) {
			// If so, calculate the range to use
			$range = explode('-', substr($_SERVER['HTTP_RANGE'], 6));

			$seekStart = intval($range[0]);
			if ($range[1] > 0) {
				$seekEnd = intval($range[1]);
			}

			// Seek to the start
			fseek($fp, $seekStart);

			// Set headers incl range info
			header('HTTP/1.1 206 Partial Content');
			header(sprintf('Content-Range: bytes %d-%d/%d', $seekStart, $seekEnd, $size));
		}
		else {
			// Set headers for full file
			header('HTTP/1.1 200 OK');
		}

		// Output some headers
		header('Cache-Control: private');
		header('Content-Type: ' . $mimetype);
		header('Content-Disposition: attachment; filename="' . $filename . '"');
		header('Content-Transfer-Encoding: binary');
		header("Content-Description: File Transfer");
		header('Content-Length: ' . ($seekEnd - $seekStart));
		header('Accept-Ranges: bytes');
		header('Last-Modified: ' . gmdate('D, d M Y H:i:s', filemtime($filePath)) . ' GMT');

		$block = 1024;
		// limit download speed
		if($rate > 0) {
			$block *= $rate;
		}

		// disable timeout before download starts
		set_time_limit(0);

		// Send file until end is reached
		while(!feof($fp))
		{
			$timeStart = microtime(true);
			echo fread($fp, $block);
			flush();
			$wait = (microtime(true) - $timeStart) * 1000000;

			// if speedlimit is defined, make sure to only send specified bytes per second
			if($rate > 0) {
				usleep(1000000 - $wait);
			}
		}

		// Close handle
		fclose($fp);
	}
}

try {
	$ratedSender = new RatedSender();
	$ratedSender->send('data.zip', 3);
} catch (Exception $e) {
	header('HTTP/1.1 404 File Not Found');
	die('Sorry, an error occured.');
} ?>

Das Script unterstützt durch die RANGE Angabe sowohl Resume als auch den parallelen Download einer einzelnen Datei mit mehreren Threads, wie es mit vielen Download-Managern möglich ist. In diesem Beispiel wird also jede Verbindung auf 3 KB/s beschränkt. Möchte man dieses Limit nicht pro Verbindung sondern pro User/IP-Adresse setzen muss man das in PHP errechnen.

Viele Downloadscripte sind unsicher da man z.B. via GET-Parameter den Dateinamen angeben kann, dort sollte man sehr aufpassen dass man nur Dateien zum Download anbietet die auch wirklich herunterladbar sein sollen.

Verbesserungsvorschläge sind natürlich willkommen!

Written by Michael Kliewe

März 4th, 2010 at 9:16 am