Sending Files
First, we will set up our function:
Code: Select all
<?php
/*
send_file( string $file [, int $rate ] )
param $file - Path to the file to send
param $rate - Speed limit of download in kB/s
*/
function send_file($file, $rate = 0) {
// Send the file
}
?>
Code: Select all
// Check if the file exists
if (!is_file($file))
{
die('404 File Not Found');
}
Now let's collect some important info about the file:16
Code: Select all
// Get the filename, extension, and size
$filename = basename($file);
$file_extension = strtolower(substr(strrchr($filename, '.'), 1));
$size = filesize($file);
We should also determine the most accurate MIME type to send based on the extension:
Code: Select all
// Set the mime type based on the extension
switch($file_extension)
{
case 'exe':
$ctype = 'application/octet-stream';
break;
case 'zip':
$ctype = 'application/zip';
break;
case 'mp3':
$ctype = 'audio/mpeg';
break;
case 'mpg':
$ctype = 'video/mpeg';
break;
case 'avi':
$ctype = 'video/x-msvideo';
break;
// Block access to sensitive file types
case 'php':
case 'inc':
exit;
break;
default:
$ctype='application/force-download';
}
Before we send the file, we need to send the appropriate response headers:
Code: Select all
// Begin writing headers
header('Cache-Control: private');
header('Content-Type: ' . $ctype);
header('Content-Disposition: attachment; filename=' . $filename);
header('Content-Transfer-Encoding: binary');
header('Content-Length: ' . $size);
These basically tell the browser that we are sending a file, along with the name, type and size.
Now we need to open the file, send it, then close it:
Code: Select all
// Open the file for reading
$fp = fopen($file, 'rb');
// Set up the size of each piece of data we send
$block_size = 1024;
// Prevent the script from timing out
set_time_limit(0);
// Start sending the file
while(!feof($fp))
{
// Output data
print(fread($fp, $block_size));
flush();
}
// Close the file
fclose($fp);
We send the file in chunks, which will make limiting the speed possible.
Speed Limit
Now let's add the code for the speed limit:
Code: Select all
// Open the file for reading
$fp = fopen($file, 'rb');
// Set up the size of each piece of data we send
$block_size = 1024;
if($rate > 0)
{
// Multiply by rate if specified
$block_size *= $rate;
}
// Prevent the script from timing out
set_time_limit(0);
// Start sending the file
while(!feof($fp))
{
// Output data
print(fread($fp, $block_size));
flush();
if($rate > 0)
{
// Wait one second before next block if rate is specified
sleep(1);
}
}
// Close the file
fclose($fp);
The limit is accomplished by sending the number of kB specified, waiting 1 second, then sending the next piece.
Here is what the code looks like at this point:
Code: Select all
<?php
/*
send_file( string $file [, int $rate ] )
param $file - Path to the file to send
param $rate - Speed limit of download in kB/s
*/
function send_file($file, $rate = 0) {
// Check if the file exists
if (!is_file($file))
{
die('404 File Not Found');
}
// Get the filename, extension, and size
$filename = basename($file);
$file_extension = strtolower(substr(strrchr($filename, '.'), 1));
$size = filesize($file);
// Set the mime type based on the extension
switch($file_extension)
{
case 'exe':
$ctype = 'application/octet-stream';
break;
case 'zip':
$ctype = 'application/zip';
break;
case 'mp3':
$ctype = 'audio/mpeg';
break;
case 'mpg':
$ctype = 'video/mpeg';
break;
case 'avi':
$ctype = 'video/x-msvideo';
break;
// Block access to sensitive file types
case 'php':
case 'inc':
exit;
break;
default:
$ctype='application/force-download';
}
// Begin writing headers
header('Cache-Control: private');
header('Content-Type: ' . $ctype);
header('Content-Disposition: attachment; filename=' . $filename);
header('Content-Transfer-Encoding: binary');
header('Content-Length: ' . $size);
// Open the file for reading
$fp = fopen($file, 'rb');
// Set up the size of each piece of data we send
$block_size = 1024;
if($rate > 0)
{
// Multiply by rate if specified
$block_size *= $rate;
}
// Prevent the script from timing out
set_time_limit(0);
// Start sending the file
while(!feof($fp))
{
// Output data
print(fread($fp, $block_size));
flush();
if($rate > 0)
{
// Wait one second before next block if rate is specified
sleep(1);
}
}
// Close the file
fclose($fp);
}
?>
Now we need to add support for download managers that allow resuming partial downloads.
First we need to tell the client that we accept ranges:
Code: Select all
header('Accept-Ranges: bytes');
We also removed the Content-Length header for now since it might change.
After we open the file, we need to check for the HTTP_RANGE request header and figure out where to start the download:
Code: Select all
// Check if http_range was sent by client
if(isset($_SERVER['HTTP_RANGE']))
{
// If so, calculate the range to use
$seek_range = substr($_SERVER['HTTP_RANGE'], 6);
$range = explode('-', $seek_range);
if($range[0] > 0){
$seek_start = intval($range[0]);
}
if($range[1] > 0){
$seek_end = intval($range[1]);
}
// Seek to the requested position in the file
fseek($fp, $seek_start);
// Set the range response headers
header('HTTP/1.1 206 Partial Content');
header('Content-Length: ' . ($seek_end - $seek_start + 1));
header(sprintf('Content-Range: bytes %d-%d/%d', $seek_start, $seek_end, $size));
}
else
{
// Set default response headers
header('Content-Length: ' . $size);
}
If the range header exists, we parse the value (ex. bytes=100-4564) and seek to the part of the file requested. We also send the appropriate headers, including the new Content-Lenght and Content-Range headers. If a range was not requested, we just send the full Content-Length.
Final Product
Code: Select all
<?php
/*
send_file( string $file [, int $rate ] )
param $file - Path to the file to send
param $rate - Speed limit of download in kB/s
*/
function send_file($file, $rate = 0) {
// Check if the file exists
if (!is_file($file))
{
die('404 File Not Found');
}
// Get the filename, extension, and size
$filename = basename($file);
$file_extension = strtolower(substr(strrchr($filename, '.'), 1));
$size = filesize($file);
// Set the mime type based on the extension
switch($file_extension)
{
case 'exe':
$ctype = 'application/octet-stream';
break;
case 'zip':
$ctype = 'application/zip';
break;
case 'mp3':
$ctype = 'audio/mpeg';
break;
case 'mpg':
$ctype = 'video/mpeg';
break;
case 'avi':
$ctype = 'video/x-msvideo';
break;
// Block access to sensitive file types
case 'php':
case 'inc':
exit;
break;
default:
$ctype='application/force-download';
}
// Begin writing headers
header('Cache-Control: private');
header('Content-Type: ' . $ctype);
header('Content-Disposition: attachment; filename=' . $filename);
header('Content-Transfer-Encoding: binary');
header('Accept-Ranges: bytes');
// Open the file for reading
$fp = fopen($file, 'rb');
// Check if http_range was sent by client
if(isset($_SERVER['HTTP_RANGE']))
{
// If so, calculate the range to use
$seek_range = substr($_SERVER['HTTP_RANGE'], 6);
$range = explode('-', $seek_range);
if($range[0] > 0){
$seek_start = intval($range[0]);
}
if($range[1] > 0){
$seek_end = intval($range[1]);
}
// Seek to the requested position in the file
fseek($fp, $seek_start);
// Set the range response headers
header('HTTP/1.1 206 Partial Content');
header('Content-Length: ' . ($seek_end - $seek_start + 1));
header(sprintf('Content-Range: bytes %d-%d/%d', $seek_start, $seek_end, $size));
}
else
{
// Set default response headers
header('Content-Length: ' . $size);
}
// Set up the size of each piece of data we send
$block_size = 1024;
if($rate > 0)
{
// Multiply by rate if specified
$block_size *= $rate;
}
// Prevent the script from timing out
set_time_limit(0);
// Start sending the file
while(!feof($fp))
{
// Output data
print(fread($fp, $block_size));
flush();
if($rate > 0)
{
// Wait one second before next block if rate is specified
sleep(1);
}
}
// Close the file
fclose($fp);
}
?>