Using PHP to serve protected files to specific web users can be very useful for the developer, but the end user usually misses out as functionality such as download resuming is often missed out.
I’ve put together the following function which will serve files to the user with support for download resuming after they’ve been paused or the connection has been dropped. Also, by serving the data in 1MB chunks this function is very low on memory usage.
function dl($file_location, $file_name) {
try {
//check if file exists
if(!file_exists($file_location))
throw new Exception('Could not find file');
//file size
$file_size = filesize($file_location);
//open download file
$open_download_file = fopen($file_location, 'rb');
if(!$open_download_file)
throw new Exception('Could not open file');
//download range
$start_point = 0;
$end_point = $file_size - 1;
//partial download
if(isset($_SERVER['HTTP_RANGE']) && !empty($_SERVER['HTTP_RANGE'])) {
//download range
$http_range = explode('-', substr($_SERVER['HTTP_RANGE'], strlen('bytes=')));
$start_point = $http_range[0];
if($http_range[1] > 0)
$end_point = $http_range[1];
//headers
header('HTTP/1.0 206 Partial Content');
header('Status: 206 Partial Content');
header('Accept-Ranges: bytes');
header('Content-Range: bytes '.$_SERVER['HTTP_RANGE'].'/'.$file_size);
header('Content-Length: '.($end_point - $start_point + 1));
}
//full download
else {
//headers
header('Content-Length: '.$file_size);
}
//headers
header('Content-Type: application/force-download');
header('Content-Disposition: attachment; filename="'.$file_name.'"');
header('Last-Modified: '.date('D, d M Y H:i:s \G\M\T', filemtime($file_location)));
//headers for ie6 & ie7
header('Expires: 0');
header('Pragma: public');
header('Cache-Control: must-revalidate');
//jump ahead in file to start
if($start_point > 0)
fseek($open_download_file, $start_point);
//serve data chunk and update download progress log
$download_bytes_position = $start_point;
while(!feof($open_download_file) && $download_bytes_position <= $end_point) {
//mark download as aborted if connection has been closed
if(connection_aborted() || connection_status() != 0)
throw new Exception('Connection aborted');
//calculate next chunk size
$download_data_chunk_size = 1048000;
if($download_bytes_position + $download_data_chunk_size > $end_point + 1)
$download_data_chunk_size = $end_point - $download_bytes_position + 1;
//get data chunk
$download_data_chunk = fread($open_download_file, $download_data_chunk_size);
if(!$download_data_chunk)
throw new Exception('Could not read file');
//output it
print($download_data_chunk);
flush();
//increment download point
$download_bytes_position += $download_data_chunk_size;
}
//close archive file
fclose($open_download_file);
}
catch(Exception $e) {
//any error handling you need to do should go here
return false;
}
return true;
}