Created
January 3, 2012 16:02
-
-
Save mindplay-dk/1555496 to your computer and use it in GitHub Desktop.
File Downloader for Yii (PHP)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* @author Rasmus Schultz | |
* @link http://mindplay.dk | |
* @copyright Copyright © 2010 Rasmus Schultz | |
* @license http://www.gnu.org/licenses/lgpl-3.0.txt | |
*/ | |
/** | |
* This class allows multi-threaded file downloads, and regular file downloads. | |
* | |
* You can use this class when you need to control which users are allowed to | |
* download a file from a protected area of the local filesystem. | |
* | |
* Note that downloading files in this way does result in some memory and processor | |
* overhead - you should not use this class sporadically for all downloads in your | |
* application, only when a regular download is not possible for some reason (usually | |
* because the file in question must not be made available to the general public). | |
* | |
* Avoid using this class when you need to log access to a public file - you're | |
* probably better off using a pre-download logging action, which then redirects to | |
* the actual file download. | |
*/ | |
class GDownload { | |
/** | |
* Buffer size (memory requirement per download / user / thread) | |
*/ | |
const BUFFER_SIZE = 32768; # = 1024*32 | |
/** | |
* Time limit (time allowed to send one buffer) | |
*/ | |
const TIME_LIMIT = 30; | |
/** | |
* Sends a file to the client. | |
* | |
* This is a blocking method, which means that the method-call will not return until | |
* the client has completed the download of the file (or segment), or until the client | |
* is disconnected and the connection/download is aborted. | |
* | |
* Check the return value of this method to see if the download was successful - this | |
* if useful, for example, if you need to count and limit the number of times a user can | |
* download a file; you should increase your counters only if the call returns TRUE. | |
* | |
* It is important that your action produces <strong>no other output</strong> before | |
* or after calling this method, as this will corrupt the downloaded binary file. | |
* | |
* Output buffers will be cleared and disabled, and any active CWebLogRoute instances | |
* (which might otherwise output logging information at the end of the request) will | |
* be detected and disabled. | |
* | |
* If your application might produce any other output after your action completes, you | |
* should suppress this by using the exit statement at the end of your action. | |
* | |
* This method throws a CException if the specified path does not point to a valid file. | |
* | |
* This method throws a CHttpException (416) if the requested range is invalid. | |
* | |
* @param string full path to a file on the local filesystem being sent to the client. | |
* @param string optional, alternative filename as the client will see it (defaults to the local filename specified in $path) | |
* @return boolean true if the download succeeded, false if the connection was aborted prematurely. | |
*/ | |
public static function send($path, $name=null) | |
{ | |
// turn off output buffering | |
while (ob_get_level()) | |
ob_end_clean(); | |
// disable any CWebLogRoutes to prevent them from outputting at the end of the request | |
foreach (Yii::app()->log->routes as $route) | |
if ($route instanceof CWebLogRoute) | |
$route->enabled = false; | |
// obtain headers: | |
$envs = ''; | |
foreach ($_ENV as $item => $value) | |
if (substr($item, 0, 5) == 'HTTP_') | |
$envs .= $item.' => '.$value."\n"; | |
if (function_exists('apache_request_headers')) { | |
$headers = apache_request_headers(); | |
foreach ($headers as $header => $value) { | |
$envs .= "apache: $header = $value\n"; | |
} | |
} | |
// obtain filename, if needed: | |
if (is_null($name)) | |
$name = basename($path); | |
// verify path and connection status: | |
if (!is_file($path) || !is_readable($path) || connection_status()!=0) | |
throw new CException('GDownload::send() : unable to access local file "'.$path.'"'); | |
// obtain filesize: | |
$size = filesize($path); | |
// configure download range for multi-threaded / resumed downloads: | |
if (isset($_ENV['HTTP_RANGE'])) | |
{ | |
list($a, $range) = explode("=", $_ENV['HTTP_RANGE']); | |
} | |
else if (function_exists('apache_request_headers')) | |
{ | |
$headers = apache_request_headers(); | |
if (isset($headers['Range'])) { | |
list($a, $range) = explode("=", $headers['Range']); | |
} else { | |
$range = false; | |
} | |
} | |
else | |
{ | |
$range = false; | |
} | |
// produce required headers for partial downloads: | |
if ($range) | |
{ | |
header('HTTP/1.1 206 Partial content'); | |
list($begin, $end) = explode("-", $range); | |
if ($begin == '') | |
{ | |
$begin = $size-$end; | |
$end = $size-1; | |
} | |
else if ($end == '') | |
{ | |
$end = $size-1; | |
} | |
$header = 'Content-Range: bytes '.$begin.'-'.$end.'/'.($size); | |
$size = $end-$begin+1; | |
} | |
else | |
{ | |
$header = false; | |
$begin = 0; | |
$end = $size-1; | |
} | |
// check range: | |
if (($begin > $size-1) || ($end > $size-1) || ($begin > $end)) | |
throw new CHttpException(416,'Requested range not satisfiable'); | |
// suppress client-side caching: | |
header("Cache-Control: no-store, no-cache, must-revalidate"); | |
header("Cache-Control: post-check=0, pre-check=0", false); | |
header("Pragma: no-cache"); | |
header("Expires: ".gmdate("D, d M Y H:i:s", mktime(date("H")+2, date("i"), date("s"), date("m"), date("d"), date("Y")))." GMT"); | |
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT"); | |
// send a generic content-type: | |
header("Content-Type: application/octet-stream"); | |
// send content-range header, if present: | |
if ($header) header($header); | |
// send content-length header: | |
header("Content-Length: ".$size); | |
// send content-disposition, with special handling for IE: | |
if (strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') !== FALSE) | |
{ | |
header("Content-Disposition: inline; filename=".str_replace(' ', '%20', $name)); | |
} | |
else | |
{ | |
header("Content-Disposition: inline; filename=\"$name\""); | |
} | |
// set encoding: | |
header("Content-Transfer-Encoding: binary\n"); | |
// stream out the binary data: | |
if ($file = fopen($path, 'rb')) | |
{ | |
fseek($file, $begin); | |
$sent = 0; | |
while ($sent < $size) | |
{ | |
set_time_limit(self::TIME_LIMIT); | |
$bytes = $end - ftell($file) + 1; | |
if ($bytes > self::BUFFER_SIZE) | |
$bytes = self::BUFFER_SIZE; | |
echo fread($file, $bytes); | |
$sent += $bytes; | |
flush(); | |
if (connection_aborted()) | |
break; | |
} | |
fclose($file); | |
} | |
// check connection status and return: | |
$status = (connection_status()==0) and !connection_aborted(); | |
return $status; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment