From Adobe Labs
By Darron Schall (http://www.darronschall.com)
A key feature Apollo brings is the ability for web applications to interact with the local file system on the user's computer. Examples of the file system interaction exposed through Apollo include activities such as listing the contents of a directory, reading and writing text and/or binary files, copying and moving files, etc. This article will guide you through a high level overview of the Apollo File API, and cover in depth some of the common tasks you'll likely want to perform.
Table of contents <script type="text/javascript">showTocToggle("show","hide")</script> [showhide] |
Requirements
In order to make the most of this article, you need the following software and files:
Prerequisites: General knowledge of ActionScript 3.0.
General overview
With the new File API, Apollo applications can take advantage of an unprecedented level of access to the user's local file system. Apollo applications run in their own security sandbox and aren't confined by the limitations web developers have grown accustomed to inside of the browser. In traditional web applications, the only file system action available is selecting a file to be uploaded to a server. Apollo expands on this ability by enabling developers to perform operations such as:
- Create Files and Directories
- Open and Read Files
- Write Files
- List the contents of a Directory
- Find the user's home or documents directory
- Inspect File and Directory Properties
The Apollo File API centers on the new flash.filesystem package. The File class is the workhorse of the File API. Most of the operations you perform will be done through him. His supporting cast includes the characters FileStream and FileMode. Their roles are broken down in the following manner:
Table 1. File API classes (flash.filesystem.*)
Class | Description |
File | The representation of a File or a Directory |
FileMode | Contains constant strings that specify different ways to open a file, used in the open() and openAsync() method of File |
FileStream | An object used when reading or writing files |
One thing that might be confusing at first is the apparent lack of support for Directories. There exists a File class, but no Directory counterpart. It turns out that files and directories share the same basic operations (copying, moving, deleting, etc.) and are very similar on a conceptual level. Because of this, the directory functionality has been rolled up in the File class. The isDirectory property allows you to easily determine if the object being pointed to is either a file or a directory.
Referencing a file or directory
Creating a reference to a file is usually done in one of two ways. The first way involves the use of one of the directory shortcuts available as static constants of the File class, coupled with the resolve() method to convert a custom string path to the actual file or subdirectory itself, like this:
/* Create a reference to the "example.txt" file in the "apollo" subdirectory of my documents directory. This resolves to C:/Documents and Settings/Darron/My Documents/apollo/example.txt on Windows. */ var file:File = File.documentsDirectory.resolve( "apollo/example.txt" ); // Get a reference to the desktop directory. For me, this points to // C:/Documents and Settings/Darron/Desktop var dir:File = File. desktopDirectory;
The second way involves setting the nativePath property of a File instance to the specific file or directory:
// Create a new file instance first var file:File = new File(); // Use the string path to the file to get a reference to it via nativePath. file.nativePath = "C:/Documents and Settings/Darron/Desktop/example.txt";
Table 2. The static string constants of the File class that point to common directories
Static constant | Description |
appStorageDirectory | A place to store files needed by the application, such a settings or log files. |
appResourceDirectory | The application's installation directory. |
currentDirectory | The current directory of the application when it was launched. This is useful when dealing with command-line parameters. |
desktopDirectory | The "Desktop" directory underneath the user directory. |
documentsDirectory | Staring from user directory, this is the "My Documents" subdirectory on Windows, and the "Documents" subdirectory on Mac OS X. |
userDirectory | The user's home directory. This is typically C:/Documents and Settings/<username>/Desktop on Windows and /Users/<username/Desktop on Mac OS X. |
Once you have a reference to a file or directory, you can start interacting with the file system. The File API provides two different approaches for working with file system data, as you'll see in the next section.
Synchronous vs. asynchronous
When you start exploring the File API, one of the first things you'll notice is that there are two similarly named methods to perform a single operation. For example, to copy a file or directory you use either the copyTo() method, or the copyToAsync() method. While the methods are named very similarly and they do in fact perform the same operation, they are conceptually worlds apart.
In synchronous operations, the code will wait for the method to return before moving on to the next line:
// Copy a file to the desktop var file:File = File.userDirectory.resolve( "apollo/example.txt" ); file.copyTo( File.desktopDirectory.resolve( "Copy of example.txt" ) ); trace( "Copy complete" ); // Displays after "copyTo" finishes up and // the file has been copied
In asynchronous operations, the operation is started in the background and the code execution continues on without waiting for the original operation to complete. An event is generated by the background process when it's done working, letting listeners know that the operation has completed:
// Copy a file to the desktop var file:File = File.userDirectory.resolve( "apollo/example.txt" ); file.copyToAsync( File.desktopDirectory.resolve( "Copy of example.txt" ) ); trace( "Started copy" ); // Displays right away, before the copy can finish // Listen for the "complete" event to know when the background copy process // has completed file.addEventListener( Event.COMPLETE, completeHandler ); public function completeHandler( event:Event ):void { trace( "Copy complete" ); // Displays when the copy finishes }
Both synchronous and asynchronous operations have their place. As you can clearly see, the synchronous method results in less overall code and is more readable and easier to write. However, the downside is that if you are performing a long running process, the application will appear "stuck" until the synchronous process finishes. While the code is waiting for the operation to complete, no other code execution can be done. This means that the display list will remain frozen in time and all animations and user interactions will appear paused.
On the other hand, asynchronous operations require more care to write. Because they kick off the operation in the background, long running processes will not impact the normal use and interaction of the application. You should use the asynchronous methods when performing time-consuming actions.
The following is a list of synchronous methods and their asynchronous counterparts:
Table 3. Synchronous and Asynchronous File API methods
Synchronous methods | Asynchronous methods |
File.copyTo() | File.copyToAsync() |
File.deleteDirectory() | File.deleteDirectoryAsync() |
File.deleteFile() | File.deleteFileAsync() |
File.listDirectory() | File.listDirectoryAsync() |
File.moveTo() | File.moveToAsync() |
File.moveToTrash() | File.moveToTrashAsync() |
FileStream.open() | FileStream.openAsync() |
Reading and writing files
The FileStream class provides the necessary functionality for reading and writing files. Any time you want to interact with a file, the following steps should be performed:
# Create a reference to the file and open it with a FileStream # Perform the read / write operations as necessary # Close the file and free valuable system resources
The first step gets translated into code similar to this:
// Create a reference to the file on the file system var file:File = File.desktopDirectory.resolve( "example.txt" ); // Create a stream and open the file for reading var stream:FileStream = new FileStream(); stream.open( file, FileMode.READ );
Once the file is opened for reading, you can use any of the read methods on the stream from the flash.utils.IDataInput interface. In this case, the file is not writeable because it was opened in read-only mode (see: Ways to Open FileStreams). If it were writeable, the write methods could be used from the flash.utils.IDataOuput interface.
Because the example.txt file is a plain text file for sample purposes, the code below reads the entire contents of the file as a string and then display that output in the console window:
// Read the entire contents of the file as a string var contents:String = stream.readUTFBytes( stream.bytesAvailable ) // Displays "Hello Apollo World" trace( contents );
After the file contents have been examined the only thing left to do now is clean up and close the stream:
// Clean up - free resources that are in use stream.close();
Note that in the above code samples, the synchronous open() method was used for the sake of simplicity. The openAsync() method could have been substituted in its place. The major difference would be that reading and closing the file would occur in an Event.COMPLETE event handler, as demonstrated by the following:
// Create a stream and open the file for asynchronous reading var stream:FileStream = new FileStream(); stream.openAsync( file, FileMode.READ ); // Add the complete event handler to know when the file has been opened stream.addEventListener( Event.COMPLETE, handleComplete ); private function handleComplete( event:Event ):void { // Get the stream reference back from the event object var stream:FileStream = event.target as FileStream; // Read the entire contents of the file as a string var contents:String = stream.readUTFBytes( stream.bytesAvailable ) // Displays "Hello Apollo World" trace( contents ); // Clean up - free resources that are in use stream.close(); }
Ways to open FileStreams
There's more to reading files than simply reading all of the data at once as a string. The FileStream class supports 4 different modes for interacting with files. The mode of interaction is specified as the second parameter in open() or openAsync() by using one of the static string constants from the FileMode class.
Table 4. Different ways to open a FileStream
Open mode constant | Description |
FileMode.READ | The file is opened for read-only. The file must already exist first. |
FileMode.WRITE | The file is opened for write-only. The file will be created if it does not already exist. If the file does already exist, it is overwritten. |
FileMode.APPEND | The file is opened for write-only. The file will be created if it does not exist. Written data will be placed the end of the file. |
FileMode.UPDATE | The file is opened for read-write. If the file doesn't exist, it will be created. Data can be written to or read from any location in the file. |
Creating, writing, and reading a binary file
The process of writing a file is essentially the same as reading a file. You need to choose the appropriate FileMode that allows you to create a file if it doesn't exist. The following example creates a new file, writes some binary data into the file, and then closes it, and then reads the data back in:
// Create a reference to the file on the filesystem var file:File = File.desktopDirectory.resolve( "apollo test.dat" ); // Create a stream and open the file for asynchronous reading var stream:FileStream = new FileStream(); stream.open( file, FileMode.WRITE ); // Write some raw data to the file stream.writeBoolean( false ); stream.writeInt( 1000 ); stream.writeByte( 0xC ); // Clean up stream.close(); // For demo purposes, open the file in read-only mode and read the data back stream.open( file, FileMode.READ ); trace( stream.readBoolean() ); // false trace( stream.readInt() ); // 1000 trace( stream.readByte().toString( 16 ) ); // c // Clean up stream.close();
Working with directories
As mentioned previously, working with directories is essentially the same as working with files. The basic idea is to create a new File instance that references the directory path. Once we have a reference to the directory, there are a few directory specific operations to perform:
Table 5. Common directory operations
Method name | Description |
File.createDirectory() | Creates the directory (and all parent directories) if it does not exist. |
File.deleteDirectory() | Removes the directory. Can be done asynchronously. |
File.listDirectory() | Lists the contents of a directory. Can be done asynchronously. |
File.listRootDirectories() | Lists all of the root directories of the file system. |
Creating a new directory
The following code example will create a new directory and a corresponding subdirectory on the user's desktop.
// Create a reference to the target directory var dir:File = File.desktopDirectory.resolve( "Apollo/Dir Test" ); // Check to see if the directory exists.. if not, then create it if ( !dir.exists ) { dir.createDirectory(); trace( "Directory created." ); } else { trace( "Directory already exists." ); }
Creating a temporary directory
Apollo allows for the ability to create a temporary directory, rather than creating a more permanent directory on the file system. Temporary directories are useful in a number of situations, one of which is a centralized place to store files that are going to get archived as a .zip file.
// Create a temporary directory via a static File method var tempDir:File = File.createTempDirectory(); // Displays C:/Documents and Settings/Darron/Local Settings/Temp/fla4.tmp trace( tempDir.nativePath ); // ... // Delete the temporary directory once we're done with it tempDir.deleteDirectory(); // Clear the variable to prevent accidentally using it at this point tempDir = null;
Once the temporary directory is created, use the reference to the directory as a place to store anything you might need (by creating new files or directory there, or moving/coping items there). When finished, delete the directory and set the variable to null so that you don't accidentally try to reference a directory that no longer exists.
Listing the contents of a directory
Through the listDirectory() and listDirectoryAsync() methods, Apollo makes it easy to iterate through the contents of a directory. In general, listing directory contents should be performed asynchronously. A directory could have any number of children, from 0 upwards into the thousands. Because this could be a potentially long operation, performing it asynchronously will ensure that your application doesn't appear to be frozen while directory contents are retrieved.
The following example shows how to get the contents within a directory, and displays various properties of each item:
// List the contents of the user's home directory var dir:File = File.userDirectory; dir.listDirectoryAsync(); // Listen for the appropriate event to handle when the listing is complete dir.addEventListener( FileListEvent.DIRECTORY_LISTING, handleDirectoryListing ); private function handleDirectoryListing( event:FileListEvent ):void { // Display a header trace( "Name/tSize/tDir?/tCreated On" ); // Loop over all of the files and subdirectories, and display // some relevant information about each item in the console window for each ( var item:File in event.files ) { trace( item.name + "/t" + item.size + "/t" + item.isDirectory + "/t" + item.creationDate ); } }
Where to go from here
This article has demonstrated basic concepts of the new Apollo File API. The File API centers on the File class in the flash.filesystem package and allows developers access to the user's local file system, exposing a new world of functionality.
The File API allows for synchronous and asynchronous operations to be performed. Synchronous operations are easier to read, but have the disadvantage of locking up the application until the process completes. For long-running processes, it's best to use the asynchronous methods to kick the process off in the background, allowing the application to run as it normally would. When the process completes, an event is generated that can be handled and acted upon accordingly.
Working with files and directories is straightforward through Apollo's File API. Pay attention to the FileMode specified when opening a file, as that dictates what actions can be take on the FileStream. Working with files is the same as working with any class that supports the flash.utils.IDataInput and IDataOutput interfaces. If you're familiar with flash.net.Socket or flash.utils.ByteArray, you'll feel right at home.
- For specific information about the classes in the File API, see the flash.filesystem package in the Apollo ASDoc documentation.
- For general information about working with the File API, see "Using the Apollo file APIs" in the Apollo documentation.
- For help about specific problems or for more ideas about what to do with the File API, see the Apollo for Adobe Flex developers pocket guide, specifically Chapter 4 ("Using the File System API") and Chapter 5 ("Working with the File System").
About the author
Darron Schall has a BS in Computer Science from Lehigh University. Darron is an independent consultant specializing in Rich Internet Applications and Flash Platform development. He maintains a Flash Platform related blog at http://www.darronschall.com and is an active voice in the Flash and Flex communities.