One SWF To Rule Them All - a universal file wrapper for flash on web and the AIR runtime October 19, 2008
Introduction
With the introduction of the Adobe AIR runtime, what power over the desktop, that used to be only available to full desktop applications written in 'serious' languages like C, is now within the capabilities of normal web developers, in those warm fuzzy languages like Flash. We can now write files and create folders as easy as publishing for AIR and using the AIR specific flash.filesystem.* set of classes. That's great but... it has one serious drawback..anytime we use any of those API's we lose portability of our swf to the web! So if you have a swf, that can be run off the web or locally you'd normally be forced to maintaining two independent SWFs - one for AIR and one for flash player. Needless to say this leads to twice the headache.
In this article we present a solution, showing how to use a universal file wrapper as a part of the troyworks framework. Using this approach, you will learn how to create an SWF that will read/write to files through flash.filesystem.* when embedded in the AIR runtime and that will save files through HTTP when run in the flash player (browser or standalone)...resulting in one swf to rule them all.
If you're currently not in the mood for going through the internals, jump straight to the usage examples section.
Also I'll be giving covering this topic at the <head> presentation on Saturday October 25 2008, using some different code examples. If you haven't heard of <head> (formerly Singularity) It's a kick ass lineup inexpensive web-accessed and covers all manners of scaling, love, flash, flex, and donuts. Okay maybe not donuts, but be sure to give it some love by checking it out. It's only $149!!!
AIR's flash.filesystem
The are three classes in the flash.filesystem library: File, FileStream and FileMode. This is what you normally use in Adobe AIR to access files on the local machine. Each File object represents a file or directory and the FileStream class provides methods to run read & write operations on files. FileMode is simply a container for the various modes in which you can open a file: read, write, append, update.
In our case, when running in the AIR runtime we want to save on the local filesystem, when in a browser we want to save via the web. The save features on the local filesystem are provided already by the AIR runtime. For the server side we need to create a special receptor:
Server side HTTP file operations
In order to provide similar file functionality for the flash player we need to prepare a server side script that will parse HTTP requests and execute file operations accordingly. The example implementation has been written in PHP, and uses HTTP post, but could easily be any other language, or possibly sockets.
Wrapping
In order to comfortably perform file operations under both environments, we need a common interface that won't change with the different underlying libraries. (why the API's weren't written with an interface baffles me)
So we're going to wrap the functionality under a set of new classes: IFile, IFileStream and IFileMode. For ease of use the wrapper has an interface almost identical to the flash.filesystem libraries for AIR except that synchronous operations are excluded due to the limitations of the flash player library.
Now that it's clear how to perform file operations in both environments, we need a way to detect the runtime and establish a way to invoke the appropriate classes accordingly. The type of player is easy to determine using Capabilities.playerType which is set to Desktop in Adobe AIR. The next step is to dynamically load an external SWF file with the libraries corresponding to the player type.
The following piece of code will load the appropriate library.
-
if (Capabilities.playerType == 'Desktop') {
-
// Load dynamic library for Adobe AIR
-
ExternalLibrary.loadAirLib(loadTests);
-
} else {
-
// Load dynamic library for Flash Player
-
ExternalLibrary.loadPlayerLib(loadTests);
-
}
Utilizing the ApplicationDomain functionality we'll be able to import the classes from the external SWF into the parent movie. Having that, we can use getDefinition() to fetch a class reference and assign it to a variable (which will act similar to a static class) or we can instantiate an object from it.
Returns a class reference:
-
ExternalLibrary.static('IFile');
Returns a class instance (object):
-
ExternalLibrary.create('IFile')
File operations
As previously mentioned, the wrapper presents an interface very similar to the original AIR flash.filesystem interface. However all the operations need to be asynchronous due to the fact that we have to keep the interface unified and the synchronous operations aren't possible over plain HTTP. That means we have to fall back to asynchronous mode... this is generally a safer way to program as when accessing network drives or slow disks, it's preferential to be non-blocking for the task to complete.
Example of reading file contents:
-
public class FileRead
-
{
-
public function FileRead()
-
{
-
var iFile = ExternalLibrary.static('IFile');
-
// Creates an object for file 'myFile.txt' on the desktop
-
var aFile = iFile.desktopDirectory.resolvePath('myFile.txt');
-
// File is ready for access only after the STATUS event
-
aFile.addEventListener(iFile.STATUS, onStatus);
-
}
-
-
function onStatus(ev:Event):void
-
{
-
// The event target is the file object
-
var aFile = ev.target;
-
// Open stream for 'myFile.txt'
-
var aStream = ExternalLibrary.create('IFileStream');
-
var iFileMode = ExternalLibrary.static('IFileMode');
-
aStream.addEventListener(Event.COMPLETE, fileCompleteHandler);
-
// Define other nifty event handlers
-
// aStream.addEventListener(ProgressEvent.PROGRESS, fileProgressHandler);
-
// aStream.addEventListener(Event.CLOSE, fileCloseHandler);
-
// aStream.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);
-
// aStream.addEventListener(HTTPStatusEvent.HTTP_STATUS, httpStatusHandler);
-
// aStream.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);
-
aStream.openAsync(aFile, iFileMode.READ);
-
}
-
-
function fileCompleteHandler(ev:Event):void
-
{
-
var aStream = ev.target;
-
var ba = aStream.bytesAvailable;
-
var cs = iFile.systemCharset;
-
var str:String = aStream.readMultiByte(ba, cs);
-
// trace('The file contains: ' + str);
-
aStream.close();
-
}
-
}
To get an idea of how to do other useful things using this library please browse to the flash.filesystem section in Adobe's LiveDocs. All you need to remember is that you must use the asynchronous versions of the methods. Watch out for the ones that are originally synchronous only and were converted to asynchronous only in the wrapper.
Be sure to check out the tests directory and you'll find many examples to learn from.
Image save example
Prerequisites to this example is having an AIR runtime installed and the ability to publish for AIR, if using CS3 you'll need these updates. If using CS4 they are likely built in.
Lets practice using the wrapper library by studying an example project from scratch. This result will download a photo off the web and ten download it to the desktop if local Svn checkout http://troyworks.googlecode.com/svn/trunk/AS3/examples/SaveImage to download the project file sources. The first thing we need to do is prepare an air-lib.swf and fp-lib.swf file. For each one you need to create a new Flash ActionScript 3.0 project, add the classpath for the troyworks.com and set the document class to com.troyworks.io.AirLibSwf and com.troyworks.io.FpLibSwf accordingly. The Version setting needs to be set to Adobe AIR in the Publish settings for the AIR project. Once you publish both projects and no compiler errors occure you will end up with air-lib.swf and fp-lib.swf with the wrapper classes embedded into them.
You are now ready to create the actual image save project which we'll call SaveImage. Set the document class to SaveImage and create a SaveImage.as file alongside the fla file:
-
package
-
{
-
import flash.display.Sprite;
-
import com.troyworks.util.ExternalLibrary;
-
import flash.net.URLLoader;
-
import flash.net.URLLoaderDataFormat;
-
import flash.system.Capabilities;
-
import flash.events.Event;
-
import flash.utils.ByteArray;
-
import flash.net.URLLoader;
-
import flash.net.URLRequest;
-
import flash.display.Loader;
-
import com.adobe.images.JPGEncoder;
-
-
public class SaveImage extends Sprite
-
{
-
// Change this to your own domain
-
protected var imgUrl:String = 'http://blaze.dnc.pl/~tw/flash_cs3_logo.jpg';
-
-
// Image data
-
protected var imgData:ByteArray;
-
-
public function SaveImage():void
-
{
-
if (Capabilities.playerType == 'Desktop') {
-
ExternalLibrary.loadAirLib(init);
-
} else {
-
ExternalLibrary.loadPlayerLib(init);
-
}
-
}
-
-
protected function init(ev:Event):void
-
{
-
var request:URLRequest = new URLRequest(this.imgUrl);
-
var loader:Loader = new Loader;
-
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onImgLoad);
-
loader.load(request);
-
this.addChild(loader);
-
}
-
-
protected function onImgLoad(ev:Event):void
-
{
-
//trace(ev.target.content.bitmapData);
-
var jpgEncoder:JPGEncoder = new JPGEncoder(85);
-
this.imgData = jpgEncoder.encode(ev.target.content.bitmapData);
-
-
// iFile is the definition of file.flash.filesystem.File (static)
-
var iFile = ExternalLibrary.static('IFile');
-
var aFile = iFile.desktopDirectory.resolvePath('flash-logo.jpg');
-
aFile.addEventListener(iFile.STATUS, onStatus);
-
}
-
-
protected function onStatus(ev:Event):void
-
{
-
var aFile = ev.target;
-
-
var aStream = ExternalLibrary.create('IFileStream');
-
var iFileMode = ExternalLibrary.static('IFileMode');
-
-
aStream.addEventListener(Event.CLOSE, onClose);
-
aStream.openAsync(aFile, iFileMode.WRITE);
-
aStream.writeBytes(this.imgData);
-
aStream.close();
-
}
-
-
protected function onClose(ev:Event):void
-
{
-
trace('file written to disk');
-
}
-
}
-
}
Remember that both of the swf files published earlier (fp-lib and air-lib) have to be present in the same directory. The flow of the above code is pretty simple:
- the constructor is invoked (since the project's document class was set to this class) and the proper wrapper library is loaded
- when the library is downloaded the callback handler is invoked (SaveImage::init in this case)
- a sample image is downloaded from the web (a flash logo)
- onImgLoad invokes a jpgEncoder that will translate the image's BitmapData into a ByteArray; a file object is registered for the destination file (the file that we want to save the image into) and we need to wait until it's status clears (file information might be fetched from a remote server)
- when the file details become available the onStatus method opens a stream and writes the binary data into the target file
- reaching onClose() means that the stream closed and the file should be saved (provided that no errors occurred - additional event listeners can be set up to catch them)
Switching the version field in the Publish settings for this project will cause the output swf to save a file on your desktop (when using AIR) or server (when publishing for the flash player).
Server side file operation handler
The script is built in PHP and uses Apache's mod_rewrite to proxy file calls to a PHP handler script. The .htaccess contents are as follows:
-
RewriteEngine On
-
RewriteRule ^files(/.*)?$ receiver.php?data=$1 [L]
Note if you update the .htaccess you will likely have to restart your webserver. Considering the following directory structure:
-
/PostReceiver/.htaccess
-
/PostReceiver/receiver.php
-
/PostReceiver/data
...that means that any HTTP request for http://example.com/PostReceiver/files/* will be parsed by receiver.php. In that PHP script you can determine the type of request (read/write/append/delete/list/etc.) and execute the necessary actions. For example accessing /PostReceiver/files/bDir?mode=list will return the file listing for the directory bDir (/PostReceiver/data/bDir that is since files is just a mod_rewrite keyword in this case). Review the code in the PostReceiver directory in the library package to gain more knowledge about this reference implementation. A good idea would be to download LiveHttpHeaders for Firefox and try out a few requests manually yourself.
In the making: Flash 10 file handling
The newest version of the flash player will be able to save files locally. This fact however does not make this solution obsolete, because it will not handle programmatic file operations. Any time you want to read or save a file you will need to open the system's file browse dialog and have the user to choose the file.


Add a Comment: