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));

...

Advertisements

Actions

Information

11 responses

25 03 2009
Vinay

Can you post/mail the complete code ?

26 03 2009
Justin J. Moses

Vinay,

I didn’t post a complete solution for three reasons. Firstly because there are different ways you could solve/implement the code. Secondly because I didn’t feel the need – the code is fairly self explanatory. Thirdly because I composed the code from a project I’m working on, rather than creating one from scratch.

Is there anything specific you don’t understand?

justin

19 05 2009
Shogun

Can you show example how to read a large file (>100 MB) into a byte array and then to divide it into packages, upload it to the server and then to collect it in one file??
It is possible??

4 06 2009
Nathan

Very cool ! Also, What do you use to format your code in wordpress ??

4 06 2009
Justin J. Moses

That’s just the basic “code” tags. WordPress automatically formats then with line numbers (up to 99 that is 🙂

j

9 09 2009
Olli

Thanks, this have been really useful as my AIR coding experience is really near to zero.

12 09 2009
Mark Embrey

hey Justin!

I’m really a beginner at all of this… I have a couple questions, in reverse order (for some reason!):

2. what is the use of the term “model” as in var file:File in model.filesToUpload?

1. what is the relationship (if any) between the array _filesToUpload and filesToUpload, as in 2 above?

many thanks,

(btw, thanks for posting this!)

MCE

29 09 2009
DOOM

Hi, Nice article.
Is it possible to make drag and drop in browser with flex 3 or 4?

29 09 2009
Justin J. Moses

@Doom,

I’ve got news and it’s all bad. No drag n drop via the browser and Flex.

justin

12 10 2009
Chad

Hi Justin,
I am using Adobe Air client to talk to a PHP server(WAMP).
I want to drag and drop a directory from windows having subFolders and files onto air. The whole structure has to get uploaded.
Please let me know,

Thanks.
Chad

17 05 2010
Divakarla Srinivas

Hi Justin,

I am working in a project (web based), where i need to upload compressed images followed by modifying bitmap data, the problem is I cant write into FileReference.data as it is read only properly, So I am using URLLoader POST, but again the problem is ProgressEvent Is not fired, but I have noticed that it is possible (Can you just any workaround way), visit http://www.snapfish.com , they are able to show a progress bar while uploading a compressed images? Let us know if you have any solution to trigger ProgressEvent while using URLLoader to upload

Thanks,
Divakarla Srinivas

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s




%d bloggers like this: