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:
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.
<?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.
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.
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.
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-2012 Boutell.Com, Inc. All Rights Reserved.
