Simple Perl Uploader with Progress Bar

November 25, 2009 by Noah

This is a re-do of my previous blog post about Perl upload progress bars - my previous approach was completely wrong. By the time $q->upload(); is used, the file has already been received and stored in a temporary location, and so the "progress bar" in this case is really just gauging how fast the server can copy the file from one place to another on its hard drive.

So this post is how to really do a real working file uploader progress bar in Perl.

The basic steps required to do this include:

  • Set an onSubmit handler on your form that will set some ajax requests running in the background. The ajax will continuously poll your CGI script to see how the upload is going.
  • The upload CGI script needs to set an "upload hook" that calls repeatedly as the file is being received by the server. This hook can store the current progress of the upload somewhere, so that when the ajax pings it for the status, it can report the status.
  • Besides that everything else works the same as for a non-progress-bar upload. You can still do $q->upload(); and everything like before.

The source code needed for this is still amazingly short and concise, compared to the source codes you'll get when you download solutions from elsewhere.

Implementing this doesn't require any special Apache handlers or mod_perl or anything fancy like that.


  • upload.html

<!DOCTYPE html>
<title>Upload Test</title>
<style type="text/css">
body {
 background-color: #FFFFFF;
 font-family: Verdana,Arial,sans-serif;
 font-size: small;
 color: #000000
#trough {
 border: 1px solid #000000;
 height: 16px;
 display: block;
 background-color: #DDDDDD
#bar {
 background-color: #0000FF;
 background-image: url("blue-clearlooks.png");
 border-right: 1px solid #000000;
 height: 16px

<h1>File Upload Test</h1>

<div id="progress-div" style="display: none; width: 400px; margin: auto">
                <legend>Upload Progress</legend>
                <div id="trough">
                        <div id="bar" style="width: 0%"></div>
                Received <span id="received">0</span>/<span id="total">0</span> (<span id="percent">0</span>%)

<div id="upload-form" style="display: block; width: 600px; margin: auto">
                <legend>Upload a File</legend>
                <form name="upload" method="post" action="upload.cgi" enctype="multipart/form-data" onSubmit="return startUpload()" id="theform">
                <input type="hidden" name="do" value="upload">

                <table border="0" cellspacing="0" cellpadding="2">
                                <td align="left" valign="middle">
                                        Session ID<span style="color: #FF0000">*</span>:
                                <td align="left" valign="middle">
                                        <input type="text" size="40" name="sessid" id="sessid" readonly="readonly">
                                <td align="left" valign="middle">
                                <td align="left" valign="middle">
                                        <input type="file" name="incoming" size="40">

                <input type="submit" value="Upload It!"><p>

                <span style="color: #FF0000">*</span> Randomly generated by JavaScript. In practice this would be
                randomly generated by server-side script and "hard-coded" into the HTML you see on this page.

<div id="debug"></div>

<script type="text/javascript">
// a jquery-like function, a shortcut to document.getElementById
function $(o) {
        return document.getElementById(o);

// called on page load to make up a session ID (in real life the session ID
// would be made up via server-side script and "hard-coded" in the HTML received
// by the server, thus it wouldn't require javascript at all)
function init() {
        // Make up a session ID.
        var hex = [ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
        "A", "B", "C", "D", "E", "F" ];
        var ses = "";

        for (var i = 0; i < 8; i++) {
                var rnd = Math.floor(Math.random()*16);
                ses += hex[rnd];

        $("sessid").value = ses;

        // we set the form action to send the sessid in the query string, too.
        // this way it's available inside the CGI hook function in a very easy
        // way. In real life this would probably be done better.
        $("theform").action += "?" + ses;
window.onload = init;

// This function is called when submitting the form.
function startUpload() {
        // Hide the form.
        $("upload-form").style.display = "none";

        // Show the progress div.
        $("progress-div").style.display = "block";

        // Begin making ajax requests.
        setTimeout("ping()", 1000);

        // Allow the form to continue submitting.
        return true;

// Make an ajax request to check up on the status of the upload
function ping() {
        var ajax = new XMLHttpRequest();

        ajax.onreadystatechange = function () {
                if (ajax.readyState == 4) {
        };"GET", "upload.cgi?do=ping&sessid=" + $("sessid").value + "&rand=" + Math.floor(Math.random()*99999), true);

// React to the returned value of our ping test
function parse(txt) {
        $("debug").innerHTML = "received from server: " + txt;

        var parts = txt.split(":");
        if (parts.length == 3) {
                $("received").innerHTML = parts[0];
                $("total").innerHTML = parts[1];
                $("percent").innerHTML = parts[2];
                $("bar").style.width = parts[2] + "%";

        // Ping again!
        setTimeout("ping()", 1000);


  • upload.cgi

#!/usr/bin/perl -w

use strict;
use warnings;
use CGI;
use CGI::Carp "fatalsToBrowser";

# Make a file upload hook.
my $q = new CGI (\&hook);

# This is the file upload hook, where we can update our session
# file with the dirty details of how the upload is going.
sub hook {
        my ($filename,$buffer,$bytes_read,$file) = @_;

        # Get our sessid from the form submission.
        my ($sessid) = $ENV{QUERY_STRING};
        $sessid =~ s/[^A-F0-9]//g;

        # Calculate the (rough estimation) of the file size. This isn't
        # accurate because the CONTENT_LENGTH includes not only the file's
        # contents, but also the length of all the other form fields as well,
        # so it's bound to be at least a few bytes larger than the file size.
        # This obviously doesn't work out well if you want progress bars on
        # a per-file basis, if uploading many files. This proof-of-concept only
        # supports a single file anyway.
        my $length = $ENV{'CONTENT_LENGTH'};
        my $percent = 0;
        if ($length > 0) { # Don't divide by zero.
                $percent = sprintf("%.1f",
                        (( $bytes_read / $length ) * 100)

        # Write this data to the session file.
        open (SES, ">$sessid.session");
        print SES "$bytes_read:$length:$percent";
        close (SES);

# Now the meat of the CGI script.
print "Content-Type: text/html\n\n";

my $action = $q->param("do") || "unknown";
if ($action eq "upload") {
        # They are first submitting the file. This code doesn't really run much
        # until AFTER the file is completely uploaded.
        my $filename = $q->param("incoming");
        my $handle   = $q->upload("incoming");
        my $sessid   = $q->param("sessid");
        $sessid     =~ s/[^A-F0-9]//g;
        $filename =~ s/(?:\\|\/)([^\\\/]+)$/$1/g;

        # Copy the file to its final location.
        open (FILE, ">./files/$filename") or die "Can't create file: $!";
        my $buffer;
        while (read($handle,$buffer,2048)) {
                print FILE $buffer;
        close (FILE);

        # Delete the session file.

        # Done.
        print "Thank you for your file. <a href=\"files/$filename\">Here it is again.</a>";
elsif ($action eq "ping") {
        # Checking up on the status of the upload.
        my $sessid = $q->param("sessid");
        $sessid =~ s/[^A-F0-9]//g;

        # Exists?
        if (-f "./$sessid.session") {
                # Read it.
                open (READ, "./$sessid.session");
                my $data = <READ>;
                close (READ);
                print $data;
        else {
                print "0:0:0:error session $sessid doesn't exist";
else {
        print "0:0:0:error invalid action $action";

You can download my full proof-of-concept test below:

Notice: this code is called "proof of concept"; it is NOT production-ready code. You should NOT download this if all you want is a complete plug-and-play solution you can quickly upload to your web server to get file uploading to work. I wrote this code only to show how to make a file uploader in the simplest way possible; this is useful for developers who only needed to know how this is done and who will write the code themselves to develop their production-ready file uploader.

If you want to treat this as a plug-and-play solution, I'm not your tech support about it. The code was never meant to be secure or useful to allow the general public to upload files through it. Session IDs are made up client side for example which is a bad idea in real use case scenarios, etc.



There are 44 comments on this page. Add yours.

Avatar image
Anonymous posted on November 29, 2009 @ 03:06 UTC

Neat trick! I'll have to use this some time.

Avatar image
moosa maher posted on January 19, 2010 @ 22:50 UTC

that a perfect solution for most of developer headache. thank for your honesty to share. i always love simple non-commercial script. can you help for config my page? i used <?php session_start(); ?> at top of upload.html and echo session_id() in value of <input id='sessid', but got received from server: 0:0:0:error session 7647507949712654928 doesn't exist. what could i do? thanks alot friend.

Avatar image
Noah (@kirsle) posted on January 19, 2010 @ 22:57 UTC

In my code the session ID is used in two places. In the JavaScript function init(), it sets the session ID in the text box you see on the page, and also modifies the form's action to add a "?" followed by the session ID.

When you click the upload button and the ajax starts polling it to see the progress, it sends the session ID from the text box. So, if you're replacing the session ID stuff with PHP calls, make sure you put the session ID in both places. The Perl script receiving the upload only knows about the session ID from the form's action, and if the JavaScript is using a different one when polling for the progress the Perl script won't be able to find it.

In any event, the code I have on my site is just a proof of concept, you'll eventually want to handle the session ID stuff in a better way than that.

Avatar image
free checkers online posted on January 25, 2010 @ 05:25 UTC

i think.XUpload is an advanced upload of progress bar indicator for web based file uploads written and Perl and One of my major problems is attempting to have long-running processes initiated via a web browser and all the things that go with it: making sure the process works, not using the many server resources at one time, working within a basically stateless environment

Avatar image
Mick posted on July 31, 2010 @ 01:50 UTC

I got the same error,

received from server: 0:0:0:error session CE54B75E doesn't exist

Avatar image
Absolute posted on August 9, 2010 @ 18:04 UTC

I haven't (yet) setup the script to use PHP session, this may or may not be the root cause.

Issue: When uploading a file, the progress bar will pause randomly, this happens about 75% of the time. It appears that the .session file is not longer being updated.

What could cause this?

Avatar image
Roland posted on September 3, 2010 @ 21:22 UTC

Kirsle - you are amazing! That was EXACTLY the script I was searching for. I think uber-uploader is an excellent script for uploading and following the upload progress - but it is somewhat overkill. Especially what i don't like is that you are using a chimere of two languages (PHP and Perl). Your script is truly amazing because it does the job AND it is incredibly small! (which leaves a lot of options for customization) Great job!

Avatar image
orton posted on October 16, 2010 @ 21:49 UTC

Best uploader ever. Still is any way to show the upload speed? Thanks

Avatar image
Noah (@kirsle) posted on October 17, 2010 @ 00:33 UTC

Calculating upload speed just involves checking how much has been uploaded at two points in time and comparing them.

For example, if 100 KB was uploaded at one point and then the app checks how much was uploaded 5 seconds later, and it's now 200 KB, then it means 100 KB was uploaded in 5 seconds, or 20 KB/s (100 / 5).

Avatar image
orton posted on October 17, 2010 @ 18:33 UTC

The java script checks every 5 seconds? how can i set the time i want. Thanks

Avatar image
Noah (@kirsle) posted on October 17, 2010 @ 18:40 UTC

Just edit the JavaScript to increase the interval. Hint: look for the setTimeout lines, and change the number from 1000 ms (1 second) to whatever you want.

Avatar image
orton posted on October 17, 2010 @ 18:57 UTC

Ok i will try now.

Avatar image
orton posted on October 17, 2010 @ 19:38 UTC

I tryed but im not good at javascript:( here is the page

maybe you can help me

Avatar image
Carsten posted on November 25, 2010 @ 21:19 UTC


can i use the script with more than one files ?

Avatar image
Errror posted on December 11, 2010 @ 20:04 UTC

Krisle please help me always have error: how i can make?

received from server: 0:0:0:error session C6A77578 doesn't exist

your script!

Avatar image
Noah (@kirsle) posted on December 11, 2010 @ 20:25 UTC

My code isn't a complete application ready to be downloaded and installed on your server. Instead it's just a set of proof of concept code, demonstrating the bare minimum that's required to get a file uploader working in Perl. I wrote it only because it's hard to find this information online, and anyone who's ever had to dissect an existing file uploader application would know that the existing solutions are massive and bloated and it's hard to find out how they actually work.

I wish people would stop assuming my code is plug-and-play and ready to go; it's not; all it does is show you how to make a file uploader. It's not very secure at all and the fact that session IDs are generated client side is what many web developers would call "very bad practice" in terms of web security.

Having said all that, if you can't resolve this error on your own, you should probably find an existing plug-and-play solution; mine is useful to developers who want to write their own code, and developers would know how to debug errors like this.

Avatar image
Yashkin posted on December 11, 2010 @ 20:45 UTC

Dear Kirsle!

Thank your for your reply! Im understand what this not plug and play script.

And ask your help me and find mistake.

Now i see Sub hook, making later than sub ping? why?

And on server file $sessid.session making to late.

Avatar image
Noah (@kirsle) posted on December 11, 2010 @ 20:49 UTC

The Perl CGI module will call the hook sub for each chunk of data received from the user. When the upload first begins, the .session file won't exist yet because the hook sub hasn't created it yet. So you should only see that error message for a couple seconds at most until the file starts being received by the server.

Even when the JavaScript gets that error, it still continues pinging the uploader to check the status again.

Also note that this won't work well at all on Safari and Chrome; on these browsers, all ajax requests get blocked as soon as the page begins trying to load another page. There's a workaround for this by submitting the form into an iframe on the page, via , so that the main page doesn't have to go away so the ajax requests aren't stopped. The result page that loads in the iframe, then, would do a parent.window.location="..." to send the user to the results page.

Avatar image
Evgeny posted on December 11, 2010 @ 21:15 UTC

Im understand about Safari and Chrome. But im used Exporer and Mozilla.

1) No im comment: #unlink("./$sessid.session");

2) And server have: 70A26E90.session

only when file is UPLOADED.

all files. 1444548:1444942:100.0

But when file uploading session file is not making. He not have on server.

3) Im not understand why is sub hook make file only in last time.

Avatar image
Noah (@kirsle) posted on December 11, 2010 @ 21:19 UTC

It may be due to a special server configuration. My code was tested on a default Apache install on a Fedora server. If the hook sub is only being called once the server has received 100% of the file, then I'd look into the server's configuration; it would seem to be buffering the file somewhere and not invoking the CGI script until it already has the entire file.

Avatar image
yashkin posted on December 12, 2010 @ 06:16 UTC

Thank Your!

Im test on another serever every is ok, i think is problem with this server.

Avatar image
T. posted on December 15, 2010 @ 03:18 UTC

Excellent script. I'm learning Perl and this went a long ways towards that end.

One issue I found is that this script does not treat binary files very well on a Windows (IIS) host.

I changed this bit of code...

# Copy the file to its final location.
open (FILE, ">X:/upload/$filename") or die "Can't create file: $!";
binmode FILE;
while (<$handle>) {print FILE;}
close FILE;

...and it now works on IIS (tested).

Near as I can tell, this method would be good practice on any system and regardless of file types being dealt with.

Thanks again.


Avatar image
Noah (@kirsle) posted on December 15, 2010 @ 03:20 UTC

Indeed. I usually always put binmode on my filehandles, but as this was just a test script and I was only running it on a Linux server (where binmode doesn't matter), I didn't bother to write that extra line of code.

Thanks for pointing it out though; shows your Perl skills are coming along pretty nicely too.

Avatar image
Rudolf posted on February 3, 2011 @ 00:43 UTC

You have done a great job!

Your solution is very compact and does the job perfectly.

Congratulations and THANK YOU!

Avatar image
Matthew posted on March 8, 2011 @ 15:19 UTC

Fantastic! Thank You.

What is

"&rand=" + Math.floor(Math.random()*99999)

for in function ping()?

Avatar image
Noah (@kirsle) posted on March 8, 2011 @ 17:57 UTC

The web browser will cache the results of an ajax request if the request URI is the same each time. Adding a random number to each request prevents the caching and forces the browser to hit the server again.

Avatar image
Matthew posted on March 8, 2011 @ 22:17 UTC


Avatar image
Matthew posted on March 10, 2011 @ 09:49 UTC

Hi There,

I sent a file 140MB and the progress stopped at 40MB, the upload was still going and the session file still being updated but no more ajax pings were being generated by the browser (IE8)

I thought maybe the ping was being seen as a TCP flood attack so I turned off TCP flood detection at both ends and set to 3000 ms This time the browser stopped pinging at 70MB, I ran a sniffer and there were no ping requests coming from the client.

I increased to 10000 ms and got the whole file through OK :)

Do you know if ajax has a limit (quantity or time)?

Avatar image
Matthew posted on March 10, 2011 @ 10:33 UTC

Just a couple more points for anyone doing large uploads.

    # Copy the file to its final location.
    open (FILE, ">./files/$filename") or die "Can't create file: $!";
    my $buffer;
    while (read($handle,$buffer,2048)) {
            print FILE $buffer;
    close (FILE);

This is very slow and happens after the upload hits 100% leaving the client wondering what is going on now?

At the start of the script

my $q = new CGI ( \&amp;hook, undef, 0 );

will tell CGI not to bother saving the data

I then add this in sub hook {

# Write file chunk to a bin file.
open (NEW, ">>$sessid.bin");
binmode NEW;
print NEW $buffer;
close (NEW);

so that the file is saved as it arrives

I then and replace the copy code above with

rename ("$sessid.bin", "./files/$filename");

On a Windows system this passes the task to the OS and is instant even if the destination is on a different drive or NAS!


close $handle;

Will ensure that the CGITemp file gets deleted, in my case it's a 0 byte file anyway.

Avatar image
Matthew posted on March 10, 2011 @ 12:47 UTC

Please ignore my ajax limit post I was sending over ssl and each ping opened a new tcp connection I will try getting the pings unencrypted via a seperate script :)

Avatar image
Martin posted on September 4, 2011 @ 15:13 UTC

Hi kirsle, first, Thanks! 2nd i have a problem :S ... that example not work for me on IE and Chrome, and i new in perl, i cant resolv, can you help me? please...

(sorry for my bad english)

Avatar image
Noah (@kirsle) posted on September 4, 2011 @ 15:28 UTC

On Chrome (and Safari) there's a known problem where ajax requests aren't allowed to run when the page begins trying to load a new page (so, when you hit the submit button, that stops ajax from working).

I posted a workaround in an earlier comment:

"Also note that this won't work well at all on Safari and Chrome; on these browsers, all ajax requests get blocked as soon as the page begins trying to load another page. There's a workaround for this by submitting the form into an iframe on the page, via , so that the main page doesn't have to go away so the ajax requests aren't stopped. The result page that loads in the iframe, then, would do a parent.window.location="..." to send the user to the results page. "

I haven't done extensive testing on IE (might have only tried IE 6)... it worked for me on whichever version of IE it was that I tested, but it might be that IE is following Chrome's lead now on that ajax thing.

Avatar image
Starfish888 posted on October 25, 2011 @ 15:47 UTC

Thanks for such a compact and simple to comprehend code.

Just 1 question. I have been searching around to figure out how to write the final line I need. This final line would replace the "Print" line after the file has completed it's download.

I don't know too much about cgi.

I need a line of code that will load a specific PHP file using the POST method.

Can you provide an example that will work with your code? Many Thanks.

Avatar image
reborn Webbyprogenthusiast posted on November 23, 2011 @ 18:44 UTC

Good job! I planned to implement an upload progressbar to my site this weekend. This will help. The CGI module (which is a core module in Perl) appears to be at the heart of most implementations I have seen so far on the web. Apparently PHP is not cut out to do this kind of work at such low level. For those looking for more info on check out the documentation at . Thanks for the article, you are right that the examples on the web are not very good.

Avatar image
Eric posted on June 9, 2012 @ 09:44 UTC

Great script! I have a question: My hosting provider limits the time that a perl program is allowed to run to 1 minute. So if a user has a slow connection or a large file, the upload fails. Would it be possible to end the cgi script every 30 seconds from the browser, and then restart and append the next 30 seconds of upload to the file on the server?

Avatar image
Noah (@kirsle) posted on June 9, 2012 @ 18:35 UTC

As far as I know that isn't possible. :(

Avatar image
Revlin J posted on January 15, 2013 @ 22:35 UTC

Thanks for this insightful solution, K. I love how you syntax-color-coded the example files. How did you do this?

Avatar image
Noah (@kirsle) posted on January 15, 2013 @ 23:17 UTC

I used the vim text editor ( The commands are:

:let html_use_css=1 :TOhtml

And then :wq the file it generated and there ya have it.

Avatar image
Troutmanlove posted on October 9, 2013 @ 12:19 UTC

Thanks for sharing. The script, even if it's just a proof of concept, seems to work almost well.

The only bug I noticed is that the ajax ping doesn't stop to be executed when the upload is completed even if the file is correctly uploaded and you will see "received from server: 0:0:0:error session NUM OF SESSION doesn't exist" .

I am using an iframe as target of the form but I don't think is the problem. On your original version the ping stop only because the page is reloaded.

Do you have any hint? Using a JSON as output method and intercept the response within the ajax call will do the trick perhaps ?


Avatar image
Egon posted on April 21, 2014 @ 21:03 UTC

Pardon my ignorance, but I don't quite understand your explanation of how to use an iframe to get this to work with Safari or Chrome.

  1. Where do you put the iframe? Somewhere on upload.html, I assume?

    </p></li> <li><p>What is the src for the iframe? upload.cgi doesn't seem to work. I tried making a third file I called iframe.html and made that the src, but that didn't work either. I just end up with the &quot;0:0:0:error invalid action unknown&quot; message as soon as I load upload.html</p></li> </ol> <p>iframe.html</p> <pre><code> &lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt; &lt;/head&gt; &lt;body&gt; iframe&lt;br&gt; &lt;script type=&quot;text/javascript&quot;&gt; if (window.location != window.parent.location) window.parent.location = &quot;upload.cgi&quot; &lt;/script&gt; &lt;/body&gt; &lt;/html&gt; </code></pre> <p>A push in the right direction would be much appreciated. Thanks.</p>

Avatar image
Noah (@kirsle) posted on April 21, 2014 @ 21:09 UTC


What you'd do is, the HTML form would post into the iframe, like this:

<iframe name="my_iframe" src="about:blank" width="1" height="1" style="display: none"></iframe>
<form name="upload" action="upload.cgi" target="my_iframe">

So when the form is submitted, it gets posted into the iframe instead of the main window. Then in upload.cgi when the upload is complete, it would print out some JS code that would make the parent window redirect, like,

window.onload = function() {
   parent.window.location = "/upload-complete.html?id=$whatever";

Avatar image
Egon posted on April 21, 2014 @ 22:21 UTC

Great. That makes much more sense and it worked like a charm. Thanks for the quick reply.

Avatar image
trigger posted on August 16, 2015 @ 16:12 UTC

Hi, I love the script. I am trying to modify it for use with multiple files. Any ideas on that??

Avatar image
stingraze (Tsubasa Kato) posted on March 1, 2016 @ 06:23 UTC

Thank you very much for making such an awesome script! Regards, Tsubasa Kato

Add a Comment

Used for your Gravatar and optional thread subscription. Privacy policy.
You may format your message using GitHub Flavored Markdown syntax.