dO-BoY

Managing File Downloads

My friends’ band Surrender is heading off on a European tour. (Yes, I’m jealous.) In addition to their other merchandise, they wanted to be able to include MP3 downloads with their vinyl records, or even just sell the downloads directly. This is a service United Record Pressing offers but it’s a little, ah, invasive, and not available a la carte, as it were, if they don’t also press your record.

My band played a show with The Measure [SA] this year, and their record came with a download option as well, and it’s pretty DIY. But Surrender wanted something DIYer, with more control over things. I had been intrigued by the VinylDownloads.com style, and figured it wouldn’t be too hard to build something similar, at least for one band. So I tried.

First is the db: one table with a list of unique download keys and how many times each was used:

+----------+-------------+------+-----+-------------------+
| Field    | Type        | Null | Key | Default           |
+----------+-------------+------+-----+-------------------+
| id       | int(11)     | NO   | PRI | NULL              |
| key      | varchar(16) | NO   |     | 0                 |
| used     | tinyint(1)  | NO   |     | 0                 |
| mod_date | timestamp   | NO   |     | CURRENT_TIMESTAMP |
+----------+-------------+------+-----+-------------------+

I generated the keys–16 alphanumerics–and the MySQL commands for inserting each one into the table in a PHP script. Here are the lines of interest:

$key = substr(md5(rand()),0,16);
$query = "INSERT INTO file_keys (`key`,`used`,`add_date`) VALUES('$key','0',NOW());";
mysql_query($query,$DB_CONNECTION);

I didn’t explicitly check for uniqueness of the key values. What can I say? I have faith in MD5, even if collisions have been found.

The last part is getting the file to download without actually providing direct access to it. PHP makes this pretty easy, although there are some gotchas related to IE and Safari:

// Stuff only IE seems to need
header("Pragma: public");
header("Expires: 0");
header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
header("Cache-Control: private",false); 

// We'll be outputting a ZIP
header("Content-type: application/zip");
// It will be called surrender.zip
// (double-quote file name in header or Safari will be sad)
header('Content-Disposition: attachment; filename="surrender.zip"');

header("Content-Transfer-Encoding: binary");
header("Content-Length: " . filesize($DL_FILE_PATH) ); 

// The ZIP source is in $DL_FILE_PATH
readfile($DL_FILE_PATH);
The problem is the step between checking the DB and allowing/disallowing the download. How do you authorize the download page conditionally, without passing along information easily intercepted by nefarious types? A session variable seemed reasonable–theoretically hackable, but kudos to anyone who does it.

So after checking for whether the download key can be used–each one can be used up to three times–a session variable is set:

$_SESSION['downloadfile'] = 'secret';

Another issue is how to message the user about what’s happening. If a code is valid, we redirect to the “downloading now” page, or else the user sees the “sorry” page. To actuate the download, I went back to some 20th century web technology: a META tag to “refresh” the page to the page that does the download:

<meta http-equiv="refresh" content="2;url=getfile.php">

This sends the user to getfile.php, which doesn’t actually display in the browser but just starts a file download (solving the problem of triggering both the download and a message to the user to let him know what’s going on). The download happens and everybody’s happy.

The last step is to unset the session variable:

$_SESSION['downloadfile'] = '';

I’m working on more generic code to share, or maybe this would be a good SourceForge project?

Cats: experiment