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
Common MIME types
HTTP, caching and other stuff

Related posts :

110 Responses to “How To Force File Download With PHP”

  1. Anees says:

    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

  2. Francesco says:

    Very useful article and script, thank you very much.

  3. Westerner says:

    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?

  4. White Shadow says:

    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.

  5. westerner says:

    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?

  6. White Shadow says:

    SmartSniff is free and pretty easy to use. Wireshark is probably the most popular one, but personally I find it a bit too complicated.

  7. Eric says:

    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.

  8. Westerner says:

    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?

  9. White Shadow says:

    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).

  10. Westerner says:

    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.

  11. Westerner says:

    I commented out @ob_end_clean(); and it seems to have fixed the issue. Do you think that will cause issues?

  12. White Shadow says:

    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.

  13. VoodooJai says:

    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.

  14. White Shadow says:

    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).

  15. VoodooJai says:

    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

  16. White Shadow says:

    It’s probably Firefox caching the file. You could try varying the download URL by attaching some superfluous parameters, like “&rand=64564″ or similar.

  17. VoodooJai says:

    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

  18. White Shadow says:

    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.

  19. mark says:

    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?

  20. White Shadow says:

    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.

Leave a Reply