Wednesday, February 11, 2009

Uploading Files from AIR

The Problem
You want to upload multiple files from within an AIR application without user intervention.

The FileReference class has an 'upload()' method for doing just this, however it requires the user to first browser for and select the files on their computer. This is user intervention, so we cannot use this class.

The URLRequest class accepts a URLVariables object as its 'data' property. The problem with this is that the POSTed variables are encoded using 'x-www-form-urlencoded' format instead of the 'multipart/form-data' format which is required for sending files. So we cannot use this method of sending files.

The Solution
Using the URLRequest class, we assign a ByteArray object to it's 'data' property.

The URLRequest class accepts asigning a byte array to it's 'data' property, which it then uses as the body of the POST. So the idea is to manually construct the body of a POST and store it into a byte array.

Sounds simple enough, but there are a few gotchas that need we need to consider.

First of all, we need to make sure that we first set the correct content type. The content type also needs to contain a boundary definition so that the body of the POST can be correctly read by whatever server we're sending it to.

Here is an example:

var boundary        ='---------------------------------ApawaB03x';
var request =new air.URLRequest();
request.contentType ='multipart/form-data, boundary='+boundary;

The next thing to consider is the format of the bytes that we'll be writing into the byte array. The safest thing to do is to write them as UTF using the 'writeUTFBytes()' method available on the ByteArray object. The actual file data however must be written as raw binary using the 'writeBytes()' method. Additionally, with each line that we write to the byte array, we must make sure that we insert new line characters. These could be '\n' or '\r\n' depending on your choice. Since I'm a windows user, I've chosen to use '\r\n'.

The final thing to consider is the content-type of the files that will become part of this POST request. Getting this wrong can have consequences depending on the server you're POSTing to. Some server's may be configured to reject files which don't have a correct file extension vs. content-type match.

Now lets look at how a file should look like within our POST body.


-----------------------------------ApawaB03x
Content-Disposition: form-data; name="Filedata[]"; filename="foo.bar"
Content-Transfer-Encoding: binary
Content-Length: 32543600
Content-Type: application/octet-stream

{raw binary data goes here}
---------------------------------ApawaB03x



So with all these things in mind, here is how we bring it all togeather.


//Dummy files.
var files=
[
'C:\\sound.wav',
'C:\\music.mp3',
'C:\\picture.jpg',
'C:\\text.txt'
];
//Initiate objects and static variables.
var boundary ='---------------------------------ApawaB03x',
buffer =new air.ByteArray(),
loader =new air.URLLoader(),
request =new air.URLRequest();

//Setup the request.
request.contentType ='multipart/form-data, boundary='+boundary;
request.method ='POST';
request.url ='http://localhost/';
request.useCache =false;
request.cacheResponse =false;

//Loop through each file and add it to the buffer.
for (var i=0,j=files.length; i<j; i++)
{
var file =new air.File(files[i]),
fileContents =new air.ByteArray(),
fileStream =new air.FileStream();

if (!file.exists)
{
throw 'The file "'+files[i]+'" does not exist and cannot be uploaded.';
delete file,fileContents,fileStream;
}
else
{
fileStream.open(file,air.FileMode.READ);
fileStream.readBytes(fileContents,0,file.size);
fileStream.close();

var mimeType=getMimeTypeByExtension(file.extension);
if (!mimeType)mimeType='application/octet-stream';
buffer.writeUTFBytes
(
[
'\r\n',
'--'+boundary+'\r\n',
'Content-Disposition: form-data; name="Filedata[]"; filename="'+file.name+'"\r\n',
'Content-Transfer-Encoding: binary\r\n',
'Content-Length: '+file.size+'\r\n',
'Content-Type: '+mimeType+'\r\n',
'\r\n'
].join('')
);
buffer.writeBytes(fileContents,0,fileContents.length);
delete file,fileContents,fileStream,mimeType;
}
}
//Close off the buffer with one final boundary.
buffer.writeUTFBytes('\r\n--'+boundary+'--\r\n');
//Assign the buffer to the body of the request.
request.data=buffer;
//Finally initiate the request.
loader.load(request);


Notice the getMimeTypeByExtension function call within the above code. That is obviously an example function. That function would be pre-written to do a look-up on a mime table and return the correct mime type according to the file extension.

Aside from that, everything else in that script will work in AIR without any tweaking. You also may want to add some event listeners to the 'loader' object so that you can fetch a response from the server.