HTML5 provides us with a whole crop of new possibilities, such as drawing with canvas, implementing multimedia with the audio and video APIs, and so on. One of these tools, which is still relatively new, is the File System API. It gives us access to a sandboxed section of the user's local file system, thus filling the gap between desktop and web applications even further! In today's tutorial, we'll go through the basics of this new and exciting API, exploring the most common filesystem tasks. Let's get started!
Introduction
No longer do we need to download and install a given piece of software in order to use it. Simply a web browser and an internet connection gives us the ability to use any web application, anytime, anywhere, and on any platform.
In short, web apps are cool; but, compared to desktop apps, they still have one significant weakness: they don't have a way to interact and organize data into a structured hierarchy of folders - a real filesystem. Fortunately, with the new Filesystem API, this can be changed. This API gives web applications controlled access to a private local filesystem "sandbox," in which they can write and read files, create and list directories, and so on. Although at the time of this writing only Google's Chrome browser supports the "full" implementation of the Filesystem API, it still deserves to be studied as a powerful and convenient form of local storage.
The Filesystem API comes in two different versions. The asynchronous API, which is useful for normal applications, and the synchronous API, reserved for use with web workers. For the purposes of this tutorial, we will exclusively explore the asynchronous version of the API.
Step 1 - Getting Started
Your first step is to obtain access to the HTML5 Filesystem by requesting a LocalFile
System object, using the window.requestFileSystem()
global method:
window.requestFileSystem(type, size, successCallback, opt_errorCallback)
There's no way for a web application to "break out" beyond the local root directory.
As the first two parameters, you specify the lifetime and size of the filesystem you want. A PERSISTENT filesystem is suitable for web apps that want to store user data permanently. The browser won't delete it, except at the user's explicit request. A TEMPORARY filesystem is appropriate for web apps that want to cache data, but can still operate if the web browser deletes the filesystem. The size of the filesystem is specified in bytes and should be a reasonable upper bound on the amount of data you need to store.
The third parameter is a callback function that is triggered when the user agent successfully provides a filesystem. Its argument is a FileSystem
object. And, lastly, we can add an optional callback function, which is called when an error occurs, or the request for a filesystem is denied. Its argument is a FileError
object. Although this parameter is optional, it's always a good idea to catch errors for users, as there are a number of places where things can go wrong.
The filesystem obtained with these functions depends on the origin of the containing document. All documents or web apps from the same origin (host, port, and protocol) share a filesystem. Two documents or applications from different origins have completely distinct and disjoint filesystems. A filesystem is restricted to a single application and cannot access another application's stored data. It's also isolated from the rest of the files on the user's hard drive, which is a good thing: there's no way for a web application to "break out" beyond the local root directory or otherwise access arbitrary files.
Let's review an example:
window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem; window.requestFileSystem(window.TEMPORARY, 5*1024*1024, initFS, errorHandler); function initFS(fs){ alert("Welcome to Filesystem! It's showtime :)"); // Just to check if everything is OK :) // place the functions you will learn bellow here } function errorHandler(){ console.log('An error occured'); }
This creates a temporary filesystem with 5MB of storage. It then provides a success callback function, which we will use to operate our filesystem. And, of course, an error handler is also added - just in case something goes wrong. Here, the errorHandler()
function is too generic. So if you want, you can create a slightly optimized version, which gives the reader a more descriptive error message:
function errorHandler(err){ var msg = 'An error occured: '; switch (err.code) { case FileError.NOT_FOUND_ERR: msg += 'File or directory not found'; break; case FileError.NOT_READABLE_ERR: msg += 'File or directory not readable'; break; case FileError.PATH_EXISTS_ERR: msg += 'File or directory already exists'; break; case FileError.TYPE_MISMATCH_ERR: msg += 'Invalid filetype'; break; default: msg += 'Unknown Error'; break; }; console.log(msg); };
The filesystem object you obtain has a name
(a unique name for the filesystem, assigned by the browser) and root
property that refers to the root directory of the filesystem. This is a DirectoryEntry
object, and it may have nested directories that are themselves represented by DirectoryEntry
objects. Each directory in the file system may contain files, represented by FileEntry
objects. The DirectoryEntry
object defines methods for obtaining DirectoryEntry
and FileEntry
objects by pathname (they will optionally create new directories or files if you specify a name that doesn't exist). DirectoryEntry
also defines a createReader()
factory method that returns a DirectoryReader
object for listing the contents of a directory. The FileEntry
class defines a method for obtaining the File
object (a Blob) that represents the contents of a file. You can then use a FileReader
object to read the file. FileEntry
defines another method to return a FileWriter
object that you can use to write content into a file.
Phhew...sounds complicated? Don't worry. Everything will become clearer as we progress through the examples below.
Step 2 - Working With Directories
Obviously, the first thing you need to create in a filesystem is some buckets, or directories. Although the root directory already exists, you don't want to place all of your files there. Directories are created by the DirectoryEntry
object. In the following example, we create a directory, called Documents
, within the root directory:
fs.root.getDirectory('Documents', {create: true}, function(dirEntry) { alert('You have just created the ' + dirEntry.name + ' directory.'); }, errorHandler);
The getDirectory()
method is used both to read and create directories. As the first parameter, you can pass either a name or path as the directory to look up or create. We set the second argument to true
, because we're attempting to create a directory - not read an existing one. And at the end, we add an error callback.
So far, so good. We have a directory; let's now add a subdirectory. The function is exactly the same with one difference: we change the first argument from 'Documents' to 'Documents/Music'. Easy enough; but what if you want to create a subfolder, Sky
, with two parent folders, Images
and Nature
, inside the Documents
folder? If you type 'Documents/Images/Nature/Sky
' for the path argument, you will receive an error, because you can't create a directory, when its immediate parent does not exist. A solution for this is to create each folder one by one: Images
inside Documents
, Nature
inside Images
, and then Sky
inside Nature
. But this is a very slow and inconvenient process. There is a better solution: to create a function which will create all necessary folders automatically.
function createDir(rootDir, folders) { rootDir.getDirectory(folders[0], {create: true}, function(dirEntry) { if (folders.length) { createDir(dirEntry, folders.slice(1)); } }, errorHandler); }; createDir(fs.root, 'Documents/Images/Nature/Sky/'.split('/'));
With this little trick, all we need to do is provide a full path representing the folders which we want to create. Now, the Sky
directory is successfully created, and you can create other files or directories within it.
Now it's time to check what we have in our filesystem. We'll create a DirectoryReader
object, and use the readEntries()
method to read the content of the directory.
fs.root.getDirectory('Documents', {}, function(dirEntry){<br> var dirReader = dirEntry.createReader(); dirReader.readEntries(function(entries) {<br> for(var i = 0; i < entries.length; i++) { var entry = entries[i]; if (entry.isDirectory){ console.log('Directory: ' + entry.fullPath); } else if (entry.isFile){ console.log('File: ' + entry.fullPath); } } }, errorHandler); }, errorHandler);
In the code above, the isDirectory
and isFile
properties are used in order to obtain a different output for directories and files, respectively. Additionally, we use the fullPath
property in order to get the full path of the entry, instead of its name only.
There are two ways to remove a DirectoryEntry
from the filesystem: remove()
and removeRecursively()
. The first one removes a given directory only if it is empty. Otherwise, you'll receive an error.
fs.root.getDirectory('Documents/Music', {}, function(dirEntry) { dirEntry.remove(function(){ console.log('Directory successfully removed.'); }, errorHandler); }, errorHandler);
If the Music
folder has files within it, then you need to use the second method, which recursively deletes the directory and all of its contents.
fs.root.getDirectory('Documents/Music', {}, function(dirEntry) { dirEntry.removeRecursively(function(){ console.log('Directory successufully removed.'); }, errorHandler); }, errorHandler);
Step 3 - Working With Files
Now that we know how to create directories, it's time to populate them with files!
The following example creates an empty test.txt
in the root directory:
fs.root.getFile('test.txt', {create: true, exclusive: true}, function(fileEntry) { alert('A file ' + fileEntry.name + ' was created successfully.'); }, errorHandler);
The first argument to getFile()
can be an absolute or relative path, but it must be valid. For instance, it is an error to attempt to create a file, when its immediate parent does not exist. The second argument is an object literal, describing the function's behavior if the file does not exist. In this example, create: true
creates the file if it doesn't exist and throws an error if it does (exclusive: true
). Otherwise, if create: false
, the file is simply fetched and returned.
Having an empty file is not very useful, though; so let's add some content inside. We can use the FileWriter
object for this.
fs.root.getFile('test.txt', {create: false}, function(fileEntry) { fileEntry.createWriter(function(fileWriter) { window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder; var bb = new BlobBuilder(); bb.append('Filesystem API is awesome!'); fileWriter.write(bb.getBlob('text/plain')); }, errorHandler); }, errorHandler);
Above, we retrieve the test.txt
file, and create a FileWriter
object for it. We then append content to it by creating a new BlobBuilder
object and using the write()
method of FileWriter
.
Calling getFile()
only retrieves a FileEntry
. It does not return the contents of the file. So, if we want to read the content of the file, we need to use the File
object and the FileReader
object.
fs.root.getFile('test.txt', {}, function(fileEntry) { fileEntry.file(function(file) { var reader = new FileReader(); reader.onloadend = function(e) { alert(this.result); }; reader.readAsText(file); }, errorHandler); }, errorHandler);
We have written some content to our file, but what if desire to add more at a later date? To append data to an existing file, the FileWriter
is used once again. We can reposition the writer to the end of the file, using the seek()
method. seek
accepts a byte offset as an argument, and sets the file writer's position to that offset.
fs.root.getFile('test.txt', {create: false}, function(fileEntry) { fileEntry.createWriter(function(fileWriter) { fileWriter.seek(fileWriter.length); window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder; var bb = new BlobBuilder(); bb.append('Yes, it is!'); fileWriter.write(bb.getBlob('text/plain')); }, errorHandler); }, errorHandler);
To remove a file from the filesystem, simply call entry.remove()
. The first argument to this method is a zero-parameter callback function, which is called when the file is successfully deleted. The second is an optional error callback if any errors occur.
fs.root.getFile('test.txt', {create: false}, function(fileEntry) { fileEntry.remove(function() { console.log('File successufully removed.'); }, errorHandler); }, errorHandler);
Step 4 - Manipulating Files and Directories
FileEntry
and DirectoryEntry
share the same API methods for copying, moving and renaming entries. There are two methods you can use for these operations: copyTo()
and moveTo()
. They both accept the exact same parameters:
copyTo(parentDirEntry, opt_newName, opt_successCallback, opt_errorCallback); moveTo(parentDirEntry, opt_newName, opt_successCallback, opt_errorCallback);
The first parameter is the parent folder to move/copy the entry into. The second is an optional new name to give the moved/copied entry, which is actually required when you copy an entry in the same folder; otherwise you will get an error. The third and fourth parameters were explained previously.
Let's review some simple examples. In the following one, we copy the file test.txt
from the root
to the Documents
directory.
function copy(currDir, srcEntry, destDir) { currDir.getFile(srcEntry, {}, function(fileEntry) { currDir.getDirectory(destDir, {}, function(dirEntry) { fileEntry.copyTo(dirEntry); }, errorHandler); }, errorHandler); } copy(fs.root, 'test.txt', 'Documents/');
This next example moves test.txt
to Documents
, instead of copying it:
function move(currDir, srcEntry, dirName) { currDir.getFile(srcEntry, {}, function(fileEntry) { currDir.getDirectory(dirName, {}, function(dirEntry) { fileEntry.moveTo(dirEntry); }, errorHandler); }, errorHandler); } move(fs.root, 'test.txt', 'Documents/');
The following example renames test.txt
to text.txt
:
function rename(currDir, srcEntry, newName) { currDir.getFile(srcEntry, {}, function(fileEntry) { fileEntry.moveTo(currDir, newName); }, errorHandler); } rename(fs.root, 'test.txt', 'text.txt');
Learn More
In this introductory tutorial, we've only scratched the surface of the different filesystem interfaces. If you want to learn more and dig deeper into Filesystem API, you should refer to the W3C specifications specifications:
Now that you have a basic understanding of what the Filesystem API is, and how it can be used, it should be considerably easier to understand the API documentation, which can be a bit confusing at first sight.
Conclusion
The Filesystem API is a powerful and easy to use technology, which provides web developers with a whole crop of new possibilities when building web applications. Admittedly, it's still quite new and not widely supported by all major browsers, but this will certainly change in the future. You might as well get a head start!
Comments