Hi!
This is a very simple download script that I wrote. The script was meant to be used for a specific file (hence the filename setting withing the script itself), but with simple alterations one can modify this for whatever purpose. The reason one can specify input filename and output filename separately is to protect original filename so it won't be accessed directly.
It also supports partial downloads, ie. HTTP ranges, and logs transferred bytes into MySQL tables for eventual bandwidth monitoring.
<?php
// Setup inputfile, output filename and mimetype
$strDatafile= "input_file_name";
$strOutfile= "output_file_name";
$strMimetype= "application/octet-stream";
// Get filesize and set default file offset to 0
$iFilesize= filesize ($strDatafile);
$range_from=0;
// Check for available bandwidth and route if none available
if (!checkBandwidth()) {
header ("Location: no_enough_bandwith.php");
exit;
}
// try opening input file
if ($fd = fopen($strDatafile, "rb")) {
// Setup headers
header ("Cache-Control:");
header ("Cache-Control: public");
header ("Pragma: public");
header ("Content-Description: File Transfer");
header ("Content-Type: $strMimetype");
header ("Content-Disposition: attachment; filename= \"$strOutfile\"");
header ("Content-Transfer-Encoding: binary");
// Check if particular range has been requested
if(isset($_SERVER['HTTP_RANGE'])) {
list($a, $range)= explode ("=",$_SERVER['HTTP_RANGE']);
list($range_from, $range_to)= explode("-", trim($range));
// Check the format of requested range. Sometimes it is
// startbyte-
// and sometimes it is full range:
// startbyte-endbyte
if (is_numeric($range_to)) {
$txt_range= "".$range_from."-".$range_to;
$content_length= (int) ($range_to) - (int) ($range_from);
} else {
$txt_range= "".$range_from."-". ($iFilesize-1);
$content_length= (int) ($iFilesize) - (int) ($range_from);
}
// Output headers defining partial content
header("HTTP/1.1 206 Partial Content");
header("Accept-Ranges: bytes");
header("Content-Range: bytes $txt_range/$iFilesize");
header("Content-Length: $content_length");
} else {
// No range requested, define full range then
header("Content-Length: ".$iFilesize);
}
// Seek to requested startbyte offset
fseek($fd, (int) $range_from);
// Ignore user abort if you wish to log transfer (see below)
ignore_user_abort(true);
// Set timeout limit to 0 (script doesn't time out)
set_time_limit (0);
// Total bytes output (starts with 0)
$totalBytesSent= 0;
$bytesSent=0;
// Connection status var
$cstatus= 0;
// Begin outputting loop:
// Loop until EOF is reached, or connection is broken
while (!feof($fd) && (($cstatus=connection_status())==0)) {
$buffer= fread ($fd, 65536); // read in 64k chunks
print $buffer; // send to browser
flush(); // Flush output (this also waits until flushed)
$bytesSent= strlen($buffer);
$totalBytesSent+= $bytesSent;
logBytesSentInDatabase ($bytesSent);
}
fclose ($fd);
// Here you can log the ouput. Available vars are
// $totalBytesSent -- total bytes sent
// $cstatus -- connection status at the exit from the loop
// (0- ok, 1- aborted, 2- timeout, 3- other)
} else {
// If opening input file failed, send HTTP 404
// or do something else here
}
?>
The only very specific thing here is that I check bandwidth prior to opening the file for ouput. This is checked from a MySQL table. Also, at the end of each packet output iteration (in the loop), the pakcet size sent is logged in that same MySQL table.
As you can see, if user aborts somewhere in the middle of a packet, the entire packet will be sent nonetheless, and all 64k are logged. This is imho acceptable margin error for actual bandwidth used from server to client, per EACH download attempt. Someone may wish to raise or lower packet size, though. It all depends on traffic expected, monthly bandwidth, precision wanted, etc...
There is one small problem. On my server this script simply ends after, say, 2MB, sometimes after 4MB. It ends abruptly so nothing is logged at the end of the loop (it does not exit the loop cleanly). I tried altering set_time_limit, or even including it in the loop, so it would be reset to, for example 20 seconds, at the beginning of each iteration. It didn't help.
My server support tells me the script is hanging because it consumes too much CPU! I have no idea how to optimize this for lower CPU usage. The files this script is supposed to transfer are large, from 60MB to a couple of hundred.