I want to create xlsx file and send to the browser as an attachment, so the user downloads it immediately. I literally did copy-paste the code from this example.
But it doesn't work. I fixed the path in require_once, but the issue is somewhere else.
The xlsx file is generated corerctly - when I save ot on the server, I can open it. It is also sent to the browser - firebug's console shows some funny characters in output window. The headers are also correct.
But no Save as... dialog is shown. I did some basic checks based on google search results - I have no extra white space after ?>.
The only difference in my code is that I call php script from jQuery's $.post function with some additional arguments.
Could it be the reason why I can't download this file?
Any help would be greatly appreciated!
PHP version: 5.4.20
PHPExcel version: 1.8.0
Server: Apache/2.4.6 (Linux/SUSE)
This question is also posted on codeplex.
You can't download files via an ajax request such as $.post for security reasons.
You could use a link that opens in a new window instead.
<?php
// If user click the download link
if(isset($_GET['filename'])){
// The directory of downloadable files
// This directory should be unaccessible from web
$file_dir="";
// Replace the slash and backslash character with empty string
// The slash and backslash character can be dangerous
$file_name=str_replace("/", "", $_GET['filename']);
$file_name=str_replace("\\", "", $file_name);
// If the requested file is exist
if(file_exists($file_dir.$file_name)){
// Get the file size
$file_size=filesize($file_dir.$file_name);
// Open the file
$fh=fopen($file_dir.$file_name, "r");
// Download speed in KB/s
$speed=50;
// Initialize the range of bytes to be transferred
$start=0;
$end=$file_size-1;
// Check HTTP_RANGE variable
if(isset($_SERVER['HTTP_RANGE']) &&
preg_match('/^bytes=(\d+)-(\d*)/', $_SERVER['HTTP_RANGE'], $arr)){
// Starting byte
$start=$arr[1];
if($arr[2]){
// Ending byte
$end=$arr[2];
}
}
// Check if starting and ending byte is valid
if($start>$end || $start>=$file_size){
header("HTTP/1.1 416 Requested Range Not Satisfiable");
header("Content-Length: 0");
}
else{
// For the first time download
if($start==0 && $end==$file_size){
// Send HTTP OK header
header("HTTP/1.1 200 OK");
}
else{
// For resume download
// Send Partial Content header
header("HTTP/1.1 206 Partial Content");
// Send Content-Range header
header("Content-Range: bytes ".$start."-".$end."/".$file_size);
}
// Bytes left
$left=$end-$start+1;
// Send the other headers
header("Content-Type: application/octet-stream");
header("Accept-Ranges: bytes");
// Content length should be the bytes left
header("Content-Length: ".$left);
header("Content-Disposition: attachment; filename=".$file_name);
// Read file from the given starting bytes
fseek($fh, $start);
// Loop while there are bytes left
while($left>0){
// Bytes to be transferred
// according to the defined speed
$bytes=$speed*1024;
// Read file per size
echo fread($fh, $bytes);
// Flush the content to client
flush();
// Substract bytes left with the tranferred bytes
$left-=$bytes;
// Delay for 1 second
sleep(1);
}
}
fclose($fh);
}
else
{
// If the requested file is not exist
// Display error message
echo "File not found!";
}
exit();
}
?>
Download.
You can use this code for download you can edit extension and also speed
Related
I wanted to create a .php file, that streams a video!
Now, the problem is, that it works, only if i use a normal readfile(), but then, you can not go back and forward in the video, so i searched on google, to find this code:
(basically, the HTTP_RANGE does not work, NEVER, i do not know why, when testing it, it always fires my die("lol?");, so it clearly does not support it for some reason)
(the die() function is left there on purpose, it will be taken out if it would work..)
(note that i changed "$size = filesize($file);" to "$size = filesize(".".$file);", because someone mentioned that this is required, and "filesize($file);" does not work for me anyways, it always fires an error)!
(and, the $file, shows the actual path for my file, nothing replaced, its how it looks in the original php of me!)
<?php
// Clears the cache and prevent unwanted output
ob_clean();
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
apache_setenv('no-gzip', 1);
ini_set('zlib.output_compression', 'Off');
$file = "/cdn4-e663/zw4su8jiy8skgvizihsjehj/2038tkusi9u848sui7zh/2q3z6hjk97ujduz/a1-cdn/9zw35jbmhkk47wi63uu7.mp4"; // The media file's location
$mime = "application/octet-stream"; // The MIME type of the file, this should be replaced with your own.
$size = filesize(".".$file); // The size of the file
// Send the content type header
header('Content-type: ' . $mime);
// Check if it's a HTTP range request
if(isset($_SERVER['HTTP_RANGE'])){
// Parse the range header to get the byte offset
$ranges = array_map(
'intval', // Parse the parts into integer
explode(
'-', // The range separator
substr($_SERVER['HTTP_RANGE'], 6) // Skip the `bytes=` part of the header
)
);
// If the last range param is empty, it means the EOF (End of File)
if(!$ranges[1]){
$ranges[1] = $size - 1;
}
// Send the appropriate headers
header('HTTP/1.1 206 Partial Content');
header('Accept-Ranges: bytes');
header('Content-Length: ' . ($ranges[1] - $ranges[0])); // The size of the range
// Send the ranges we offered
header(
sprintf(
'Content-Range: bytes %d-%d/%d', // The header format
$ranges[0], // The start range
$ranges[1], // The end range
$size // Total size of the file
)
);
// It's time to output the file
$f = fopen($file, 'rb'); // Open the file in binary mode
$chunkSize = 8192; // The size of each chunk to output
// Seek to the requested start range
fseek($f, $ranges[0]);
die("working?");
// Start outputting the data
while(true){
// Check if we have outputted all the data requested
if(ftell($f) >= $ranges[1]){
break;
}
// Output the data
echo fread($f, $chunkSize);
// Flush the buffer immediately
ob_flush();
flush();
}
}
else {
die("lol?");
header('Content-Length: ' . $size);
// Read the file
readfile($file);
// and flush the buffer
ob_flush();
flush();
}
?>
so, the die("lol?"); was added by me to see if the
if(isset($_SERVER['HTTP_RANGE'])){
/*function fires or not, and no, as it seems it returns FALSE every time..8/
}
so i wanted to ask you all, how can i fix this? i really want to use php to stream my video, because of security reasons, and because i like it, i already use this methode with images but its a different code(and working)!
I am using Apache 2.4 (Windows 10 - 64bit PC) with the latest version of PHP7, but it seems that apache does not support HTTP_RANGE? am i missing something, is there something i need to enable inside either the php.ini or the httpd.conf??
Thank you in advance, i hope someone can tells me what to do, because i really am stuck here, and i tried ALL examples of mp4 video streaming i could find on google, and none worked for me :/
There are 2 parts to this:
The request made by the browser/client. This must send appropriate request headers.
The response given by your server. This is done by your PHP script and must also send the appropriate response headers
When you try and stream your video (or whatever the content is) open the Network tab in your browser.
Look at the Request Headers (in Chrome this is under the Network tab). I've posted a screenshot below. Note that in the request there is a Range: parameter. If this is not present in the request, you'll have problems. This is what tells the PHP script on the server that you are doing a range request in the first place. If the server does not see this header in the request then it will just bypass the if statement and go into the die.
Note that the Range: request header is not normally included in requests by default, so unless you are specifying this, it will never work. If you don't see it in the Request Headers on your Network tab, it is not present, and you need to fix that.
You may also want to examine the response headers - which are totally different from the request headers. Again, these can be seen in the Network tab in your browser. See below for the appropriate headers that must be set:
Going back to the original question, none of it has anything to do with the response (which is what you were describing). The initial problem you are having is all to do with how you're making the request and the fact it does not contain a Range: header, when it must do so.
I am trying to find a solution for this problem for some days already, I tried all advices I could find here on stackoverflow and other platforms. And still, there is no solution.
I am embedding a video via HTML5 video tag:
<video poster="thumb.png" controls="controls" preload="none" width="640" height="480">
<source src="provider.php?secure=12345" type="video/mp4">
</video>
I try to deliver the MP4 video file by PHP instead of linking it directly. Linking the mp4 file directly works and plays the file!
Testing:
the video file: https://github.com/q2apro/videotest-ipad/raw/master/video.mp4 (plays on iPad)
the video file loaded by PHP with same headers: https://github.com/q2apro/videotest-ipad/blob/master/test-headers.php (not playing on iPad) - Sourcecode
the video file loaded by PHP with Byte Ranges: https://github.com/q2apro/videotest-ipad/blob/master/test-byterange.php (not playing on iPad) - Sourcecode
the video file loaded by PHP with Byte Ranges (another script): https://github.com/q2apro/videotest-ipad/blob/master/test-byterange-2.php (not playing on iPad, alert "The operation could not be completed") - Sourcecode
Notes:
all links above are directly accessing/playing the video file without embed tag
video works on all browsers in Windows (but not in Safari/Chrome on iPad, probably not iPhone either)
My Setup:
testing device: iPad iOS 6 (I don't have a mac, cannot debug)
iPad with Safari and Chrome (tried both browsers)
my server is shared-hosting from domainfactory
tool for debugging: Firefox 29 Web Developer Console / WIN7
The .htaccess in the test folder sets the MIME type and Accept-Ranges:
AddType video/mp4 .mp4
<IfModule mod_headers.c>
Header set Accept-Ranges "bytes"
</IfModule>
Even though I created the same header (compare test URLs 1. and 2.), the iPad is not playing the file through the PHP request.
Instead I always get this strikedthrough play button:
The headers of 1. (direct mp4 call):
The headers of 2. The same headers as above, but set up by PHP (mp4 delivered by PHP):
--
I have also tried reading the entire video file and sending it to the browser using PHP's fread(), fpassthru() and file_get_contents() but the iPad is always showing the cannot-play-icon.
--
My hosted server does not supply Connection keep-alive, could this be a problem?
Is the iPad interpreting .php different from .mp4?
Can somebody help me out of pain? I am totally stuck.
PS: What I tried to consider:
Byte range requests (206 partial content) 01 02 03
correct video encoding 04
used other encoded videos while testing
disabled zlib.output_compression in php scripts
UPDATE: Debug Console
I finally got a MAC of a friend, connected the iPad, opened the Debug Console in Safari on the Mac, loaded the page on the iPad and checked the error messages appearing on the Mac (btw, how more complicated could apple force us to develop...). For all test scripts this error appears:
Failed to load resource: Plug-in handled load
Wow, that was tough!
**1. First major Problem**
It turned out to be no encoding problem but a problem with the mp4 container header set during the video conversion process - iPad has obviously a problem with MP4 videos that are prepared for progressive streaming.
First I discovered that in a conversation here. After converting a video I always used the tool MP4 Fast Start to prepare the video file for progressive stream. This was necessary to stream the video file to the Flash Player in pieces (progressively), so it did not load the entire file (and the user had to wait).
With Handbrake there is a similar setting, that is called Web Optimized. It does the same:
Web Optimized
Also known as "Fast Start"
This places the container header at the start of the file, optimizing it for streaming across the web.
If you enable this and convert your video, the iPad will not play the video file! Instead you get the error "The operation could not be completed".
Check out and test it yourself: video test resources.
**2. Second Problem**
In production environment I always used PHP to check the referer. As I found out, the iPad does not send the referer information. This also prevents the streaming and you will also see the cannot-play-symbol (striked-through play icon).
**3. Third Problem**
I could not find out why, but the iPad only accepts the video streaming from this script http://ideone.com/NPSlw5
<?php
// disable zlib so that progress bar of player shows up correctly
if(ini_get('zlib.output_compression')) {
ini_set('zlib.output_compression', 'Off');
}
$folder = '.';
$filename = 'video.mp4';
$path = $folder.'/'.$filename;
// from: http://licson.net/post/stream-videos-php/
if (file_exists($path)) {
// Clears the cache and prevent unwanted output
ob_clean();
$mime = "video/mp4"; // The MIME type of the file, this should be replaced with your own.
$size = filesize($path); // The size of the file
// Send the content type header
header('Content-type: ' . $mime);
// Check if it's a HTTP range request
if(isset($_SERVER['HTTP_RANGE'])){
// Parse the range header to get the byte offset
$ranges = array_map(
'intval', // Parse the parts into integer
explode(
'-', // The range separator
substr($_SERVER['HTTP_RANGE'], 6) // Skip the `bytes=` part of the header
)
);
// If the last range param is empty, it means the EOF (End of File)
if(!$ranges[1]){
$ranges[1] = $size - 1;
}
// Send the appropriate headers
header('HTTP/1.1 206 Partial Content');
header('Accept-Ranges: bytes');
header('Content-Length: ' . ($ranges[1] - $ranges[0])); // The size of the range
// Send the ranges we offered
header(
sprintf(
'Content-Range: bytes %d-%d/%d', // The header format
$ranges[0], // The start range
$ranges[1], // The end range
$size // Total size of the file
)
);
// It's time to output the file
$f = fopen($path, 'rb'); // Open the file in binary mode
$chunkSize = 8192; // The size of each chunk to output
// Seek to the requested start range
fseek($f, $ranges[0]);
// Start outputting the data
while(true){
// Check if we have outputted all the data requested
if(ftell($f) >= $ranges[1]){
break;
}
// Output the data
echo fread($f, $chunkSize);
// Flush the buffer immediately
#ob_flush();
flush();
}
}
else {
// It's not a range request, output the file anyway
header('Content-Length: ' . $size);
// Read the file
#readfile($path);
// and flush the buffer
#ob_flush();
flush();
}
}
die();
?>
I hope this information will help others to cope with the problem.
Update: Three months later in production environment, some of my users still reported playback issues. There seems to be another problem with Safari. I advised them to use Chrome for iPad, this fixed it.
PS: A couple of days of research and hassle only to play a video file that, by the way, runs on all other devices. This again proves to me that Apple got successful just because of great marketing, not because of great software.
Thanks for your contribution, very important...
But even your code didn't make it for my iphone.
Even if I still don't know why, the following code worked for me.
Probably for the line:
header('HTTP/1.1 416 Requested Range Not Satisfiable');
I got this from here:
https://github.com/tikiatua/internal-assets-plugin/issues/9
$fp = fopen($filepath, "rb");
$size = filesize($filepath);
$length = $size;
$start = 0;
$end = $size - 1;
header('Content-type: video/mp4');
header("Accept-Ranges: 0-$length");
if (isset($_SERVER['HTTP_RANGE'])) {
$c_start = $start;
$c_end = $end;
list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
if (strpos($range, ',') !== false) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $start-$end/$size");
exit;
}
if ($range == '-') {
$c_start = $size - substr($range, 1);
} else {
$range = explode('-', $range);
$c_start = $range[0];
$c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size;
}
$c_end = ($c_end > $end) ? $end : $c_end;
if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $start-$end/$size");
exit;
}
$start = $c_start;
$end = $c_end;
$length = $end - $start + 1;
fseek($fp, $start);
header('HTTP/1.1 206 Partial Content');
}
header("Content-Range: bytes $start-$end/$size");
header("Content-Length: ".$length);
$buffer = 1024 * 8;
while(!feof($fp) && ($p = ftell($fp)) <= $end) {
if ($p + $buffer > $end) {
$buffer = $end - $p + 1;
}
set_time_limit(0);
echo fread($fp, $buffer);
flush();
}
fclose($fp);
exit;
(unfortunately I can't comment since I'm new to the forum, so it's a new post :)
Concerning the script at #3 (Third Problem): It worked for me on all browsers including older versions of Safari on iOS – but not on current versions of Safari, neither on iOS nor MacOS.
Reducing the end of each range ($ranges[1]) by 1 in line 37:
else {
$ranges[1]--;
}
... and extending the Content-Range by 1 (as Konrad suggested) in line 41:
header('Content-Length: ' . (($ranges[1] - $ranges[0]) + 1));
... worked for me and solved the problem for current Safari versions, although older versions (e.g. on iOS 9.3.5) now don't seem to work anymore.
I'm writing a PHP program that will get an image from the filesystem and display it on the returned page. The catch is that the file isn't stored in the /var/www directory. It's stored in /var/site/images. How can I do this? Do I have to read it into memory with fopen, then echo the contents?
Use fpassthru to dump the contents from the file system to the output stream. In fact the docs for fpassthru contains a demo of exactly what you're trying to do: http://us3.php.net/fpassthru
<?php
// open the file in a binary mode
$name = './img/ok.png';
$fp = fopen($name, 'rb');
// send the right headers
// - adjust Content-Type as needed (read last 4 chars of file name)
// -- image/jpeg - jpg
// -- image/png - png
// -- etc.
header("Content-Type: image/png");
header("Content-Length: " . filesize($name));
// dump the picture and stop the script
fpassthru($fp);
fclose($fp);
exit;
?>
I am trying to output an mp4 video file through PHP.
When it is used through a flash player (eg. flowplayer) it is working great.
But when I'm trying to use it as a source on an html5 video tag or to call directly the php file, it doesn't work.
The code I use is the following:
$filesize = filesize($file);
header("Content-Type: video/mp4");
if ( empty($_SERVER['HTTP_RANGE']) )
{
header("Content-Length: $filesize");
readfile($file);
}
else //violes rfc2616, which requires ignoring the header if it's invalid
{
rangeDownload($file);
}
and rangeDownload function is from http://mobiforge.com/developing/story/content-delivery-mobile-devices Appendix A.
Even when I use a Content-Range header (Content-Range:bytes 0-31596111/31596112), it stucks on downloading 30.13 MB of the video.
Finally I've found a way to make it work
header("Content-Type: $mediatype");
if ( empty($_SERVER['HTTP_RANGE']) )
{
header("Content-Length: $filesize");
$fh = fopen($file, "rb") or die("Could not open file: " .$file);
# output file
while(!feof($fh))
{
# output file without bandwidth limiting
echo fread($fh, $filesize);
}
fclose($fh);
}
else //violes rfc2616, which requires ignoring the header if it's invalid
{
rangeDownload($file);
}
It is working in direct link of the php file and inside html5 video tag.
But in order to work in Flowplayer (and maybe in other flash/html5 players) you need to add a mp4 extension (eg. view.php?id=XXX&file=type.mp4)
This could have to do with your browser and what plugin it uses to view video files ie) quicktime. The reason it works with Flash is flash handles buffering and time sync and such. It is usually not recommended to let the browser handle playing media files, because it completely depends on the browser configuration and the plugins they have installed.
Some browsers automatically download media files, it's completely configurable by browser and end user.
I'm writing a PHP script that allows the user to download a file. Basically the idea is to prevent the file being downloaded more than X times, since it is paid content, and the link should not be spread around.
Since the files will be pretty large, it should be good to implement resuming. I've read the standard, but it's pretty long and allows for some flexibility. Since I need to get it done quickly, I'd prefer a stable, tested implementation of this feature.
Can anyone point me to such a a script?
Seems that I found what I needed myself. So that other may benefit from this, here is the link: http://www.coneural.org/florian/papers/04_byteserving.php
And just in case the original page stops to work (the script is pretty old already), here is a copy of it:
<?php
/*
The following byte serving code is (C) 2004 Razvan Florian. You may find the latest version at
http://www.coneural.org/florian/papers/04_byteserving.php
*/
function set_range($range, $filesize, &$first, &$last){
/*
Sets the first and last bytes of a range, given a range expressed as a string
and the size of the file.
If the end of the range is not specified, or the end of the range is greater
than the length of the file, $last is set as the end of the file.
If the begining of the range is not specified, the meaning of the value after
the dash is "get the last n bytes of the file".
If $first is greater than $last, the range is not satisfiable, and we should
return a response with a status of 416 (Requested range not satisfiable).
Examples:
$range='0-499', $filesize=1000 => $first=0, $last=499 .
$range='500-', $filesize=1000 => $first=500, $last=999 .
$range='500-1200', $filesize=1000 => $first=500, $last=999 .
$range='-200', $filesize=1000 => $first=800, $last=999 .
*/
$dash=strpos($range,'-');
$first=trim(substr($range,0,$dash));
$last=trim(substr($range,$dash+1));
if ($first=='') {
//suffix byte range: gets last n bytes
$suffix=$last;
$last=$filesize-1;
$first=$filesize-$suffix;
if($first<0) $first=0;
} else {
if ($last=='' || $last>$filesize-1) $last=$filesize-1;
}
if($first>$last){
//unsatisfiable range
header("Status: 416 Requested range not satisfiable");
header("Content-Range: */$filesize");
exit;
}
}
function buffered_read($file, $bytes, $buffer_size=1024){
/*
Outputs up to $bytes from the file $file to standard output, $buffer_size bytes at a time.
*/
$bytes_left=$bytes;
while($bytes_left>0 && !feof($file)){
if($bytes_left>$buffer_size)
$bytes_to_read=$buffer_size;
else
$bytes_to_read=$bytes_left;
$bytes_left-=$bytes_to_read;
$contents=fread($file, $bytes_to_read);
echo $contents;
flush();
}
}
function byteserve($filename){
/*
Byteserves the file $filename.
When there is a request for a single range, the content is transmitted
with a Content-Range header, and a Content-Length header showing the number
of bytes actually transferred.
When there is a request for multiple ranges, these are transmitted as a
multipart message. The multipart media type used for this purpose is
"multipart/byteranges".
*/
$filesize=filesize($filename);
$file=fopen($filename,"rb");
$ranges=NULL;
if ($_SERVER['REQUEST_METHOD']=='GET' && isset($_SERVER['HTTP_RANGE']) && $range=stristr(trim($_SERVER['HTTP_RANGE']),'bytes=')){
$range=substr($range,6);
$boundary='g45d64df96bmdf4sdgh45hf5';//set a random boundary
$ranges=explode(',',$range);
}
if($ranges && count($ranges)){
header("HTTP/1.1 206 Partial content");
header("Accept-Ranges: bytes");
if(count($ranges)>1){
/*
More than one range is requested.
*/
//compute content length
$content_length=0;
foreach ($ranges as $range){
set_range($range, $filesize, $first, $last);
$content_length+=strlen("\r\n--$boundary\r\n");
$content_length+=strlen("Content-type: application/pdf\r\n");
$content_length+=strlen("Content-range: bytes $first-$last/$filesize\r\n\r\n");
$content_length+=$last-$first+1;
}
$content_length+=strlen("\r\n--$boundary--\r\n");
//output headers
header("Content-Length: $content_length");
//see http://httpd.apache.org/docs/misc/known_client_problems.html for an discussion of x-byteranges vs. byteranges
header("Content-Type: multipart/x-byteranges; boundary=$boundary");
//output the content
foreach ($ranges as $range){
set_range($range, $filesize, $first, $last);
echo "\r\n--$boundary\r\n";
echo "Content-type: application/pdf\r\n";
echo "Content-range: bytes $first-$last/$filesize\r\n\r\n";
fseek($file,$first);
buffered_read ($file, $last-$first+1);
}
echo "\r\n--$boundary--\r\n";
} else {
/*
A single range is requested.
*/
$range=$ranges[0];
set_range($range, $filesize, $first, $last);
header("Content-Length: ".($last-$first+1) );
header("Content-Range: bytes $first-$last/$filesize");
header("Content-Type: application/pdf");
fseek($file,$first);
buffered_read($file, $last-$first+1);
}
} else{
//no byteserving
header("Accept-Ranges: bytes");
header("Content-Length: $filesize");
header("Content-Type: application/pdf");
readfile($filename);
}
fclose($file);
}
function serve($filename, $download=0){
//Just serves the file without byteserving
//if $download=true, then the save file dialog appears
$filesize=filesize($filename);
header("Content-Length: $filesize");
header("Content-Type: application/pdf");
$filename_parts=pathinfo($filename);
if($download) header('Content-disposition: attachment; filename='.$filename_parts['basename']);
readfile($filename);
}
//unset magic quotes; otherwise, file contents will be modified
set_magic_quotes_runtime(0);
//do not send cache limiter header
ini_set('session.cache_limiter','none');
$filename='myfile.pdf'; //this is the PDF file that will be byteserved
byteserve($filename); //byteserve it!
?>
You should be using PEAR HTTP_Download. It is pretty easy to use and it allows download resuming just file:
http://pear.php.net/manual/en/package.http.http-download.intro.php
Based on this:
http://w-shadow.com/blog/2007/08/12/how-to-force-file-download-with-php/
(which you also could use)
I've made a small lib that does what PECL http_send_file extension does:
http://php.net/manual/en/function.http-send-file.php
(which you also could use)
The lib resembles the http_send_file, but if you don't have the option of installing the PECL lib, you could use the http-send-file lib:
https://github.com/diversen/http-send-file
See http://us3.php.net/manual/en/function.fread.php
An alternative is to let the web server can handle http by redirecting to the file in question.
A PHP script can do any checks needed (security, authentication, validate the file, incrementing the download count) and any other tasks before calling header("Location $urltofile");
I tested this with apache. Interrupt/resume download works. The server's mime type configuration will determine client behavior. For apache, if defaults in mime.types are not suitable, configuration directives for mod_mime could go in a .htaccess file in the directory of the file to download. If really necessary, these could even by written by the PHP script before it redirects.
Perhaps instead of implementing web server in a web server (yo dawg!) you could use mod trigger before download in lighttpd or mod X-Sendfile available for both lighttpd and Apache2?