Build a File Download Script in Perl

I have a Website that presents photographs for visitors to download. I used to simply have the files display in the browser window so that visitors could right-click and save them to their computers. However, 2 problems soon became apparent with this system.

First, some visitors didn’t understand exactly how to download the files to their computers. Second, I discovered that some Websites had direct links to my photographs, so that my images appeared to be part of the content of these sites.

Besides being impolite, this so called "hot" linking costs me money, as my Web host charges for the bandwidth I use. My solution was to create a Perl script to mask the URLs of the files on my Website.

In this tutorial I’m going to show you how to build a simple download script using Perl. The example we’ll go through will mask the download URL, record each download to a log file, and present the visitor with a “save as…” dialog box.

Requirements

For this script to work, all your photographs (or any other files that you’re offering as a download) must be located in one directory. The script also has to be on the same server as the directory of the files to be downloaded.

Let’s Get Started!

Begin your script by adding the shebang line. The shebang line begins with the pound character, followed by an exclamation point and the path to the server’s Perl interpreter. The “-wT” is optional, but you should use it because it turns on warnings and checks for tainted data.

#!/usr/bin/perl -wT

Most Linux/Unix servers have the Perl interpreter in this location. On a Windows machine, a common shebang line is "#!C:perlbinperl.exe" (note the slashes are backslashes in this case). If you’re not sure of your server’s operating system or the path to Perl, check with your Web host.

The next 2 lines specify that we want to use the CGI module, and that we want to display any fatal errors in the browser window.

use CGI ':standard'; 
use CGI::Carp qw(fatalsToBrowser);

Now, we’ll declare 3 variables:

my $files_location;  
my $ID;  
my @fileholder;

Next, we want to specify the server path to the files to be downloaded. The value must be an absolute reference to the folder on the server. This folder can be outside the server’s document root directory.

$files_location = "/home/files/download/photos";

If your server is a Windows machine, the path would use backslashes, like this:

$files_location = "C:filestodownload";
Now, For The Good Stuff

This next line is where the fun begins. We have already declared a variable called “$ID“. Now we need to specify its value. This variable holds the name of the file to be downloaded. Rather than directly assigning its value, however, we want its value to be passed from the browser. There are a couple of ways to pass the file name to this variable, but, for now we’ll just specify that the value will come from the browser. Note that at the end of this article you’ll be able to download complete source code for this script, as well as an HTML form that you can use to call the script.

$ID = param('ID');  

Now, suppose the script is called but no file name is specified. We don’t want the script to simply display a blank screen. Instead, we want to tell our visitors that a file name must be specified. To do this, we’ll add the following lines:

if ($ID eq '') {  
print "Content-type: text/htmlnn";  
print "You must specify a file to download.";  
} else {

If a file name is specified, we want to open that file and assign its contents to a temporary placeholder called “@fileholder“. If there are any errors during the opening or closing of the file, we’ll need to display an error message.

open(DLFILE, "<$files_location/$ID") || Error('open', 'file');  
@fileholder = <DLFILE>;  
close (DLFILE) || Error ('close', 'file');

Logging The Download

The next 3 lines are optional, but they’re useful for keeping track of the number of times each file is downloaded. The 3 lines simply open the log file called “dl.log” (or display an error), print the file name, and close the log file. As before, you must specify the absolute path on your server.

open (LOG, ">>/home/files/dl.log") || Error('open', 'file');  
print LOG "$IDn";  
close (LOG);

For this to work, 2 things must be in place. First, the directory must already exist on your server. Second, the file “dl.log” must exist in the directory and be writable. To change the file permissions on a Linux/Unix server, you must use the command chmod as follows: chmod 777 dl.log/.

Sending The File

Now, we want to send the file to the user. What if the file is an image, PDF, or some other file type that the browser recognizes? Even though we’ve masked the URL, the visitor’s browser will display the file. To coax the browser into displaying the "save as…" dialog box, we need to fake the file type in the header that’s sent:

print "Content-Type:application/x-downloadn";   
print "Content-Disposition:attachment;filename=$IDnn";  
print @fileholder  
}
Some Error Handling

We have directed the script to call a subroutine if something should go wrong when the file is opened, or the download is recorded to the log file. This next section of code displays the error in the browser window:

  sub Error {  
       print "Content-type: text/htmlnn";  
   print "The server can't $_[0] the $_[1]: $! n";  
   exit;  
 }
Done! But How Does It Work?

To test it out, upload the script to your cgi-bin directory and chmod it to 755.

Now, how do we call this script? There are a couple of ways to do this. First, you can create a hyperlink in your Web page that reads:

<a href="/cgi-cin/script.cgi?ID=file.zip">Download File</a>

Or, you can create a form that includes a field (checkbox, dropdown menu, etc.) called ID, and an action of: <http://www.yourserver.com/cgi-bin/script.cgi> /cgi-bin/script.cgi

Source Code

Below is a sample HTML Webpage that you can use as a template. This sample provides a dropdown menu through which the user can select the file they'd like to download.

<html><head>  
<title>Download Page</title>  
</head>  
<body>  
<form action="/cgi-bin/script.cgi">  
<p>  
 <select size="1" name="ID">  
 <option value="0001.jpg">File One  
 </option>  
 <option value="file.zip">File Two  
 </option>  
</p>  
 </select>  
<input type="submit" value="Submit" name="B1">  
</form>  
</body>  
</html>

Here's the complete script:

#!/usr/bin/perl -wT  
 
use CGI ':standard';  
use CGI::Carp qw(fatalsToBrowser);  
 
my $files_location;  
my $ID;  
my @fileholder;  
 
$files_location = "/usr50/home/webdata/photos";  
 
$ID = param('ID');  
 
if ($ID eq '') {  
print "Content-type: text/htmlnn";  
print "You must specify a file to download.";  
} else {  
 
open(DLFILE, "<$files_location/$ID") || Error('open', 'file');  
@fileholder = <DLFILE>;  
close (DLFILE) || Error ('close', 'file');  
 
open (LOG, ">>/usr50/home/webdata/public_html/test.log") || Error('open', 'file');  
print LOG "$IDn";  
close (LOG);  
 
print "Content-Type:application/x-downloadn";  
print "Content-Disposition:attachment;filename=$IDnn";  
print @fileholder  
}

Don't forget to download the code in full!

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Andrew

    Looks really nice, and simple. Thank you a lot ;)

  • Joe

    Why am I getting the error “Error – Bad Header” when I try this code?