January 21, 2014

Chaining HTML5 File operations

Being a consultant, I am often approached with interesting problems, that have even more interesting solutions.

Recently, a colleague showed me a code snippet that was misbehaving. the purpose of the snippet was to create mutliple levels of directories inside the browser sandbox using HTML5 File APIs.

Let's say, we need to create a set of directories, using an array of paths:

var paths = ["dir1/dir2/dir3/", "dir1/dir2/dir4/", "dir1/dir5/"];

The resulting directory tree should look as follows:

/dir1
	/dir2
    	/dir3
        /dir4
	/dir5

When running the client's code, the result was as follows:

/dir1
	/dir5

Only the last path in the array was being created, and after I looked at the source code I knew why.

The developer had a for loop that iterated through the array of paths and ran functions that would navigate into and create directories recursively. Having worked with a similar propblem I knew, that some platforms (BlackBerry 10 in this example), only allow a single HTML5 File operation at a time. This is why, only the last operation was exectuted, and the previous two ignored.
Because File operations are asynchronous, we shouldn't batch them, but rather chain them, by waiting for one to successfully return before doing the next.

I decided to write some code to solve this problem.


//index for a path in the array
var pathIndex = 0;
function createDirs(rootDirPaths) {
	
    //grab the n-th path, split by slashes and store in array
	var paths = rootDirPaths[pathIndex].split("/");
    
    //index of individual directory in a path
	var dirIndex = 0;

	function createDir(rootDir, path, finished) {
		console.log("creating " + paths[dirIndex]);
        /* 
        * rootDir will be modified as we traverse down the
        * directory tree
        */
        rootDir.getDirectory( path ? path : "/" + paths[dirIndex], 			{
			create : true,
			exclusive : false
		}, function(directoryEntry) {
        	//if no more paths, call the finished() callback
			if (!paths[dirIndex + 1]) {
				finished();
				return;
			} else {
            	//otherwise move on to the next path
				dirIndex++;
                //and recursively call createDir
				createDir(directoryEntry, paths[dirIndex], finished);
			}
		}, function(error) {
				console.log("Failed to get directory " + path + " Error code : " + error.code);
			});
	};

	//root createDir function
	createDir(fs.root, null, function() {
		console.log("back to root");
        //if no more paths exist, return
		if (!rootDirPaths[pathIndex + 1])
			return;
		else {
        	//otherwise, increment and run createDirs recursively
			pathIndex++;
			createDirs(rootDirPaths);
		}
	});
};

This code will iterate through an array of paths, creating each one and it's subdirectories. This works cross-platform.

To initialize, just use:

var paths = ["dir1/dir2/dir3/", "dir1/dir2/dir4/", "dir1/dir5/"];
function onFileSystemSuccess(fileSystem) {
	window.fs = fileSystem;
	createDirs(paths);
};
window.requestFileSystem(window.PERSISTENT, 5 * 1024 * 1024, success);