AIR multiple file uploads: drag n drop to client zip to server upload and unzip

25 03 2009

The aim of this post is to get an AIR application to take a collection of files dragged n dropped onto it, zip them up on the client, and send them off to the server (ASP.NET). 

There are a few things to note here:

  • Flash officially supports files of up to 100MB, trying to ZIP anything larger and you WILL run into server memory issues; Flash will crash. You can use File.upload() for files > 100MB, but trying to ZIP them will cause the crash.
  • ZIPping on the client takes time, 50MB could take from 15s to a minute. The code supplied performs it sync (not async). 
  • client software FZip, server zip : ISharpZipLib 
  • uploading manually (using URLLoader instead of File.upload means in you cannot track the Upload’s progress – as of AIR 1.5).

 

Step 1. The Drag & Drop in AIR

This is quite trivial. Allowing the user to drag and drop as many files/folders onto your application.

Client (AIR) code:

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication 

	nativeDragEnter="onDragEnter(event)"
	nativeDragDrop="onDragDrop(event)"

	layout="absolute" 

	xmlns:mx="http://www.adobe.com/2006/mxml" >

	<mx:Script>
		<![CDATA[

			import mx.managers.DragManager;
			import mx.events.DragEvent;

			private function onDragEnter(evt:NativeDragEvent):void
			{
				//ensure clipboard contains files
				if (evt.clipboard.hasFormat(ClipboardFormats.FILE_LIST_FORMAT))
				{
					NativeDragManager.acceptDragDrop(this);
				}
			}

			private var _filesToUpload:Array;

			private function onDragDrop(evt:NativeDragEvent):void
			{
				_filesToUpload = evt.clipboard.getData(ClipboardFormats.FILE_LIST_FORMAT) as Array;
			}

		]]>
	</mx:Script>

</mx:WindowedApplication>

 

Step 2. Zipping on the client

The nicest solution I could find for compressing files in AIR to ZIP is FZip.

I tried another library, but ISharpLib didn’t like it, it was giving me EOF file header issues in the ZIP decompression.

So the solution is to take the ZIP byteArray and to send it via the lower level URLLoader class.

NOTE: this uses the FileStream.open is sync mode. You could use FileStream.openAsAsync though it would obviously require listening and handling the corresponding events.

Client (AIR) code [URLLoader.load() solution]:

var request:URLRequest = new URLRequest("http://example.com/Handlers/UploadHandler.ashx");

request.method = URLRequestMethod.POST;
request.contentType = "application/octet-stream";

var loader:URLLoader = new URLLoader();

loader.dataFormat =  URLLoaderDataFormat.BINARY;

loader.addEventListener(Event.OPEN, onFileUploadStart,false,0,true);

//no point listening for progress, because it doesn't work for URLLoader uploads, only downloads
//loader.addEventListener(ProgressEvent.PROGRESS, onFileUploadProgress,false,0,true);

loader.addEventListener(Event.COMPLETE,onFileUploadComplete,false,0,true);

var fzip:FZip = new FZip();

for each (var file:File in model.filesToUpload)
{
	var fs:FileStream = new FileStream();
	fs.open(file, FileMode.READ);
	var data:ByteArray = new ByteArray();
	fs.readBytes(data);
	fs.close();

	fzip.addFile(file.name, data);
}

var bytes:ByteArray = new ByteArray();
fzip.serialize(bytes);

request.data = bytes;

loader.load(request);

 

What about sending extra parameters?

Client (AIR) code:
var request:URLRequest = new URLRequest("http://example.com/Handlers/UploadHandler.ashx?someVariable=" + someVariable);

What about the upload progress bar? 

If you need the progress bar, then you’re best option is writing this zip file to the client and uploading that. 

This also means that if you have optional parameters, you don’t have to append them to the URL but can send them in the data.

This changes the client code to use File.upload().

Client (AIR) code – [File.upload() solution]:

var params:URLVariables = new URLVariables();

params.someVariable = someVariable;
//add other variables here

var request:URLRequest = new URLRequest(model.UPLOAD_URL);

request.method = URLRequestMethod.POST;
request.data = params;

var fzip:FZip = new FZip();

for each (var file:File in model.filesToUpload)
{
	var fs:FileStream = new FileStream();
	fs.open(file, FileMode.READ);
	var data:ByteArray = new ByteArray();
	fs.readBytes(data);
	fs.close();

	fzip.addFile(file.name, data);
}

//get the zip as a byte array
var bytes:ByteArray = new ByteArray();
fzip.serialize(bytes);

//write the zip to a local file
var fsW:FileStream = new FileStream();
//change "tmp.zip" to a better name :)
var wFile:File = File.applicationStorageDirectory.resolvePath("tmp.zip");

fsW.open(wFile, FileMode.WRITE);
fsW.writeBytes(bytes);
fsW.close();

//listen to upload events
wFile.addEventListener(Event.OPEN, onFileUploadStart,false,0,true);
wFile.addEventListener(ProgressEvent.PROGRESS, onFileUploadProgress,false,0,true);
wFile.addEventListener(Event.COMPLETE,onFileUploadComplete,false,0,true);

//start the upload
wFile.upload(request);

 

Step 3. Unzipping on the server

Now, I use SharpZipLib in .NET because it’s Open Source, and because it’s also bundled with WebORB .NET, the remoting gateway that I often use.

If you use the URLLoader.load() option above (from Step 2) – then you must use Request.InputStream to access the parameters

Server (ASP.NET C# Handler – ashx) code: (SharpZipLib extraction)

using ICSharpCodeInternal.SharpZipLib.Zip; 
   public class UploadHandler : IHttpHandler {

        public void ProcessRequest (HttpContext context)
        {
            String someVariable = context.Request["someVariable"];

            ZipInputStream zipStream = new ZipInputStream(context.Request.InputStream);
            ZipEntry zipEntry;

            while (true)
            {
                zipEntry = zipStream.GetNextEntry();

                if (zipEntry == null)
                {
                    break;
                }

                //choose location to extract files to
                string serverFolder = context.Server.MapPath("~/Uploads/");

                FileStream streamWriter = File.Create(( serverFolder + zipEntry.Name));

                int size = 2048;

                byte[] data = new byte[2048];

                while (true)
                {
                    size = zipStream.Read(data, 0, data.Length);

                    if (size > 0)
                    {
                        streamWriter.Write(data, 0, size);
                    }
                    else
                    {
                        break;
                    }
                }

                streamWriter.Close();

            }

            zipStream.Close();
        }
    }

 

 

If you are using File.upload() on the client 

You’re Handler in ASP.NET now will be looking in the context.Request.Files array for the attached ZIP.

Modified Server (ASP.NET C# Handler – ashx) code:

ZipInputStream zipStream = new ZipInputStream(context.Request.Files[0].InputStream);

 

If you need folder structures

To do ZIP folders, you simply add the folder name before the file when you ZIP:

Modified Client (AIR) code:

fzip.addFile(folderName + "/" + file.name, data);

 

And then in the server code, you need to create directories if required:

Modified Server (ASP.NET C# Handler – ashx) code:

//existing code
...
string serverFolder = context.Server.MapPath("~/Uploads/");
string dir = Path.GetDirectoryName(serverFolder + zipEntry.Name);

if (!Directory.Exists(dir))
{
    Directory.CreateDirectory(dir);
}                

//existing code
FileStream streamWriter = File.Create(( serverFolder + ze.Name));

...





Threading in ASP.NET : quick start

28 03 2008

Threading. God loves it, and so should you.

It’s pretty easy to get started threading in .NET. So why wait?

But why would you want to multithread? There are plenty of different occasions, but essentially when you have some processes that should run asynchronously. This could include an automatic conversion process for some uploaded file – there’s nothing better than throwing it to a thread (or even a Windows Service) to process. That way, the user doesn’t have to wait for conversion to complete before they get back to your site.

Getting going is pretty easy, the complications come when you start looking at threads that use the same data.

Essentially, you start the Thread either as an empty method call, or by passing it some object. You should at least know what a delegates is first (essentially it is a function that can be passed around like a variable).

Now, I prefer the Parameterised (i’m Aussie, so I don’t like using Z where it’s an S 😛 ) option below just because I often want to pass at least something.

C#

using System.Threading;

ParameterizedThreadStart starter = delegate(object prop){SomeObject.SomeMethod(prop)); };
new
Thread(starter).Start(someObject);

One thing you need to remember here though is, if you are calling this in ASP.NET, then it’s the aspnet_wp.exe process that starts the thread, and if this is killed or restarted somehow, then it will kill your thread also.

It could be killed or restarted for the following reasons:

  • change in global.asax file,
  • change in machine.config,
  • change in web.config
  • change of content of /bin folder
  • IIS service is interrupted (incl restart)
  • Server is restarted

One thing you will learn to love: the Thread.Sleep() method and the System.Threading.Timer class…

Enjoy!

Useful Reference: This article gives a great intro into threads.