Build a File Download Script in Perl

Erick Jones
Erick Jones
Share

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!

Frequently Asked Questions (FAQs) about Perl File Download Script

How can I use Perl to download a file from a URL?

To download a file from a URL using Perl, you need to use the LWP::Simple module. This module provides functions to fetch web pages. Here is a simple example:

use LWP::Simple;
getstore('http://example.com/file.txt', 'localfile.txt');
In this example, ‘http://example.com/file.txt‘ is the URL of the file you want to download, and ‘localfile.txt’ is the name you want to give to the file on your local system.

How can I handle errors when downloading files with Perl?

When downloading files using Perl, it’s important to handle potential errors. The getstore function from the LWP::Simple module returns a HTTP status code. You can check this code to see if the download was successful:

use LWP::Simple;
my $url = 'http://example.com/file.txt';
my $file = 'localfile.txt';
my $status = getstore($url, $file);
if(is_success($status)) {
print "File downloaded successfully\n";
} else {
print "Failed to download file: $status\n";
}
In this example, the is_success function checks if the status code indicates a successful download.

How can I download multiple files using Perl?

If you need to download multiple files, you can put the URLs in an array and use a loop to download each one:

use LWP::Simple;
my @urls = ('http://example.com/file1.txt', 'http://example.com/file2.txt');
foreach my $url (@urls) {
my $file = $url;
$file =~ s/.*\///; # extract file name from URL
my $status = getstore($url, $file);
if(is_success($status)) {
print "Downloaded $file successfully\n";
} else {
print "Failed to download $file: $status\n";
}
}
In this example, the script downloads each file in the @urls array and saves it under the same name on the local system.

How can I download a file in the background using Perl?

Perl doesn’t natively support downloading files in the background. However, you can achieve this by using the fork function to create a new process:

use LWP::Simple;
my $url = 'http://example.com/file.txt';
my $file = 'localfile.txt';
my $pid = fork();
if($pid) {
# parent process
print "Download started in background process $pid\n";
} elsif($pid == 0) {
# child process
my $status = getstore($url, $file);
if(is_success($status)) {
print "File downloaded successfully\n";
} else {
print "Failed to download file: $status\n";
}
exit(0);
} else {
die "Failed to fork: $!\n";
}
In this example, the fork function creates a new process that downloads the file, allowing the main process to continue running.

How can I download a file from a website that requires login using Perl?

To download a file from a website that requires login, you can use the WWW::Mechanize module. This module provides functions to navigate websites, fill out forms, and handle cookies:

use WWW::Mechanize;
my $mech = WWW::Mechanize->new();
$mech->get('http://example.com/login');
$mech->submit_form(
form_number => 1,
fields => { username => 'your_username', password => 'your_password' },
);
$mech->get('http://example.com/file.txt', ':content_file' => 'localfile.txt');
In this example, the script first navigates to the login page, fills out the login form, and then downloads the file. The ‘:content_file’ option tells the get function to save the content to a file.

How can I download a file from a website that uses JavaScript using Perl?

Downloading files from websites that use JavaScript can be challenging with Perl, as the LWP::Simple and WWW::Mechanize modules don’t support JavaScript. However, you can use the WWW::Mechanize::Firefox module, which uses the Firefox browser to handle JavaScript:

use WWW::Mechanize::Firefox;
my $mech = WWW::Mechanize::Firefox->new();
$mech->get('http://example.com/file.txt');
$mech->save_content('localfile.txt');
In this example, the script uses Firefox to download the file. Note that this requires Firefox to be installed on your system and the MozRepl extension to be enabled.

How can I download a file from a website that uses HTTPS using Perl?

To download a file from a website that uses HTTPS, you can use the LWP::UserAgent module together with the LWP::Protocol::https module:

use LWP::UserAgent;
my $ua = LWP::UserAgent->new(ssl_opts => { verify_hostname => 1 });
my $response = $ua->get('https://example.com/file.txt');
if($response->is_success) {
open(my $fh, '>', 'localfile.txt') or die "Could not open file 'localfile.txt' $!";
print $fh $response->decoded_content;
close $fh;
print "File downloaded successfully\n";
} else {
print "Failed to download file: ", $response->status_line, "\n";
}
In this example, the script uses LWP::UserAgent to download the file. The ssl_opts option is used to enable SSL certificate verification.

How can I download a file from a FTP server using Perl?

To download a file from a FTP server, you can use the Net::FTP module:

use Net::FTP;
my $ftp = Net::FTP->new("ftp.example.com", Debug => 0)
or die "Cannot connect to ftp.example.com: $@";
$ftp->login("anonymous",'-anonymous@')
or die "Cannot login ", $ftp->message;
$ftp->get("file.txt", "localfile.txt")
or die "get failed ", $ftp->message;
$ftp->quit;
In this example, the script connects to the FTP server, logs in, downloads the file, and then disconnects.

How can I download a file with a progress bar using Perl?

To download a file with a progress bar, you can use the LWP::UserAgent module together with the Term::ProgressBar module:

use LWP::UserAgent;
use Term::ProgressBar;
my $ua = LWP::UserAgent->new();
my $response = $ua->get('http://example.com/file.txt', ':content_cb' => sub {
my ($data, $response, $protocol) = @_;
state $progress = Term::ProgressBar->new({name => 'Download', count => $response->content_length, ETA => 'linear'});
state $received = 0;
$received += length($data);
$progress->update($received);
});
if($response->is_success) {
open(my $fh, '>', 'localfile.txt') or die "Could not open file 'localfile.txt' $!";
print $fh $response->decoded_content;
close $fh;
print "File downloaded successfully\n";
} else {
print "Failed to download file: ", $response->status_line, "\n";
}
In this example, the script uses a callback function to update the progress bar as the file is downloaded.

How can I download a file and save it to a specific directory using Perl?

To download a file and save it to a specific directory, you can specify the directory in the file name:

use LWP::Simple;
getstore('http://example.com/file.txt', '/path/to/directory/localfile.txt');
In this example, ‘/path/to/directory/localfile.txt’ is the path and name of the file on your local system.