Skip to content

Appendix D: File System Providers

Robert Silverton edited this page Jul 7, 2014 · 2 revisions

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.

IMultiFileSystemProvider

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.


< Previous