Armed with the code that accepts file uploads and stores them in a database, you're halfway home. But you still need to be able to pull that data out of the database to use it. For our purposes, this will mean sending the file to a requesting browser.
Once again, this turns out to be a relatively straightforward process. We simply retrieve the data for the requested file from the database and send it on to the Web browser. The only tricky part is to send the browser information about the file, including:
the file size (so that the browser can display accurate download progress information to the user)
the file type (so that the browser knows what to do with the data it receives, e.g. display it as a Web page, a text file, an image, or offer to save the file)
the file name (if we don't specify it, the browser will assume all files downloaded from our script have the same file name as the script)
All this information is sent to the browser using HTTP headers—special lines of information that precede the transmission of the file data itself. Sending HTTP headers via PHP is quite easy using the header function, but as headers must be sent before plain content, any calls to this function must come before anything is output by your script.
The file size is specified with a content-length header:
header('content-length: ' . strlen($filedata));
strlen is a built-in PHP function that returns the length of the given string. Since binary data is just a string of bytes as far as PHP is concerned, you can use this function to count the length in bytes of the file data.
The file type is specified with a content-type header:
header("content-type: $mimetype");
Finally, the file name is specified with a content-disposition header:
header("content-disposition: inline; filename=$filename");
Use the script below to fetch a file with a given ID from the database and send it to the browser:
$sql = "SELECT FileName, MimeType, FileData FROM filestore WHERE ID = '$id'"; $result = @mysql_query($sql); if (!$result) die('Database error: ' . mysql_error()); $file = mysql_fetch_array($result); if (!$file) die('File with given ID not found in database!'); $filename = $file['FileName']; $mimetype = $file['MimeType']; $filedata = $file['FileData']; header("content-disposition: inline; filename=$filename"); header("content-type: $mimetype"); header('content-length: ' . strlen($filedata)); echo($filedata);
One final trick we can add to this code is to allow a file to be downloaded, instead of viewed, if the user so desires. Web standards suggest that the way to do this is to send a content-disposition of attachment instead of inline. Here's the modified code, which checks if the variable $action equals 'dnld', which would indicate that this special file type should be sent:
$sql = "SELECT FileName, MimeType, FileData FROM filestore WHERE ID = '$id'"; $result = @mysql_query($sql); if (!$result) die("Database error: " . mysql_error()); $file = mysql_fetch_array($result); if (!$file) die('File with given ID not found in database!'); $filename = $file['FileName']; $mimetype = $file['MimeType']; $filedata = $file['FileData']; $disposition = 'inline'; if ($action == 'dnld') $disposition = 'attachment'; header("content-disposition: $disposition; filename=$filename"); header("content-type: $mimetype"); header('content-length: ' . strlen($filedata)); echo($filedata);
Unfortunately, many browsers do not respect the content-disposition header. Netscape 4, Internet Explorer 5, and all Opera browsers, for example, will decide what to do with a file based on the content-type header.
To ensure the correct behaviour in as many browsers as possible, we can use the built-in $_SERVER['HTTP_USER_AGENT'] variable to identify the browser in use. Opera 7 and Internet Explorer 5 browsers can be coerced into displaying the download dialogue by sending a made-up content-type of application/x-download:
if ($action == 'dnld') { $disposition = 'attachment'; if (strpos($_SERVER['HTTP_USER_AGENT'],'MSIE 5') or strpos($_SERVER['HTTP_USER_AGENT'],'Opera 7')) $mimetype = 'application/x-download'; }
The strpos function used here is a built-in PHP function that takes two strings and searches for the second string within the first. If it doesn't find it, it returns FALSE, but if it does, it returns the position of the second string in the first as an integer.
Older versions of Opera and Netscape 4 are best left to their own devices; when you mess with the content-type header, these browsers display binary files as plain text in the browser window and display an error message, respectively.