Toying With the HTML5 File System API

Toying With the HTML5 File System API

Tutorial Details
  • Subject: HTML5
  • Difficulty: Intermediate-Advanced

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.

Can I Use Support}

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!

Tags: html5
Note: Want to add some source code? Type <pre><code> before it and </code></pre> after it. Find out more
  • oyebanji jacob mayowa

    Great tut. Thumbs up to the html5 working team. Hope the statement “There’s no way
    for a web
    application to
    “break out”
    beyond the local
    root directory.” would last the test of time. Thanks tuts+

    • Jules

      “There’s no way for a web application to “break out” beyond the local root directory.”

      I have always found this a source of disappointment myself. Whilst I fully respect the security aspects, and appreciate that it won’t change, it does limit web applications. and in my view prevents them from providing general application functionality or competing fully with native apps.

      For instance, if I wanted to create an application to edit something, the application would be limited to editing files in the local storage, and I would have to separately synchronise those files with my preferred storage location in the general file system (assuming general file systems have a future).

      Using either:

      (a) Microsoft .HTA files + Scripting.FileSystemObject

      (b) Firefox Prism + XPCOM Scriptable FIle System

      (c) Adobe Air

      I have managed to create myself a whole range of stand alone HTML + Javascript based utilities, including a file system synchroniser, a set of Microsoft Word automation functions, an expense management application, and a password management application. In all cases, they need to be able to access the general file system.

      Oh well.

      Jules

  • Thomas

    This is just javascript right? Why is it called HTML5?

    • David

      HTML5 ~= HTML + CSS + JS

      http://slides.html5rocks.com

      • Rob

        No it’s not. HTML = Hypertext Markup Language, and HTML5 is a new standard of HTML which does NOT include CSS or Javascript.

        CSS and Javascript are completely different languages for completely different purposes.

        This is a browser API presented to Javascript. It has no relation to HTML at all.

    • http://blog.shay.co/ Shay Ben Moshe

      I agree with Thomas, stop calling everything HTML5, it is JavaScript!

      • http://thenextlab.com/ Steven

        JavaScript is not cool anymore so better to call it HTML5 even if it’s not :)

    • GMc

      It’s called HTML5 because the Filesystem API is part of the HTML5 grouping of specs (http://dev.w3.org/2009/dap/file-system/pub/FileSystem/).

      The Filesystem API is not language-specific, so while you’ll see most examples in Javascript, the API might also be accessed in other ways, from other languages, once the necessary implementations are in place.

      So that’s why it’s referenced with HTML5. Because it’s grouped up with HTML5. It’s not “just called” Javascript, because it’s not “just Javascript.”

      • Rob

        There is no mention of HTML on that page. What makes you think that it is part of HTML5?

    • Avery

      My understanding would be that the functions required for this implementation, such as “requestFileSystem”, were not available to JavaScript prior to the HTML5 Spec. As such, it is a feature of HTML5, but is implemented through Javascript.

    • Jonathan [JCM]

      This is a HTML5 feature implemented in Javascript.

      It is not a Javascript feature, because it is not in the specification of the EcmaScript.

  • http://skied1382.mihanblog.com selina

    I didn’t know that it’s so easily possible to copy files via javascripts. So interesting and informative article. I personally learned so much reading this post.

  • Anon

    This looks like JavaScript… Why are you calling it HTML?

    HTML:
    <p>Hello, world.</p>

    JS:
    document.write(‘Hello, world.’);

    :/

    • http://www.gustavstromberg.se/ Gustav Strömberg

      sadly HTML5 is a buzzword drawing attention to the article.

      Although HTML5 is the markup, the sweet things one can do with HTML5 is all done by JavaScript…

    • http://www.interactivered.com Interactive Red

      It seems like js and html5 will always be used in the wrong terms like geo location is not actual html5

  • http://www.udgwebdev.com Caio Ribeiro Pereira

    This is Awesome!!
    With this resource we can build an entire local app, runing on the browser, but processing and storing files on the client-side.

  • http://thelucid.com Jamie Hill

    As far as I can see from the API documentation, there is no way of telling when a file is modified manually by the user. This seems like a bit of an oversight as there is no way to update the server when a file is edited locally if you don’t know when that file has changed.

    • Eduards

      use getMetadata();

  • http://www.ilmusetitik.blogspot.com Nanang Gunawan

    We love HTML 5 :)
    Thanks tuts+

  • http://www.ilmusetitik.blogspot.com Nanang Gunawan

    We love HTML 5 :)
    Thanks tuts+

  • Alex

    Hmmmm, it looks like technology with a lot of possibility, but to me it seems only a matter of time until someone finds a bug to breakout the sandbox and inject malicious files to the target system =/

  • phpman

    by the way , did you think as a web developer need to learn html5 new technology or Is sufficient
    to learn php mysql jquery

    • http://gustavstromberg.se Gustav Strömberg

      You have to learn all of them to be a successful web developer. HTML5 is relying heavily on all of the other techniques. The HTML is just markup of content, all other “logic”, “style” and serverside code is in other languages.

      • http://www.gustavstromberg.se/ Gustav Strömberg

        …not to mention where to store all data before presenting it in HTML…

        Learn:
        JavaScript
        CSS
        MySql
        HTML
        php

        :D

  • http://www.html5banneradd.com Ionel

    HTML5 capabilities are great, my question is what happens with the Native app development, clearly would be more efficient to focus on HTML5.

  • http://arsenal360.com Dennis Dinh

    Love HTML5 & CSS3
    Thanks net.tuts+

  • Yurii

    Good tutorial, but for what the server needs to store it’s files on my hard disk? It is good way for viruses to redirect me from one origin to another and fill my disk with unused stuff (and may be used by them).

  • http://herozon.com Zarel

    What a great post, Ivaylo!

    Hope in the next post, you will create an example how to use this function in real world application.

    -

    I am waiting jeffrey to explore some new javascript framework, he tweets everyday about “something” exciting to teach! Can’t waiting for that day! :D

  • https://github.com/zocky/cdjs zocky

    I’m writing a wrapper library for the FileSystem API that should greatly simplify working with the async interface. Check it out at https://github.com/zocky/cdjs .

  • http://www.healthycaremag.com Weerayut Teja

    In the old fashion we only manage file system via ActiveX only. But now we can write the script which can do that by HTML5. That’s cool. We have another way to apply javascript in the multiple ways.
    Thank you for this article. I like it. :)

  • meli

    Hello,

    I tried to create a file system to read and write a file, and i didn’t manage to get passed the creation step:
    - when i run

    window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem;
    window.requestFileSystem(window.TEMPORARY, 5*1024*1024, initFS, errorHandler);

    then nothing happens; meaning i have put some alerts after the call to create a file system, and it outputs them, but it will not enter the initFS or the errorHandler functions.

    I ran it on Google chrome, version 18.0.1025.168

    please help me.

    • Nikolius

      get the same problem with you..
      can anyone please kindly tell what the problem is?
      i run itu on chrome 20

    • skube

      If you are not running an actual server (meaning, you see file:// in location bar) you need to launch Chrome with the following flag –allow-file-access-from-files.

  • http://www.arterruption.net Sally

    I come from design world and trying to learn HTML5 – So the reason I think above difficult for me is has Java scripts that I don’t know it yet- So I have to save this tutorial after I got better in JS

  • Jasdeep Khalsa

    This article is now out of date, please refer to http://www.html5rocks.com/en/tutorials/file/filesystem/

  • http://twitter.com/erikjohnzon Erik Johnson

    You also need to update this article since the readEntries will sometimes not return all directories as it is searching them. Please update.

  • skube

    You have some errant in your readEntries code.

    • skube

      Errant ‘s

      • skube

        ugh. break tags! :)

  • dordi

    when I try second step working with Directories I get an error: Uncaught ReferenceError: fs is not defined.
    Please help!

  • www.allmyexpenses.co.uk

    Thanks for sharing this interesting post. I love it. I just love it so much, that the greedy visual-data gnome in me wants more! http://www.allmyexpenses.co.uk/home.aspx