Perl Uploader with Progress Bar

Noah Petherbridge
kirsle
Posted by Noah Petherbridge on Friday, June 05 2009 @ 02:19:34 PM

Update (11/25/09): This method is all wrong. Here is the correct way.

A thread on Tek-Tips came up recently about making a progress bar for a file uploader in Perl.

Investigating the issue more closely, I found a couple of commercial solutions (read: paid for), where even their free edition involves thousands upon thousands of lines of code, spread out across many different files. Nowhere to be found was a simple, straight-to-the-point example of how this could be done.

From poking around at what code I could find, I got the basic gist to it:

  • You have a regular file upload form as usual
  • When hitting Submit, a JavaScript callback on the submit button provokes some Ajax code to start ticking, and returns true, allowing the form to submit to the CGI script as normal.
  • The CGI script accepts along with your file, an "action" of "upload" - this instructs the CGI file to accept your uploaded file, create a session file and begin saving it to disk.
  • While your browser is waiting for the CGI script to finish processing your form, ajax is running in the background polling the CGI script with a different question: "action" = "progress"
  • When the CGI is polled for the progress of the uploaded file, it checks how big the file was, and how much has been saved already, and returns some simple numbers.
  • JavaScript, still running on your file upload page, uses these numbers to update the page to show you the current progress.

It looks like this:

Uploader

If that sounds complicated, it really isn't. 77 lines for the CGI script, and 126 lines for the HTML page, including the JavaScript (only 60 lines of JavaScript).

The screenshots, code, and download link follow.

Screenshot
The upload form. Simple.

Screenshot
Beginning an upload.

Screenshot
And the progress begins!

Source Code:

upload.html (the HTML form and JavaScript)

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>File Upload Test</title>
<style type="text/css">
    body {
        background-color: white;
        font-family: Verdana;
        font-size: small;
        color: black
    }
    #trough {
        background-color: silver;
        border: 1px solid black;
        height: 24px
    }
    #bar {
        background-color: #669900;
        height: 24px;
        width: 1%
    }
</style>
</head>
<body>

<h1>File Upload Test</h1>

<div id="progress" style="display: none; margin: auto; width: 350px">
    <fieldset>
        <legend>Uploading...</legend>

        <div id="trough"><div id="bar"></div></div>
        Uploaded: <span id="uploaded">0</span>/<span id="size">0</span><br>

        Percent: <span id="percent">0</span>%
    </fieldset>
</div>

<div id="form" style="display: block; margin: auto; width: 350px">
    <fieldset>
        <legend>Upload a File</legend>

        <form name="upload" action="upload.cgi" method="post" enctype="multipart/form-data" onSubmit="return uploadFile(this)">

        <input type="hidden" name="action" value="upload">
        File: <input type="file" name="file" size="20"><br>

        <input type="submit" value="Submit File">
        </form>
    </fieldset>
</div>

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

<script type="text/javascript">
    // When the form is submitted.
    function uploadFile(frm) {
        // Hide the form.
        document.getElementById("form").style.display = "none";

        // Show the progress indicator.
        document.getElementById("progress").style.display = "block";

        // Wait a bit and make ajax requests.
        setTimeout("getProgress()", 1000);

        return true;
    }

    // Poll for our progress.
    function getProgress() {
        var ajax = new XMLHttpRequest();

        ajax.onreadystatechange = function() {
            if (ajax.readyState == 4) {
                gotProgress(ajax.responseText);
            }
        };

        ajax.open("GET", "upload.cgi?action=progress&session=my-session&rand=" + Math.floor(Math.random()*99999), true);
        ajax.send(null);
    }

    // Got an update
    function gotProgress(txt) {
        document.getElementById("debug").innerHTML = "got: " + txt + "<br>\n";

        // Get vars outta it.
        var uploaded = 0;
        var size = 0;
        var percent = 0;
        var stat = txt.split(":");

        // Was it an error?
        if (stat[0] == "error") {
            document.getElementById("debug").innerHTML += "error: " + stat[1];
            setTimeout("getProgress()", 1000);
            return false;
        }

        // Separate the vars.
        var parts = stat[1].split("&");

        for (var i = 0; i < parts.length; i++) {
            var halves = parts[i].split("=");

            if (halves[0] == "received") {
                uploaded = halves[1];
            }
            else if (halves[0] == "percent") {
                percent = halves[1];
            }
            else if (halves[0] == "size") {
                size = halves[1];
            }
        }

        document.getElementById("debug").innerHTML += "size:" + size + "; received:" + uploaded + "; percent:" + percent + "<br>\n";

        // Update the display.
        document.getElementById("bar").style.width = parseInt(percent) + "%";
        document.getElementById("uploaded").innerHTML = uploaded;
        document.getElementById("size").innerHTML = size;
        document.getElementById("percent").innerHTML = percent;

        // Set another update.
        setTimeout("getProgress()", 1000);
        return true;
    }
</script>

</body>
</html>

upload.cgi (the CGI script)

#!/usr/bin/perl -w

use strict;

use warnings;
use CGI;
use CGI::Carp qw(fatalsToBrowser);

my $q = new CGI();

# Handle actions.
if ($q->param('action') eq "upload") {
    # They just submitted the form and are sending a file.

    my $filename = $q->param('file');
    my $handle   = $q->upload('file');
    $filename =~ s/(?:\\|\/)([^\\\/]+)$/$1/g;

    # File size.
    my $size = (-s $handle);

    # This session ID would be randomly generated for real.
    my $sessid = 'my-session';

    # Create the session file.
    open (CREATE, ">./sessions/$sessid") or die "can't create session: $!";
    print CREATE "size=$size&file=$filename";
    close (CREATE);

    # Start receiving the file.
    open (FILE, ">./files/$filename");
    while (<$handle>) {
        print FILE;
    }
    close (FILE);

    # Delete the session.
    unlink("./sessions/$sessid");

    # Done.
    print $q->header();
    print "Thank you for your file. <a href=\"files/$filename\">Here it is again</a>.";
}
elsif ($q->param('action') eq "progress") {
    # They're checking up on their progress; get their sess ID.
    my $sessid = $q->param('session') || 'my-session';
    print $q->header(type => 'text/plain');

    # Does it exist?
    if (!-f "./sessions/$sessid") {
        print "error:Your session was not found.";
        exit(0);
    }

    # Read it.
    open (READ, "./sessions/$sessid");
    my $line = <READ>;
    close (READ);

    # Get their file size and name.
    my ($size,$name) = $line =~ /^size=(\d+)&file=(.+?)$/;

    # How much was downloaded?
    my $downloaded = -s "./files/$name";

    # Calculate a percentage.
    my $percent = 0;
    if ($size > 0) {
        $percent = ($downloaded / $size) * 100;
        $percent =~ s/\.(\d)\d+$/.$1/g;
    }

    # Print some data for the JS.
    print "okay:size=$size&received=$downloaded&percent=$percent";
    exit(0);
}
else {
    die "unknown action";
}

Notes on this code: it's just a proof of concept. You'd want to handle the sessions better. Here the session ID is hard-coded as "my-session" -- that wouldn't work in real life. But it's just a barebones working implementation of a file upload progress bar, with all the crap cut out and does specifically what it's supposed to. Others should find it useful, so you can download it.

Update (11/25/09): This method is all wrong. Here is the correct way.

Categories:

[ Blog ]

Comments

There are 11 comments on this page.

guest
guest
Posted on Saturday, October 31 2009 @ 05:10:17 AM by Flavio Bianchetti.

uhm... I have tried out your script, but it actually does not work as one might expect, though... it's not really an "uploader meter", but more likely a meter for your BUS speed =) All in all, it is only capable of measuring how fast does the ALREADY UPLOADED file takes to be moved from one path into onother...

Do you know whether it's possible to get TEMP file stats through Perl?! It's my opinion that the way your script works is pretty useless for a client...

Please let me know, cheers
Flavio Bianchetti (flavio3 [at] email [dot] it)

guest
guest
Posted on Monday, November 09 2009 @ 10:50:40 AM by Andreas Schamanek.

I tried it, too. Unfortunately, doesn't work for me either using FF 3.5 and Apache 2.2.3. I wish I knew what the problem is because the simplicity otherwise is tempting. One reason is that the file is stored in /var/tmp before it gets moved to ./files. But, I am afraid this is not all.

guest
guest
Posted on Wednesday, December 02 2009 @ 05:55:36 AM by Flavio Bianchetti.

Hi Andreas Schamanek, if you are looking at simplicity and perhaps are using PHP in combination with your Apache server, you might be interested into PHP/APC module. It's really easy and straightforward to implement within Ajax. As far as I remember there is some good guideline at IBM website... I had some success with it. Moreover it does what it is intended for :-)
Tschüß
Flavio Bianchetti

ps: you need PHP5.x+ or so, check your version

guest
guest
Posted on Sunday, December 06 2009 @ 09:07:15 AM by Andreas Schamanek.

Thanks a lot Flavio, this is indeed an very good pointer :-)
Besides, I only now saw that Kirsle has come up with a new approach.

guest
guest
Posted on Tuesday, January 24 2012 @ 10:09:14 PM by Anonymous.

guest
guest
Posted on Friday, July 13 2012 @ 04:13:43 AM by Ahsan.

This progress bar not working in chrome ans Safari...What to do???

Avatar
kirsle
Posted on Friday, July 13 2012 @ 09:17:38 AM by Noah Petherbridge.

The method described on this page doesn't really work. This one is better: http://www.kirsle.net/blog/kirsle/simple-perl-uploader-with-progress-bar

The one linked there has an issue with Chrome and Safari too: when a page begins to unload (i.e. submitting a form), Chrome and Safari stop any JavaScript from running on the old page anymore, which blocks the ajax requests to check on the upload status.

The workaround is to have the form submit into a frame, like having an <iframe name="myframe"></iframe> and then using <form target="myframe">. Thus the main page doesn't unload when the form is submitted (since it loads the form result in the frame instead), so ajax can run on the main page.

You'd also have to change the upload.cgi so that when the results page loads inside the frame, it uses JavaScript to redirect the parent window to a new page, with parent.window.location = "upload-successful.html"; or whatever.

guest
guest
Posted on Sunday, July 15 2012 @ 09:50:03 PM by Ahsan .

Thanx Kirsle, Let me try .......

guest
guest
Posted on Sunday, July 15 2012 @ 10:27:50 PM by Ahsan.

Ok BOss ...........its Ok ...................Thanx Thanx a lot of THanx ..............

guest
guest
Posted on Friday, October 12 2012 @ 03:34:20 PM by Kumar.

Finally I found file upload code written in Perl works fine. Thanks for your excellent work.

guest
guest
Posted on Monday, October 15 2012 @ 10:37:37 AM by John.

Kirsle,
Excellent work
I tried your new code, it's uploading file properly, but I noticed couple of things.
1) Uploading status is not changing.
2) once file upload complete, response it display in another page (not in the same same page where we did file upload)
for example
""Thank you for your file. <a href=\"files/astar.jpg">Here it is again</a>.";

Could you please help to get it work.
Thanks,
John

Add a Comment

Your name:
Your Email:
Message:
Comments can be formatted with Markdown, and you can use
emoticons in your comment.

If you can see this, don't touch the following fields.