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

Related posts :

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

  1. Francisco Junior says:

    Thanku very much.

    Parabens pelo tutorial . 😀

  2. 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…

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

  4. MeltingMIND says:

    Hey- Thanks for the great script. I got it working, and it works perfectly with 200MB+ files. I see your donate button, so I’ll have to send you some cash! One question. How secure is this script if I’m using a variable to get the file like so:
    download.php?fileName=video.mov

    And users can see the script path in the html? I hard coded the directory in the script. Would someone be able to use the script and a bit of trickery to download any file on the site? Thanks again for the great script.

  5. White Shadow says:

    @MeltingMIND – Thanks 🙂

    The script isn’t very secure – it wasn’t intended to be. However, you could improve the security with a few modifications.

    For example, in the current implementation, a user who knows (or can guess) the names of other files, could download any file. Here’s one way to prevent this :

    Pick a secret value and put it in your conf. files or wherever…

    $secret = "asdflyu345iu2y35ui345"; //choose your own
    

    …modify the file that shows the download links to calculate a hash for each link and append it to the download URL :

    $hash = md5($fileName . "|" . $secret);
    $url = "download.php?fileName=$fileName&hash=$hash";
    

    …and verify the hash in download.php :

    $test = md5($_GET['fileName'] . '|' . $secret);
    if ( $test == $_GET['hash'] ) {
       //...run the download...
    } else {
        die("Invalid hash value!");
    }
    

    Or something like that. Obviously you’ll need to adapt this to your setup.

  6. MeltingMIND says:

    Great, thanks for that. I will give it a try just to be double-sure. But the script, as is, if I hardcoded the file path in the script, wouldn’t allow download of any other files except those in the hard coded folder? If I make it a folder of only downloads, then I should be relatively OK?

    Though as I said, I may try your hash method to be sure… 😉

  7. White Shadow says:

    @MeltingMIND – Theoretically yes, but a user could try to pass in a relative path, e.g. “fileName=../../index.php”. You could treat filenames that contain slashes as invalid, but I’m not sure that would solve the problem completely.

  8. Rhys` says:

    Great script, thanks. I need some help though please:

    1. Tested on IIS6 running php 5.2.3; no problems. The ‘save as’ dialog pops up in FireFox and IE8. Beautiful.

    2. Tried on apache running php 5.2.5; all other php code works BUT instead of getting the file download dialog, I get several packets of html data including the file chunks tacked on the end. the header states the packet type is text/html, whereas with IIS, the type is binary (which is what I’m expecting).

    I’ve not been able to locate a similar issue via google. Is this a problem with php settings, php versions, or apache vs IIS? Does anyone have a solution to this?

    Thank you.

  9. White Shadow says:

    @Rhys` – I use Apache exclusively and the script works fine for me, so it’s unlikely that the problem is with the server itself. Configuration problems are possible, though.

    What kind of html data do you get? Maybe it’s error messages or something like that?

  10. Eric says:

    @Rhys` – You could also try adding these headers to the top:
    header(“Pragma: “);
    header(“Cache-Control: “);
    header(“Cache-Control: public”);

    This corrects certain issues with IE. I don’t know why, but it works.

  11. […] and it will open in whatever picture viewer they have set as a default on their computer. I use this force download script. It’s not the easiest to implement, but unlike other force download scripts I have used, this one […]

  12. Phoenix says:

    How can make it support resume but only allow for 1 thread only, not multi threads ?

  13. White Shadow says:

    I don’t think there’s an easy way to do it. You could use a database to record who (IP address) and when starts downloading the file. Then change the script to exit if the visitor’s IP is already downloading. It would probably end up pretty complex.

  14. radsdau says:

    Re. this problem:

    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

    I had this and resolved it; it is to do with output buffering. If output buffering is off, then the page header is sent as the php code is parsed, which makes it impossible to change the header (it’s already sent). The solution is to turn output buffering on:

    ob_start();

    There are a couple of other ob_ functions I can’t recall, but for me at least, adding ob_start() to the top of the relevant php file solved it.

  15. Senthil says:

    Simply superb ! Works a charm with a bit of modifications for my requirement which included a hash verification (like white shadow said).

  16. yann says:

    Hello,
    thanks for the script !

    I have a bug on my site, when user clicks my DOWNLOAD FILE button, the image, video or sound file doesn’t get downloaded but the browser displays ASCII characters, here an extract :
    �����JFIF���������;CREATOR: gd-jpeg v1.0 (using IJG JPEG v62), quality = 90 ���C� ���C ���,�”�������������� ������ �}�!1AQa”q2���#B��R��………..

    I have this error with different MIME TYPES : mp3, jpg, mpg, flv, ….

    What could be the reason ?

    THanks in advance.

  17. White Shadow says:

    Have you tried a different browser? I’ve had this problem with certain filetypes in Opera, regardless of the server.

    Other than that, you could use a a network sniffer to find out what’s different between the downloads that work normally and those that have this problem.

  18. peter says:

    The function doesn’t work if the files are more than 1MB. I’m just getting a 404 error.

  19. White Shadow says:

    Hmm, it works fine here. And even if it glitches when loading large files, it shouldn’t produce a 404 error – the script just doesn’t include any commands that could do that.

    Nevertheless, you could try setting $chunksize to a lower value, e.g. 102400 bytes. It’s currently set to 1024*1024 (= 1 MB), which might cause the script run out of RAM on severely memory-restricted systems.

  20. peter says:

    I tried making that higher so that’s probably the problem. I only needed to download rar, zip and exe so i’m now using the few lines in the php manual which seems to work.

Leave a Reply