WWW FAQs: How do I force the browser to download a file to disk instead of playing or displaying it?

2006-12-11: Sometimes linking directly to audio or video files can cause problems because the website isn't fast enough to support "streaming" video playback on the fly. Or users may have trouble with videos in their browser, but no trouble when they access them from their desktop. Or perhaps you just want to invite the user to save a text file or an image, rather than displaying it in the browser window. It doesn't really matter what kind of file it is - the point is that the browser would normally display it directly, and you want to help the user save it to disk instead.

The first and best solution is to use the Content-disposition: header, which tells the browser that regardless of the file's content type, the proper treatment is to save it to disk. This is the best solution because it requires no extra effort on the user's part. However, it does require that you have access to PHP, ASP.NET, Perl/CGI or another dynamic web programming solution. Which in most cases means it can't be used with free hosting services. But for serious webmasters, it's the right way to go, and I'll discuss it in detail.

The second technique, which works nearly as well, is simply to store your file inside a zip file. The vast majority of users today have either Windows XP or MacOS X, both of which include very friendly built-in handlers for zip files. Windows XP displays these as "compressed folders" and makes it very easy for the user to launch files or extract them to disk. Still, zip compression probably won't save any additional space when dealing with video and audio files, and there's a performance price - the user must wait for the file to be unzipped (or at least extracted) before it can be played back. So while zipfiles might be the only way for those who don't want to use PHP or a similar language, they definitely aren't the best way. Let's move on and look at how to do it the best way.

Forcing Downloads With Content-disposition:

The rest of this article assumes that your file is a video file. Keep in mind that you can use exactly the same techniques for other file types. Just substitute your filename in place of movie.mpg.
Those who understand HTTP know that the web server returns several "headers" along with every object downloaded by the web browser. The best known of these is Content-type:, which tells the browser what kind of file it is dealing with: a web page (Content-type: text/html), a GIF image (Content-type: image/gif), and so on.

A lesser-known header, Content-disposition:, can be used to tell the browser what should be done with the file. By default, the browser will "dispose" of the object by displaying it in the browser or launching an appropriate external player, whichever is appropriate. But when Content-disposition: is set to attachment; filename=movie.mpg, the browser instead displays a "open, save or cancel" dialog box, inviting the user to save the file under the name movie.mpg. The default may be to open the file in an external player, but that does not happen automatically, and the user is given a clear "save" option in both major browsers.

How can we take advantage of this? In PHP, it's simple. Instead of linking directly to the file as we normally would:


<a href="movie.mpg">See my MPEG movie</a>

We create a simple PHP "page" that delivers the file to the browser for us. Then we link to the .php file instead, and let it do the work of outputting the correct Content-disposition: and Content-type: headers, then the actual file.

I put the word "page" in quotes here for a reason! Although we'll use a file called movie.php, this PHP file won't be generating a page of HTML. It will be generating an MPEG movie - a different content type entirely. PHP can do that, sending images, audio and video directly to the browser, as long as PHP code appears at the very beginning of the file and immediately sets the appropriate Content-type: header. Otherwise, PHP defaults to generating an HTML page.
Here's the code for movie.php:


<?php
header('Content-disposition: attachment; filename=movie.mpg');
header('Content-type: video/mpeg');
readfile('movie.mpg');
?>

NOTE: this file must contain only PHP code and nothing else. No blank lines before or after. Blank lines before will cause PHP to default to Content-type: text/html. Blank lines after will be sent as additional "garbage" data after the video, potentially causing problems.

Yes, the PHP file has a .php extension. No, this doesn't create any problems when it comes time to save the actual movie. The filename of the PHP file doesn't matter to the web browser because:

1. We are specifying a video/mpeg content type. Web browsers usually don't care about file extensions - they care about content types. That's because file extensions are not universal standards (although, in practice, they are fairly consistent).

2. Our Content-disposition header suggests the filename we really want the movie to be saved as.

Notice that PHP's readfile() function does the job of reading the entire video and writing it to the web browser for us. In other languages this can take considerably more code. Conveniences like readfile() are the reason why PHP is the most popular dynamic web programming tool.

Once you've created movie.php, all you have to do is test it by accessing movie.php with your web browser. Note that you must install movie.php on a real web server that supports PHP, you can't test it as a local file. When you access movie.php, you should be prompted to save movie.mpg to disk, which is the desired result.

When you're finished, just link to movie.php directly to invite the user to download the file:


<a href="movie.php">Download my MPEG movie</a>

You might be tempted to change the Content-type: to application/octet-stream to prevent the "open" option from appearing at all. However, in my tests this was ineffective in both Internet Explorer and Firefox, and other browsers might see it as a reason to force a new file extension. So I don't recommend it.

Offering A Choice Of Downloads

The above example is intended as a nudge in the right direction for those who know a little bit of PHP. It's not difficult to extend it to do more.

But for those who don't know PHP, and to give PHP programmers additional food for thought, I have also written a simple PHP page that allows the user to choose and then download any of the files in a designated "downloads" directory. This code demonstrates two useful techniques:

1. How to list the contents of a directory with the opendir, readdir and closedir PHP functions.

2. How to do several different jobs in the same PHP page. This page responds in one way when a filename is present, and in another way when the user has not made a choice yet. In the first case, the "page" responds with the actual file - after making sure the user's selection is safe and that the file actually exists. In the second case, the page presents a menu of available files.

3. How to validate the user's choice of file with a regular expression, using the preg_match function. This is extremely important: if you accept a filename from the web browser (using forms and $_GET or $_POST, for example), and you fail to verify that the filename is safe and cannot possibly point outside of your download directory, crackers will be able to read any file on your system. Which will help them to crack your site. Which will get your site shut down. So pay attention to security and always validate any input you receive from the user.

4. How to create a form that submits right back to the same page. This is done by checking the global variable $_SERVER['SCRIPT_URL'], which always contains the path of the current page. For instance, if your page is http://www.example.com/download.php, then $_SERVER['SCRIPT_URL'] will be /download.php. Some programmers prefer $_SERVER['PHP_SELF'] because it contains the complete URL, but since this can be incorrect in some hosting environment and a link or form action doesn't need to be a complete URL, I see no reason to use it.

5. How to exit the page early with the exit function. This guards against accidentally sending unwanted data after the contents of the downloaded file.


<?php
// If the above is not the FIRST line in the file, it WON'T WORK!

// Directory where approved downloadable files live. If we use
// a relative path here (no /) then it is relative to the directory
// where this page is
$downloads = "downloads";

// Regular expression matching a safe filename.

// Filename must contain ONLY letters, digits, and underscores,
// with a single dot in the middle. NO slashes, NO double dots,
// NO pipe characters, nothing potentially dangerous.

// ^ matches the beginning of the string.
// \w matches a "word" character (A-Z, a-z, 0-9, and _ only).
// The "+" sign means "one or more."
// \. matches a single dot.
// And the final $ matches the end of the string.

$safeFilename = '/^\w+\.\w+$/';

// Now get the filename from the user
$filename = $_GET['filename'];

if ($filename == '') {
  # We don't have a filename yet, so generate
  # a file-picking menu page.
  menu();
} else {
  # We have a filename, so download the file.
  download();
}  

function menu()
{
  global $safeFilename, $downloads;
  $uri = $_SERVER['SCRIPT_URL'];
?>
<html>
<head>
<title>Download Menu</title>
</head>
<body>
<form method="GET" action="<?php echo $uri?>">
<select name="filename">
<?php
  $dir = opendir($downloads);
  if (!$dir) {
    die("Bad downloads setting");
  }
  while (($file = readdir($dir)) !== false) {
    // List only files with a safe filename
    if (preg_match($safeFilename, $file)) {
?>
<option value="<?php echo $file?>"><?php echo $file?></option>   
<?php
    }
  }
  closedir($dir);
?>
</select>
<br>
<input type="submit" name="download" value="Download Selected File">
</form>
</body>
</html>
<?php
}

function download()
{
  global $filename, $safeFilename, $downloads;
  // MAKE SURE THE FILENAME IS SAFE!
  if (!preg_match($safeFilename, $filename)) {
    error("Bad filename");
  }
  // Now make sure the file actually exists
  if (!file_exists("$downloads/$filename")) {
    error("File does not exist");
  }

  header("Content-disposition: attachment; filename=$filename");
  header("Content-type: application/octet-stream");
  readfile("$downloads/$filename");
  // Exit successfully. We could just let the script exit
  // normally at the bottom of the page, but then blank lines
  // after the close of the script code would potentially cause
  // problems after the file download.
  exit(0);
}

function error($message) {
  // You might want to output a more attractive error
  // message page - hey, you're the web designer
?>
<html>
<head>
<title><?php echo $message?></title>
</head>
<body>
<h1><?php echo $message?></h1>
</body>
</html>
<?php
}
?>


Alternatives to PHP

While PHP is supported by most serious web hosts and requires the least work to set up, there are alternatives. In particular, Apache's mod_headers module is a clever solution that doesn't tie up a PHP process for the duration of the download, decreasing the load on your web server. You can learn more about this from badpen tech's excellent article using Apache mod_headers to change downloaded filenames.

Internet Information Server users can solve the problem in a similar way, and also have the option of using ASP.NET (or PHP, for that matter). For more information, see the Microsoft Support article How To Raise a "File Download" Dialog Box for a Known MIME Type.

Share |

Legal Note: yes, you may use sample HTML, Javascript, PHP and other code presented above in your own projects. You may not reproduce large portions of the text of the article without our express permission.

Got a LiveJournal account? Keep up with the latest articles in this FAQ by adding our syndicated feed to your friends list!


Follow us on Twitter | Contact Us

Copyright 1994-2014 Boutell.Com, Inc. All Rights Reserved.