How To Force File Download With PHP
Say you want a PHP script that will make the browser to download a file instead of opening it. This is useful for common filetypes that would normally be displayed in a browser, like .html, images, PDFs and .doc files.
You can find a lot of scripts that do this on the web but one thing most of them don’t handle is download resuming and multi-thread downloading. If you don’t need that feature, feel free to use a simpler script. Personally I’ve found that a function that handles download resuming works more reliably across various browsers (what actually happened : a simpler script didn’t work with Opera + my weird Internet connection, so I found another script and decided to make broad generalizations about download resuming :p).
Note – most of the code below isn’t mine. I found it somewhere on the web and adjusted it for my needs; unfortunately I’ve lost the URL of the original page. If you know where it came from, let me know and I’ll add a link to this post.
function output_file($file, $name, $mime_type=”)
{
/*
This function takes a path to a file to output ($file),
the filename that the browser will see ($name) and
the MIME type of the file ($mime_type, optional).
If you want to do something on download abort/finish,
register_shutdown_function(‘function_name’);
*/
if(!is_readable($file)) die(‘File not found or inaccessible!’);
$size = filesize($file);
$name = rawurldecode($name);
/* Figure out the MIME type (if not specified) */
$known_mime_types=array(
“pdf” => “application/pdf”,
“txt” => “text/plain”,
“html” => “text/html”,
“htm” => “text/html”,
“exe” => “application/octet-stream”,
“zip” => “application/zip”,
“doc” => “application/msword”,
“xls” => “application/vnd.ms-excel”,
“ppt” => “application/vnd.ms-powerpoint”,
“gif” => “image/gif”,
“png” => “image/png”,
“jpeg”=> “image/jpg”,
“jpg” => “image/jpg”,
“php” => “text/plain”
);
if($mime_type==”){
$file_extension = strtolower(substr(strrchr($file,”.”),1));
if(array_key_exists($file_extension, $known_mime_types)){
$mime_type=$known_mime_types[$file_extension];
} else {
$mime_type=”application/force-download”;
};
};
@ob_end_clean(); //turn off output buffering to decrease cpu usage
// required for IE, otherwise Content-Disposition may be ignored
if(ini_get(‘zlib.output_compression’))
ini_set(‘zlib.output_compression’, ‘Off’);
header(‘Content-Type: ‘ . $mime_type);
header(‘Content-Disposition: attachment; filename=”‘.$name.'”‘);
header(“Content-Transfer-Encoding: binary”);
header(‘Accept-Ranges: bytes’);
/* The three lines below basically make the
download non-cacheable */
header(“Cache-control: private”);
header(‘Pragma: private’);
header(“Expires: Mon, 26 Jul 1997 05:00:00 GMT”);
// multipart-download and download resuming support
if(isset($_SERVER[‘HTTP_RANGE’]))
{
list($a, $range) = explode(“=”,$_SERVER[‘HTTP_RANGE’],2);
list($range) = explode(“,”,$range,2);
list($range, $range_end) = explode(“-“, $range);
$range=intval($range);
if(!$range_end) {
$range_end=$size-1;
} else {
$range_end=intval($range_end);
}
$new_length = $range_end-$range+1;
header(“HTTP/1.1 206 Partial Content”);
header(“Content-Length: $new_length”);
header(“Content-Range: bytes $range-$range_end/$size”);
} else {
$new_length=$size;
header(“Content-Length: “.$size);
}
/* output the file itself */
$chunksize = 1*(1024*1024); //you may want to change this
$bytes_send = 0;
if ($file = fopen($file, ‘r’))
{
if(isset($_SERVER[‘HTTP_RANGE’]))
fseek($file, $range);
while(!feof($file) &&
(!connection_aborted()) &&
($bytes_send<$new_length)
)
{
$buffer = fread($file, $chunksize);
print($buffer); //echo($buffer); // is also possible
flush();
$bytes_send += strlen($buffer);
}
fclose($file);
} else die('Error - can not open file.');
die();
}
/*********************************************
Example of use
**********************************************/
/*
Make sure script execution doesn't time out.
Set maximum execution time in seconds (0 means no limit).
*/
set_time_limit(0);
$file_path='that_one_file.txt';
output_file($file_path, 'some file.txt', 'text/plain');
[/sourcecode]
Related info
Determining MIME type of a file automatically
Common MIME types
HTTP, caching and other stuff
Hi,
Warning: Cannot modify header information – headers already sent by (output started at F:\localhost\CURRENT\cngadv\mrp\incl\kondisi.php:65) in F:\localhost\CURRENT\cngadv\mrp\incl\function.php on line 361
can be fixed if you remove all priint and acho statements in your code
If it still comes then use ob_start(); at the start of file.
Anees
Very useful article and script, thank you very much.
This works great in all browsers except Firefox (v 3.5.4). In Firefox there is a prompt that says:
C:\Users\Downloads\smilies.zip.part could not be saved, because the source file could not be read.
Try again later, or contact the server administrator.
But if you hit the retry button in the Firefox download manager it downloads ok. All other browsers work fine… any suggestions?
What I would do : Use a network sniffer and compare the HTTP headers generated by the script to headers generated by normal downloads. Then tweak the script so that it outputs sufficiently similar headers.
Sorry I am not familiar with a “network sniffer” I tried googling it and found some results for freeware but I have no clue how to use it. Can you suggest a free, easy one that I could use?
SmartSniff is free and pretty easy to use. Wireshark is probably the most popular one, but personally I find it a bit too complicated.
Wireshark is way overkill for that kind of thing. Fiddler2 is probably the way to go. It’s actually Microsoft-developed and free — go figure (although beginning to be more common these days). There is even a Firefox plugin for it.
But ya, I definitely agree with White Shadow here. There is almost certainly a problem with your headers because I have had this working in Firefox for a while now.
I was able to use the FireFox add-on to view the headers. The only difference I see is that when serving the download directly the header tag Conetet-Disposition is added.
Header direct download:
HTTP/1.1 200 OK
Date: Tue, 03 Nov 2009 21:30:35 GMT
Server: Apache/2.0.52 (Red Hat)
Vary: Host
Last-Modified: Tue, 03 Nov 2009 04:43:32 GMT
Etag: “4f86a7-284aab-22e02500″
Accept-Ranges: bytes
Content-Length: 2640555
Keep-Alive: timeout=15, max=91
Connection: Keep-Alive
Content-Type: application/zip
Headers through force download
HTTP/1.1 200 OK
Date: Tue, 03 Nov 2009 21:32:26 GMT
Server: Apache/2.0.52 (Red Hat)
Vary: Host,Accept-Encoding
X-Powered-By: PHP/4.3.9
Expires: Mon, 26 Jul 1997 05:00:00 GMT
Cache-Control: must-revalidate, post-check=0, pre-check=0, private
Pragma: public
Content-Encoding: gzip
Content-Disposition: attachment; filename=”1000smilies.zip”
Content-Transfer-Encoding: binary
Accept-Ranges: bytes
Content-Length: 2640555
Keep-Alive: timeout=15, max=84
Connection: Keep-Alive
Content-Type: application/x-zip-compressed
I deleted some for testing so they matched up but I get the same result. Anything you see that sticks out at you?
No, the headers look fine to me. Perhaps there is some other difference, like the script dropping the connection too early or some other glitch?
I just tried testing the script with FF 3.5.4 myself, but it seems to work fine on my machine (at least when the script is run on localhost).
Not sure why it would work fine in all browsers except Firefox. If the script was dropping the connection I would think it would drop it in all browsers. Do you think the gzip is causing the issue? Thank you for your time.
I commented out @ob_end_clean(); and it seems to have fixed the issue. Do you think that will cause issues?
It should be fine. That ob_end_clean() call is only relevant on servers that have output buffering turned on by default, so commenting it out shouldn’t cause problems on most systems.
Wow just what I have been searching for, great script.
One or two Q’s tho, where would I place it on my web page to give a consistent feel with my site.
Do I place it in the head or body part of the page.
Also could I have a button to execute the script on the same page.
Many thanks for a fantastic script.
VoodooJai.
I think it would be better to put the script in a separate file and then place a link to the script on your page. With a bit of CSS, you can even style the link as a button (google it).
Hi again
I have tested this script locally using apache & firefox and when I change the file names to download I still get the previous file not the new one.
When I upload the files and use remotely I still have this problem with FF but all seems fine with IE8.
Any solutions !
VoodooJai
It’s probably Firefox caching the file. You could try varying the download URL by attaching some superfluous parameters, like “&rand=64564” or similar.
Hi
I think your right in that caching is occuring, even if I change the download filename and save the php file it still gets the previous file.
To get a different file I have to rename the download script file and it will get the new file, but then back to the same old problem.
Frustrated
VoodooJai
As I said, just add some parameters to the URL. If your script is download.php, change the URL to download.php?rand=123 or whatever. You could also investigate the Expires & Cache-Control HTTP headers. Also, see section 14.9 of this RFC.
Hi, i’m new to php programming, and i could use a little help with implementing this. I see that there are no open and closing tags around this script? I also understand that i need to save this script as a .php file and call it something like filedownload.php? I need this file to handle image downloads for me and i ‘m not really sure how to go about it? I have a WordPress website with a lot of images, in the images folder, and the images are stored in different folders, for different categories. Can i just put this script in my images folder and call it from html for each image? This is very tricky i guess?
Oh for the mind-tearing dread of Cthulhu…
I’d really recommend you to learn a bit more about PHP programming before attempting this. Alternatively, you could perhaps ask the same question on one of the programming forums.