-
Notifications
You must be signed in to change notification settings - Fork 3
Appendix D: File System Providers
One of the key components of the CoreApp
framework is the the use of File System providers to abstract away all interactions with file systems. Its purpose is to allow any client code to save and load data with no specific knowledge of how this is performed, or even where the data will be physically located.
At its heart, the FileSystemProvider
system is a group of interfaces that reside in this package:
core.app.core.managers.fileSystemProviders
Lets start by looking at the the most important of these interfaces.
package core.app.core.managers.fileSystemProviders
{
import flash.events.IEventDispatcher;
import flash.utils.ByteArray;
import core.app.core.managers.fileSystemProviders.operations.ICreateDirectoryOperation;
import core.app.core.managers.fileSystemProviders.operations.IDeleteFileOperation;
import core.app.core.managers.fileSystemProviders.operations.IDoesFileExistOperation;
import core.app.core.managers.fileSystemProviders.operations.IGetDirectoryContentsOperation;
import core.app.core.managers.fileSystemProviders.operations.IReadFileOperation;
import core.app.core.managers.fileSystemProviders.operations.ITraverseAllDirectoriesOperation;
import core.app.core.managers.fileSystemProviders.operations.ITraverseToDirectoryOperation;
import core.app.core.managers.fileSystemProviders.operations.IWriteFileOperation;
import core.app.entities.URI;
[Event( type="core.app.events.FileSystemProviderEvent", name="operationBegin" )]
public interface IFileSystemProvider extends IEventDispatcher
{
function get id():String;
function get label():String;
function createDirectory(uri:URI):ICreateDirectoryOperation;
function deleteFile(uri:URI):IDeleteFileOperation;
function doesFileExist(uri:URI):IDoesFileExistOperation;
function getDirectoryContents(uri:URI):IGetDirectoryContentsOperation;
function readFile(uri:URI):IReadFileOperation;
function writeFile(uri:URI, data:ByteArray):IWriteFileOperation;
function traverseToDirectory(uri:URI):ITraverseToDirectoryOperation;
function traverseAllDirectories(uri:URI):ITraverseAllDirectoriesOperation;
}
}
The first and last 2 methods we can ignore for now, it's the middle 6 methods we're interested in. Via these methods, a file system provider is capable of implementing all the operations you would expect a file system to be able to perform. More complicated actions (such as move or copy) can be achieved via multiple uses of these functions (for example copying a file is reading data, then writing to a new location, moving a file is a copy then delete etc).
One thing you may notice is that all of the 6 methods return a type of operation. This is because a FileSystemProvider
doesn't actually do the work itself, but instead delegates it out to individual operations. It works much like the Factory pattern, where the provider determines which concrete operation to return.
To write a file to disk, you would call writeFile()
on your provider, passing through the location (uri) you want to write to, and the bytes you want to be written. The provider would return you an operation which you would then either execute()
directly, or pass it to an OperationManager
to manage. Note that no bytes are written upon calling writeFile()
, you must execute the returned operation before anything will happen.
Ok, so far this has all been a bit abstract. So far we've been using examples where your code is calling methods on some class that implements the IFileSystemProvider
interface - which is great, as this means your code has no knowledge of the concrete type of the class. You could switch the concrete class and your code would now be saving/loading data in a different manner without knowing.
But how is a File system provider actually implemented?
Lets take a concrete example by looking at the LocalFileSystemProvider
.
core.app.managers.fileSystemProviders.local.LocalFileSystemProvider
As you may have guessed, this class implements the IFileSystemProvider
interface and utilises the AIR disk access API to write and read directly from your hard-disk.
If you look at the source file for this, you'll see that in each of the 6 methods it is simply returning an operation that'll do the real work.
public function readFile( uri:URI ):IReadFileOperation
{
var operation:ReadFileOperation = new ReadFileOperation( _rootDirectory, uri, this );
initOperation( operation );
return operation;
}
The ReadFileOperation
class it creates is a concrete instance of the following class.
core.app.managers.fileSystemProviders.local.ReadFileOperation
operations
This operation implements the IReadFileOperation
interface, and uses the AIR API to read bytes off disk.
If you were to write your own FileSystemProvider
, you would pretty much copy->paste something like the LocalFileSystemProvider
, but then make sure you return your own version of the ReadFileOperation
which reads bytes from whatever file system you are adding support for.
The other methods on the IFileSystemProvider
interface are implemented in exactly the same way. So if you want to understand how the LocalFileSystemProvider
works, you can simply look at each of its concrete operations. Once you understand how each operation is contributing one specific bit of functionality it should be much clearer how you'd go about writing another.
You may have spotted this interface sitting alongside the IFileSystemProvider
interface earlier and wondered what it does? Bonus points if you've also spotted that the CoreApp
has a instance of IMultiFileSystemProvider
rather than a IFileSystemProvider
on its global API.
To explain this, we're first going to look at how an operating system like Windows tends to handle file system access.
On most PCs you tend to have a number of 'drives'. So A: is usually the Floppy Drive, C: the main hard disk and D: could be a DVD Drive. The location of data on each of these devices is specified using paths. e.g.
A:/Blackmail_Letter.doc
C:/My Documents/My Pictures/Badger.jpg
D:/Segal/Out For Justice.mpg
The code involved with reading/writing bytes to/from each of these devices requires very different approaches. Under the hood, Windows is mapping the drive letters to a particular type of device and then making sure the correct code to reading/writing to that device is executed.
Within the CoreApp
, we also want to be able to take advantage of accessing multiple 'devices'. For instance, we may want to save and load files locally for speed, but back them up to the cloud now and again. The IMultiFileSystemProvider
allows us to do this. Lets take a quick look at it.
package core.app.core.managers.fileSystemProviders
{
import core.app.entities.FileSystemNode;
import core.app.entities.URI;
public interface IMultiFileSystemProvider extends IFileSystemProvider
{
function registerFileSystemProvider( provider:IFileSystemProvider, visible:Boolean = true ):void;
function get fileSystem():FileSystemNode;
function getFileSystemProviderForURI( uri:URI ):IFileSystemProvider;
}
}
There's really not that much to it. It extends IFileSystemProvider
, so we still have access to all the usual read/write methods and can treat it much the same as another other provider. What it actually does is very simple. You register multiple FileSystemProviders
with it via its registerFileSystemProvider()
method, e.g.
myMultiFileSystemProvider.registerFileSystemProvider( new LocalFileSystemProvider( "local" ) )
myMultiFileSystemProvider.registerFileSystemProvider( new MemoryFileSystemProvider( "memory" ) )
Each Provider can now be accessed like a device. The 'id' parameter we're passing in to the constructors is analogous to the 'A' 'C' and 'D' drive letters we saw earlier, only we're not limited to single letters.
So to write some bytes to the local file system, you would call the multi file system like so.
myMultiFileSystemProvider.writeFile( new URI( "local/MyFile.txt"), someTextBytes );
The MultiFileSystem
simply looks at the first part of the URI and tries to map it to a registered IFileSystemProvider
. If it finds one, it simple passes through the URI (minus the first part of the path) and the bytes to its nested provider. If it can't find one it throws an error.