Archive for the ‘PHP Speed Limit’ tag
Dateidownload via PHP mit Speedlimit und Resume
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!