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');
Related info
Determining MIME type of a file automatically
More on determining the MIME type
Common MIME types
HTTP, caching and other stuff
December 13th, 2007 at 5:25 pm
Thanks for the function! One issue:
While the browser (Firefox) is downloading a file outputted by this function, it does not appear to know the total size of the file. So there is no download progress and the download status reads something like “downloaded 3.5MB of unknown”. Is that something we can fix?
December 13th, 2007 at 7:42 pm
The total size does appear for me when using Firefox, so I don’t know about that. However, your question prompted me to review the code and I found a (non-critical) bug, which I have now fixed (I think). You can try the modified function and see if that helps any.
December 20th, 2007 at 8:36 pm
Strange. I’m still getting “unknown file size” with Firefox or IE7. Thanks for your help!
(Sorry for the late response. Forgot to check back here.)
December 20th, 2007 at 10:30 pm
Strange indeed. Oh well
January 25th, 2008 at 6:24 am
Thank you for your script! I tried a few others, and no matter what I did, powerpoint files were coming through with a filesize of zero bytes. Using this script however, I am able to get the file to come through properly.
I do have one problem though. I am programming my site for Japanese (I work in a Japanese company), and when I call the function with $name using Japanese text, it comes through garbled. I have tried using header(’Accept-Charset: Shift_JIS’) and header(’Content-Type: ‘ . $mime_type ‘; charset=Shift_JIS’); and a combination of both, but they don’t seem to work. I have also tried using the other two types of Japanese encoding, and they dont seem to work either. Any ideas?
January 25th, 2008 at 2:30 pm
I don’t have a complete solution, but here are some ideas :
* Use a HTTP header viewer and look what headers are sent by other websites that display the filename correctly. For example, if you have FireFox, you could use the LiveHTTPHeaders addon for this.
* When you set charset=whatever, are you sure you’re using the same charset in your PHP file? Maybe it’s actually Unicode?
* See this page - HTTP headers and non-ASCI characters
February 2nd, 2008 at 12:58 am
Thanks so much for this script! I was looking for a way to force the download of a vcard off my website and this did the trick!
February 22nd, 2008 at 9:26 am
hi
the code for download works fine for me…
thank you very much…
March 6th, 2008 at 10:52 pm
For some reason it is not working for me. I have these attributes assigned in my downloadfile.php:
filename: test.12.docfiletype: application/msword
filepath: /files/15.attach
I call it using the function that is in this article using the following:
output_file($filepath, $filename, $filetype);However, I get this error:
File not found or inaccessible!March 6th, 2008 at 10:53 pm
OOPS! If anyone has a solution for the above problem, please email me at mrd3sai@gmail.com (Thanks)
March 7th, 2008 at 12:35 am
A path that starts with a forward slash will be interpreted as an absolute path by most operating systems, which is probably not what you want to do here. If you need the script to download the file “15.attach” from a folder called “files” that is in the same directory as the script, you should use “file/15.attach”, not “/file/15.attach”.
March 7th, 2008 at 1:16 am
Found out the bug was in my functions.php file. I had a space at the end and because headers don’t do well if something is already outputted, it was erroring out.
March 18th, 2008 at 2:23 pm
Just wanted to say thanks - helped me get something done at work. I’ve left the URL of this page in the PHP comments in case anyone other than me works on that partic script
March 18th, 2008 at 2:32 pm
Glad to help
April 15th, 2008 at 7:52 pm
Very nice.
I am also getting the filesize problem. filesize($file); returns the correct filesize, so I can only assume the browser (firefox/ie7) is not figuring out the header.
This happens with other download scripts I have seen. For example, the simplest way of doing it:
header(’Content-Length: ‘.filesize($file));
has the same problem.
April 15th, 2008 at 8:03 pm
Just tested again with the first download manager in google (http://www.freedownloadmanager.org) and size is given correctly, resume works nicely etc. Good job.
Doesn’t help explain browsers though(?)
April 15th, 2008 at 8:18 pm
I can’t add much to what you said.
However, you might be able to locate the cause by looking at the complete HTTP headers for a download that displays the file size, and the one that doesn’t. See what the difference is.
I’m too lazy to do it myself
April 15th, 2008 at 8:53 pm
Another thing Mr. Shadow. What would you say is the most robust way of implementing this?
At the moment I have a page which fetches the information about the download. Currently, the file path is put in a session variable, and a link is given on this page to download.php. download.php contains you script. It looks at the session variable, and calls output_file accordingly.
But it is not perfect. For example, if I cancel the download in firefox half way, then click the download.php link a second time to restart it, nothing happens. (Similarly, clicking retry in firefox attempts to download download.php rather than the correct file).
Cheers
April 15th, 2008 at 9:23 pm
I think the obvious way would be to pass the information to download.php in a URL parameter. Though you probably don’t want the user to know the file path (right?). For this, there are many possible solutions :
* Store the file path in a database and pass an unique ID to download.php, e.g. “download.php?id=123″. Make the download script get the appropriate path from the DB. This is probably what I would do.
* Do the same thing but instead of a DB save the ID/filepath pairs in a file.
* Encrypt the file path and pass it to the script. You could use the mcrypt module, or something like this encryption class
Use what best fits your situation.
April 16th, 2008 at 3:23 pm
‘Robustivness’ problem identified. In my case, session_start() at the start of download.php. Calling a session messes up the flow I suppose. I will be implementing the db approach.
Still no clues about filesize.
Thanks again
May 25th, 2008 at 2:41 pm
Thanku very much .. its working fine in my code
June 10th, 2008 at 12:45 pm
[...] This script has done well for me. __________________ This is not a signature. [...]
July 11th, 2008 at 5:34 pm
Thanks for the code! Worked a charm.
I need this to get it to work with Flash-based slideshows like http://www.flashnifties.com/xml_slideshow.php or http://www.maani.us/xml_slideshow. Without download resume support, Flash cannot download the images and generates an error in the slideshow, which means you could only use static images, not ones coming from a script.
Thanks again!
July 15th, 2008 at 11:43 am
This script is working very well, there is one question I have and it’s late, or early depending on how you look at it, but I can’t get this script to do anything once its completed. I would prefer to have it return true since I am calling it from with in a custom class, but even if i could get it to do a header redirect that would be good.
Thanks so much for the fine work though and any help you can give!
July 15th, 2008 at 11:59 am
Have you tried simply replacing the last die() with “return true”? I haven’t tested it but I think it should work. This wouldn’t tell you if the file download has completed successfuly (in fact, that would be impossible to know server-side with multithreaded download managers), but you would at least know the downloader has disconnected (but may connect again later if that wasn’t the last chunk to download).
July 21st, 2008 at 11:36 am
excuse me, i just wanna ask something
what does this part means?
header(”Expires: Mon, 26 Jul 1997 05:00:00 GMT”);
thanks before
July 21st, 2008 at 1:05 pm
This is supposed to prevent the browser (if a browser is used to download the file) from caching the download. Specifically, it states that the file can only be cached until an arbitrary day in July 1997, which means it will not be cached at all.
July 25th, 2008 at 5:23 am
[...] use This script. Put it on your server, and make your links point to the script, using the link syntax that they [...]