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

Share :
  • Reddit
  • del.icio.us
  • Digg
  • StumbleUpon
  • DZone
  • Ping.fm
  • Sphinn
Related posts :

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

Pages: « 1 [2] 3 » Show All

  1. 14
    White Shadow says:

    Glad to help :)

  2. 15
    Lou says:

    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.

  3. 16
    Lou says:

    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(?)

  4. 17
    White Shadow says:

    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 :P

  5. 18
    Lou says:

    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

  6. 19
    White Shadow says:

    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.

  7. 20
    Lou says:

    ‘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

  8. 21
    varaprasad says:

    Thanku very much .. its working fine in my code

  9. [...] This script has done well for me. __________________ This is not a signature. [...]

  10. 23
    John says:

    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!

  11. 24
    Matt says:

    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!

  12. 25
    White Shadow says:

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

  13. 26
    urei says:

    excuse me, i just wanna ask something
    what does this part means?

    header(”Expires: Mon, 26 Jul 1997 05:00:00 GMT”);

    thanks before :)

  14. 27
    White Shadow says:

    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.

  15. [...] use This script. Put it on your server, and make your links point to the script, using the link syntax that they [...]

  16. 29
    alexa says:

    It works in firefox fine. but in IE the save dialog box is not coming.
    what could be the reason?

  17. 30
    White Shadow says:

    “Blame Microsoft!”
    As I might have suggested before, compare headers sent by downloads that show the download box and and those that don’t. See how they differ.

  18. 31
    valugi says:

    this works well. Thanks

  19. 32
    pixelterra says:

    Nice script. Thanks for publishing. 2 questions:

    Can I call this after output has started?
    Is it possible after calling this function to send another page to the browser?

  20. 33
    White Shadow says:

    @pixelterra – It won’t work after output has started unless you are buffering the output. This is because the script needs to send some headers which can only be done before any other (non-header) output is sent.

    I don’t think you can send another page after this script, but I suspect you could achieve the result you’re thinking of by sending the “other” page first and adding a meta refresh tag (or something similar, i.e. JS redirect) that will trigger the download.

  21. 34
    Zid says:

    how to install it?
    where do i have to put this?
    please answer me… tnx

  22. 35
    White Shadow says:

    @Zid – Basically, just put it in a .php file and change the filenames to your own. The script already includes a simple example of use (it’s at the end of the script).

  23. 36
    Zid says:

    @White Shadow – thanks… (^^,)

  24. 37
    aseng says:

    @White Shadow – i still have problem … the dowload dialog window didn’t show …

    some error :

    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

    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 362

    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 363

    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 364

    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 368

    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 369

    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 370

    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 391

    please help me,…. thx

  25. 38
    White Shadow says:

    @aseng – Well, I don’t know anything your scripts, so all this tells me is that something already sent some output before calling the output_file() function. Figuring out what did that might solve the problem.

  26. 39
    Eric says:

    Interesting that you’re using 1MB buffers. Many of the variations of this on the web use 8k buffers. That ends up making a ridiculous amount of IO calls for large files. I was using 32k, but I tried the 1MB buffer as you use here and it seems to reduce CPU usage even further.

  27. 40
    White Shadow says:

    @Eric – It’s really a bona-fide optimization problem. Larger buffers save CPU cycles but use up more memory. There are some situations when a script with a smaller buffer would hold up better under heavy load.

  28. 41
    Francisco Junior says:

    Thanku very much.

    Parabens pelo tutorial . :D

  29. 42
    cm says:

    does this script work with force downloading of video files? specifically .mov files that are on the bigger side (in excess of 100 MB)? i’m testing with 15MB .mov files – i get the download box but get a file that is 0kb – any suggestions on how to tweak accordingly?

    thanks for putting this out there…

  30. 43
    White Shadow says:

    @cm – In theory, it should work with files of any size. I’ve had a few minor problems in certain browsers (e.g. Opera sometimes stops downloading the file before the download is complete, so I need to resume it), but nothing significant.

    The way I would approach this problem is I would run a network sniffer (for example, SmartSniff – google it) and watch what actually happens during the browser-server communication when you get the 0Kb file. Otherwise there simply isn’t enough information to figure out the likely source of the problem.

Pages: « 1 [2] 3 » Show All

Leave a Reply