Search Flex Components Free

Custom Search

December 29, 2007

Multiple File Upload and Download Component in Flex

Let’s say that you take a lot of pictures. You take pictures, but you don’t want to think about how to upload them. You’d really like an application to monitor a directory for new images, and then upload them to a server. That kind of rules out the browser - it’s not going to be watching a directory on your desktop for image files, and then automatically uploading them. What about Apollo though? Couldn’t an Apollo application monitor a directory for me? Sure! Let’s make it happen…To preface this little experiment, the Apollo alpha doesn’t currently support the ability to run as a background process. To that end, this application does have to be running. It can be minimized, but it does have to be running. Oh, and I should mention that this application is written entirely in HTML, CSS and JavaScript. No Flash involved here, just Apollo DHTML goodness.

The UI for this application can be pretty simple - especially since we’re talking about an application that could eventually run in the background. To that end, my HTML (view) is little more than the basic HTML page with a single DIV. We’ll eventually use that DIV to hold a list of images and render that list with nifty upload status progress bars. Wait, progress bar of an image being uploaded? I know, it’s not something you generally see in HTML because, well, because it’s not available. That type of low-level event is available to Apollo. We’ll see more about this later, but for now let’s head back to the UI.

<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>Watcher</title> <script src="jquery-1.1.2.js" type="text/javascript"></script> <script src="watcher.js" type="text/javascript"></script> <link href="watcher.css" rel="stylesheet" type="text/css" /> </head> <body> <div id="listing"></div> </body> </html>

I'm not a CSS guru. In fact, I'm rather appalled by the current state of affairs as it pertains to browser differences in CSS implementation. Since I'm building this application for Apollo however, I know that I'll be using WebKit, and that makes my job a lot easier. One single browser that runs consistently across operating systems. That I can handle. I use some absolute positioning for the list DIV and a few other items, and then I sprinkle in some relative positioning for smaller textual regions.

.bar { position: absolute; height: 6px; margin-top: 24px; border: solid 1px #446EAC; left: 28px; right: 5px; visibility: hidden; background-color: #FFFFFF; } body { background-color: #F6F5F4; font-family: Arial, Helvetica, sans-serif; font-size: 11px; color: #000000; } .icon { position: relative; margin-left: 5px; margin-top: 3px; float: left; } .item { height: 36px; border-bottom: solid; border-bottom-color: #808080; border-bottom-width: 1px; cursor: default; background-color: #FFFFFF; } #listing { border: solid; border-color: #808080; border-width: 1px; background-color: #FFFFFF; overflow: auto; position: absolute; left: 5px; bottom: 5px; right: 5px; top: 5px; } .filename { position: relative; float: left; margin-left: 4px; margin-top: 5px; clear: right; } .filename span { color: #808080; position: relative; margin-left: 5px; } .pending { position: relative; margin-left: 27px; margin-top: -1px; color: #808080; float: left; clear: left; } .progress { width: 0%; height: 100%; background-color: #AEDDF2; }

If we can think about having CSS consistency across OS thanks to the WebKit engine in Apollo, then it stands to reason that we can think about consistency with JavaScript too. While that is true, I'm not a masochist, and I have elected to use as a JavaScript library for this application. I'd be interested in your thoughts on jQuery and it's use in sample Apollo applications. Would you prefer samples be core JavaScript, or is a library acceptable? Which libraries would you prefer?

The first thing we'll do when the application starts is check for the existence of a directory on the desktop. Apollo makes this easy, without having to think about where the desktop is located and on which operating system. The File object in Apollo has a variety of static properties to common OS locations that return a File reference. In this case we can use File.desktopDirectory and file.resolve() the specific directory on the desktop that we're interested in watching.

// Watcher logic $( document ).ready( function() { // Check for directory var watching = runtime.flash.filesystem.File.desktopDirectory; watching = watching.resolve( dir ); // Create it if it doesn't exist if( !watching.exists ) { watching.createDirectory(); } // Do the initial check and updating of the UI update(); } );

Once the application is started and we've confirmed the presence of our image directory (or created it if it doesn't exist), then we move on to checking for the presence of image files. The File object has a listDirectory() method that we can use to get the contents of the directory. We don't want to upload anything other than image files however, so we'll check the file extension. This isn't particularly accurate, but it's a good start. If the item in the list of files is an image, then we can build an item to render the details and then pop it into the list. In the case that there are images, we'll start uploading the files, otherwise we'll check back every five seconds via JavaScript's setInterval() function.

// Checks the directory and updates the UI function update() { // HTML block for list item var itemo = "<div class="item">"; var icon = "<img src="image.png" width="18" height="18" class="icon" />"; var fileo = "<div class="filename">"; var sizeo = "<span>("; var sizec = " KB)</span>"; var filec = "</div>"; var pending = "<div class="pending">Upload not yet started...</div>"; var baro = "<div class="bar">"; var progress = "<div class="progress"></div>"; var barc = "</div>"; var itemc = "</div>"; var inject = null; // Get the list of image in the directory var watching = runtime.flash.filesystem.File.desktopDirectory; var list = null; // Get the directory listing watching = watching.resolve( dir ); list = watching.listDirectory(); // Remove existing item entries $( "#listing" ).empty(); // Find the actual image files for( var i = 0; i < list.length; i++ ) { if( list[i].name.toLowerCase().indexOf( ".jpg" ) >= 0 ) { // Build the item inject = itemo + icon + fileo + list[i].name + sizeo + formatSize( Math.round( list[i].size / 1000 ) ) + sizec + filec + pending + baro + progress + barc + itemc; // Inject the new item container $( "#listing" ).append( inject ); } } // Add mouse handlers to any new items $( ".item" ).mouseover( function() { $( this ).css( "backgroundColor", "#D4D5D8" ); } ); $( ".item" ).mouseout( function() { $( this ).css( "backgroundColor", "#FFFFFF" ); } ); // TODO: Show a means to remove an image when the mouse is over an item // TODO: Have image thumbnail appear when mouse is over icon // TODO: Show summary status bar with total upload progress // Determine if uploading needs to occur // Avoids refresh of list mid-upload // Can check for more later, after upload has completed if( $( ".item" ).size() > 0 ) { upload( null ); } else { // Check again at some point in the future setTimeout( "update()", interval ); } }

Rather than check the file system again before we're done uploading, I've chosen to extract the file name from the list item and then infer the path (we know where that directory on the desktop is, right?). I figure that once we're done with this batch of uploads that we'll check the file system again later for more image files. There are some shortcomings to this approach, but I'm not trying to account for every use-case with this example.

You might initially be inclined to think that we'll use an upload form in HTML to upload our files, but as mentioned previously, there's no means provided to check the progress. As it turns out the File object has an upload() method on it. The only thing the upload() method requires is a reference to a URLRequest object. This is the object that most importantly contains the URL endpoint, but it might optionally contain some additional form data. The File.upload() will actually execute a multi-part form upload to the server, and you can treat it as such once it gets there.

For this example I'm using ColdFusion on the server. This is mostly because I have it readily available, and it's only a single line of code to get the uploaded bytes and store them on disk. I like easy. You can replace this with whatever you prefer on the server.

The other important aspect to this is the additional event listeners I've put on the File object. The progress event let's me know periodically that the upload is taking place. It passes an event object that among other data, contains bytesLoaded and bytesTotal properties. I can use this information to determine a percentage of the progress and then tell the CSS on the progress bar to size itself to that percent. The complete event will fire when the upload is finished, and then we can take additional action.

// Show progress for the image being uploaded function progress( event ) { // Determine overall percentage from event properties var percent = Math.round( ( event.bytesLoaded / event.bytesTotal ) * 100 ); // Change CSS properties to show bar width based on percent $( ".progress:first" ).css( "width", percent + "%" ); }

I've chosen to call back to the upload() function when the upload operation is completed. When that event fires, it too will pass an event object along. The upload() function checks for the presence of that event object. If the event object is present, then the application knows that the upload is complete, and can clean up the file from disk. Once again the File object provides a deleteFile() method we can use for this in a simple one-liner. If the upload() method gets called an no event object is provided, then the application knows that we need to start an upload operation.

// Perform the actual upload function upload( event ) { // Variables for managing the upload // Image is a reference to a file on disk from the first item in the list var img = extractAndResolve( $( ".item:first" ).text() ); var request = null; // If there is no argument value, then upload the first item in the list if( event == null ) { // Show the upload progress bar and hide the waiting text $( ".pending:first" ).css( "visibility", "hidden" ); $( ".bar:first" ).css( "visibility", "visible" ); // Destination of uploaded file request = new url ); // Attach the appropriate event listeners img.addEventListener(, progress ); img.addEventListener(, upload ); // Upload the file img.upload( request, field, false ); } else { // Remove the first image item from the list $( ".item:first" ).remove(); // Remove the actual image from the disk img.deleteFile(); // Move onto the next image if there are any available if( $( ".item" ).size() > 0 ) { upload( null ); } else { // Keep checking for new images setTimeout( "update()", interval ); } } }

A word of caution here is that I have chosen to remove the files after they've been uploaded. If you're going to try this application out on your machine, make sure that you copy the files and not move them. This application has no mercy and will start uploading and deleting within seconds of your paste operation. User beware!

Once the upload operation is complete, and the image file has been removed from disk, the application checks for any additional items in the list. If there are none, the process of waiting five seconds and checking for more files starts all over. If there are additional items in the list (additional files), then the next item in the list will be processed (uploaded). I've chosen to always check the first item in the list, so uploading will happen from the top down.

That's all there is to it! There's a number of to-do's that I'd like to add, and I've made comments to that point, which again is all JavaScript. Since I'm using my own server for the upload, I throttle the upload image size to no more than three megabytes (3 Mb). Feel free to point the URL to your own server, or better yet Amazon S3 and not worry about it. That was beyond my scope for this little experiment. You can access the source code and application file in the archive attached to this post.

Related Flex Tutorials