Category: JavaScript

Simple Perl Uploader with Progress Bar

Kirsle
kirsle
Posted by Kirsle on Wednesday, Nov 25 2009 @ 4:16 PM
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 guaging 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:

  1. 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.
  2. The upload CGI script needs to set an "upload hook" that CGI.pm 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.
  3. 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.

Click the links below for the source codes to the HTML and CGI file, conveniently syntax-highlighted by vim:

¤ upload.html - the HTML file
¤ upload.cgi - the CGI script

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

¤ upload.tar.gz (6.6K)

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.

Categories: Perl , HTML , JavaScript , Ajax

[ 38 comments | Add comment | Permalink ]

Perl Uploader with Progress Bar

Kirsle
kirsle
Posted by Kirsle on Friday, Jun 05 2009 @ 2:19 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:

  1. You have a regular file upload form as usual
  2. 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.
  3. 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.
  4. 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"
  5. 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.
  6. 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.

Download: upload.tar.gz

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

Categories: Perl , HTML , JavaScript , Ajax

[ 11 comments | Add comment | Permalink ]

Kirsle
» Homepage (RSS)
» About Me
» Photo Albums
» Guestbook
» Contact Me
Channels
» Linux (47)
» General (44)
» Perl (34)
» Rant (21)
» Software (15)
» RiveScript (9)
» Gnome 3 (8)
» HowTo (8)
» Windows (8)
» HTML (7)
» Android (6)
» Design (6)
» Siikir (6)
» Tk (6)
» Curiosity (5)
» Blackhat (4)
» Gay (4)
» Java (4)
» Minecraft (4)
» Reviews (4)
» VirtualBox (4)
» DOS (3)
» KAGE (3)
» Licensing (3)
» Photos (3)
» Xfce (3)
» ttf2eot (3)
Creativity
» 3D Renderings
» Flash Animation
» JavaScript
» Fonts
» Metacity
» Tutorials
Software
» RiveScript
» Error Generator
» Tk Calculator
» Terminal Apps
» CyanChat Client
Web Tools
» TTF to EOT
» Text Fader
» Favicons
» Distance Calc
» Azulian Encoder
» XBM Masks
Subdomains
» Shell Scripts
» Linux RPMs
» Kirsle::Nano
» Minecraft Server
Miscellany
¤ Pokemon Fuchsia City
¤ DOS and Windows
¤ Raspberry Pi
Links
¤ Google+
¤ Facebook
¤ New MySpace
¤ Twitter
¤ Github
¤ CPAN
Fan Club
» Log In
» Sign Up

Stats
-= Today =-
> Total hits: 2558
> Unique: 1570
-= All Time =-
> Total hits: 1419104
> Unique: 155077
» Traffic History
» Referrers