ka6/ko/index.html

HTML5 Rocks

The API is broken up into various themes:

  • Reading and manipulating files: File/Blob, FileList, FileReader
  • Creating and writing: Blob(), FileWriter
  • Directories and file system access: DirectoryReader, FileEntry/DirectoryEntry, LocalFileSystem

Browser support & storage limitations

Requesting a file system

// Note: The file system has been prefixed as of Google Chrome 12:
		window.requestFileSystem  = window.requestFileSystem || window.webkitRequestFileSystem;

		window.requestFileSystem(type, size, successCallback, opt_errorCallback)
		

Example usage:

		function onInitFs(fs) {
			console.log('Opened file system: ' + fs.name);
			}

		window.requestFileSystem(window.TEMPORARY, 5*1024*1024 /*5MB*/, onInitFs, errorHandler);
		
		function errorHandler(e) {
			var msg = '';

			switch (e.code) {
				case FileError.QUOTA_EXCEEDED_ERR:
					msg = 'QUOTA_EXCEEDED_ERR';
					break;
				case FileError.NOT_FOUND_ERR:
					msg = 'NOT_FOUND_ERR';
					break;
				case FileError.SECURITY_ERR:
					msg = 'SECURITY_ERR';
					break;
				case FileError.INVALID_MODIFICATION_ERR:
					msg = 'INVALID_MODIFICATION_ERR';
					break;
				case FileError.INVALID_STATE_ERR:
					msg = 'INVALID_STATE_ERR';
					break;
				default:
					msg = 'Unknown Error';
					break;
				};

			console.log('Error: ' + msg);
			}
		
		window.webkitStorageInfo.requestQuota(PERSISTENT, 1024*1024, function(grantedBytes) {
		window.requestFileSystem(PERSISTENT, grantedBytes, onInitFs, errorHandler);
		}, function(e) {
			console.log('Error', e);
			});
		

Properties and methods of FileEntry:

		fileEntry.isFile === true
		fileEntry.isDirectory === false
		fileEntry.name
		fileEntry.fullPath
		...

		fileEntry.getMetadata(successCallback, opt_errorCallback);
		fileEntry.remove(successCallback, opt_errorCallback);
		fileEntry.moveTo(dirEntry, opt_newName, opt_successCallback, opt_errorCallback);
		fileEntry.copyTo(dirEntry, opt_newName, opt_successCallback, opt_errorCallback);
		fileEntry.getParent(successCallback, opt_errorCallback);
		fileEntry.toURL(opt_mimeType);
	
		fileEntry.file(successCallback, opt_errorCallback);
		fileEntry.createWriter(successCallback, opt_errorCallback);
		...
		

The following code creates an empty file called "log.txt" in the root of the app's file system:

		function onInitFs(fs) {

			fs.root.getFile('log.txt', {create: true, exclusive: true}, function(fileEntry) {
	
// fileEntry.name == 'log.txt'
// fileEntry.fullPath == '/log.txt'

			}, errorHandler);

		}

		window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);
		

Reading a file by name

		function onInitFs(fs) {

		fs.root.getFile('log.txt', {}, function(fileEntry) {

// Get a File object representing the file,
// then use FileReader to read its contents.
			fileEntry.file(function(file) {
				var reader = new FileReader();

				reader.onloadend = function(e) {
				var txtArea = document.createElement('textarea');
				txtArea.value = this.result;
				document.body.appendChild(txtArea);
				};

			reader.readAsText(file);
			}, errorHandler);

			}, errorHandler);

		}

	window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);
	

Writing to a file

The following code creates an empty file called "log.txt" (if it doesn't exist) and fills it with the text 'Lorem Ipsum'.

		function onInitFs(fs) {

			fs.root.getFile('log.txt', {create: true}, function(fileEntry) {

// Create a FileWriter object for our FileEntry (log.txt).
			fileEntry.createWriter(function(fileWriter) {

			fileWriter.onwriteend = function(e) {
				console.log('Write completed.');
				};

			fileWriter.onerror = function(e) {
				console.log('Write failed: ' + e.toString());
				};

// Create a new Blob and write it to log.txt.
		var blob = new Blob(['Lorem Ipsum'], {type: 'text/plain'});

		fileWriter.write(blob);

			}, errorHandler);

			}, errorHandler);

		}

		window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);
		

This time, we call the FileEntry's createWriter() method to obtain a FileWriter object. Inside the success callback, event handlers are set up for error and writeend events. The text data is written to the file by creating a blob, appending text to it, and passing the blob to FileWriter.write().

Appending data to a file

The following code appends the text 'Hello World' to the end of our log file. An error is thrown if the file does not exist.

function onInitFs(fs) {

  fs.root.getFile('log.txt', {create: false}, function(fileEntry) {

    // Create a FileWriter object for our FileEntry (log.txt).
    fileEntry.createWriter(function(fileWriter) {

      fileWriter.seek(fileWriter.length); // Start write position at EOF.

      // Create a new Blob and write it to log.txt.
      var blob = new Blob(['Hello World'], {type: 'text/plain'});

      fileWriter.write(blob);

    }, errorHandler);

  }, errorHandler);

}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

Duplicating user-selected files

The following code allows a user to select multiple files using <input type="file" multiple /> and creates copies of those files in the app's sandboxed file system.

<input type="file" id="myfile" multiple />
document.querySelector('#myfile').onchange = function(e) {
  var files = this.files;

  window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
    // Duplicate each file the user selected to the app's fs.
    for (var i = 0, file; file = files[i]; ++i) {

      // Capture current iteration's file in local scope for the getFile() callback.
      (function(f) {
        fs.root.getFile(f.name, {create: true, exclusive: true}, function(fileEntry) {
          fileEntry.createWriter(function(fileWriter) {
            fileWriter.write(f); // Note: write() can take a File or Blob object.
          }, errorHandler);
        }, errorHandler);
      })(file);

    }
  }, errorHandler);

};

Although we've used an input for the file import, one could easily leverage HTML5 Drag and Drop to achieve the same objective.

As noted in the comment, FileWriter.write() can accept a Blob or File. This is because File inherits from Blob. Therefore, all file objects are blobs.

Removing a file

The following code deletes the file 'log.txt'.

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  fs.root.getFile('log.txt', {create: false}, function(fileEntry) {

    fileEntry.remove(function() {
      console.log('File removed.');
    }, errorHandler);

  }, errorHandler);
}, errorHandler);

Working with directories

Directories in the sandbox are represented by the DirectoryEntry interface, which shares most of FileEntry's properties (they inherit from a common Entry interface). However, DirectoryEntry has additional methods for manipulating directories.

Properties and methods of DirectoryEntry:

dirEntry.isDirectory === true
// See the section on FileEntry for other inherited properties/methods.
...

var dirReader = dirEntry.createReader();
dirEntry.getFile(path, opt_flags, opt_successCallback, opt_errorCallback);
dirEntry.getDirectory(path, opt_flags, opt_successCallback, opt_errorCallback);
dirEntry.removeRecursively(successCallback, opt_errorCallback);
...

Creating directories

Use the getDirectory() method of DirectoryEntry to read or create directories. You can pass either a name or path as the directory to look up or create.

For example, the following code creates a directory named "MyPictures" in the root directory:

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  fs.root.getDirectory('MyPictures', {create: true}, function(dirEntry) {
    ...
  }, errorHandler);
}, errorHandler);
  

Subdirectories

Creating a subdirectory is exactly the same as creating any other directory. However, the API throws an error if you attempt to create a directory whose immediate parent does not exist. The solution is to create each directory sequentially, which is rather tricky to do with an asynchronous API.

The following code creates a new hierarchy (music/genres/jazz) in the root of the app's FileSystem by recursively adding each subdirectory after its parent folder has been created.

var path = 'music/genres/jazz/';

function createDir(rootDirEntry, folders) {
  // Throw out './' or '/' and move on to prevent something like '/foo/.//bar'.
  if (folders[0] == '.' || folders[0] == '') {
    folders = folders.slice(1);
  }
  rootDirEntry.getDirectory(folders[0], {create: true}, function(dirEntry) {
    // Recursively add the new subfolder (if we still have another to create).
    if (folders.length) {
      createDir(dirEntry, folders.slice(1));
    }
  }, errorHandler);
};

function onInitFs(fs) {
  createDir(fs.root, path.split('/')); // fs.root is a DirectoryEntry.
}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

Now that "music/genres/jazz" is in place, we can pass its full path to getDirectory() and create new subfolders or files under it. For example:

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  fs.root.getFile('/music/genres/jazz/song.mp3', {create: true}, function(fileEntry) {
    ...
  }, errorHandler);
}, errorHandler);

Reading a directory's contents

To read the contents of a directory, create a DirectoryReader and call its readEntries() method. There is no guarantee that all of a directory's entries will be returned in a single call to readEntries(). That means you need to keep calling DirectoryReader.readEntries() until no more results are returned. The following is code that demonstrates this:

<ul id="filelist"></ul>
function toArray(list) {
  return Array.prototype.slice.call(list || [], 0);
}

function listResults(entries) {
  // Document fragments can improve performance since they're only appended
  // to the DOM once. Only one browser reflow occurs.
  var fragment = document.createDocumentFragment();

  entries.forEach(function(entry, i) {
    var img = entry.isDirectory ? '<img src="folder-icon.gif">' :
                                  '<img src="file-icon.gif">';
    var li = document.createElement('li');
    li.innerHTML = [img, '<span>', entry.name, '</span>'].join('');
    fragment.appendChild(li);
  });

  document.querySelector('#filelist').appendChild(fragment);
}

function onInitFs(fs) {

  var dirReader = fs.root.createReader();
  var entries = [];

  // Call the reader.readEntries() until no more results are returned.
  var readEntries = function() {
     dirReader.readEntries (function(results) {
      if (!results.length) {
        listResults(entries.sort());
      } else {
        entries = entries.concat(toArray(results));
        readEntries();
      }
    }, errorHandler);
  };

  readEntries(); // Start reading dirs.

}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

Removing a directory

The DirectoryEntry.remove() method behaves just like FileEntry's. The difference: attempting to delete a non-empty directory results in an error.

The following removes the empty directory "jazz" from "/music/genres/":

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  fs.root.getDirectory('music/genres/jazz', {}, function(dirEntry) {

    dirEntry.remove(function() {
      console.log('Directory removed.');
    }, errorHandler);

  }, errorHandler);
}, errorHandler);

Recursively removing a directory

If you have a pesky directory that contains entries, removeRecursively() is your friend. It deletes the directory and its contents, recursively.

The following code recursively removes the directory "music" and all the files and directories that it contains:

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  fs.root.getDirectory('/misc/../music', {}, function(dirEntry) {

    dirEntry.removeRecursively(function() {
      console.log('Directory removed.');
    }, errorHandler);

  }, errorHandler);
}, errorHandler);

Copying, renaming, and moving

FileEntry and DirectoryEntry share common operations.

Copying an entry

Both FileEntry and DirectoryEntry have a copyTo() for duplicating existing entries. This method automatically does a recursive copy on folders.

The following code example copies the file "me.png" from one directory to another:

function copy(cwd, src, dest) {
  cwd.getFile(src, {}, function(fileEntry) {

    cwd.getDirectory(dest, {}, function(dirEntry) {
      fileEntry.copyTo(dirEntry);
    }, errorHandler);

  }, errorHandler);
}

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  copy(fs.root, '/folder1/me.png', 'folder2/mypics/');
}, errorHandler);

Moving or renaming an entry

The moveTo() method present in FileEntry and DirectoryEntry allows you to move or rename a file or directory. Its first argument is the parent directory to move the file under, and its second is an optional new name for the file. If a new name isn't provided, the file's original name is used.

The following example renames "me.png" to "you.png", but does not move the file:

function rename(cwd, src, newName) {
  cwd.getFile(src, {}, function(fileEntry) {
    fileEntry.moveTo(cwd, newName);
  }, errorHandler);
}

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  rename(fs.root, 'me.png', 'you.png');
}, errorHandler);

The following example moves "me.png" (located in the root directory) to a folder named "newfolder".

function move(src, dirName) {
  fs.root.getFile(src, {}, function(fileEntry) {

    fs.root.getDirectory(dirName, {}, function(dirEntry) {
      fileEntry.moveTo(dirEntry);
    }, errorHandler);

  }, errorHandler);
}

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  move('/me.png', 'newfolder/');
}, errorHandler);

filesystem: URLs

The FileSystem API exposes a new URL scheme, filesystem:, that can be used to fill src or href attributes. For example, if you wanted to display an image and have its fileEntry, calling toURL() would give you the file's filesystem: URL:

var img = document.createElement('img');
img.src = fileEntry.toURL(); // filesystem:http://example.com/temporary/myfile.png
document.body.appendChild(img);

Alternatively, if you already have a filesystem: URL, resolveLocalFileSystemURL() will get you back the fileEntry:

window.resolveLocalFileSystemURL = window.resolveLocalFileSystemURL ||
                                   window.webkitResolveLocalFileSystemURL;

var url = 'filesystem:http://example.com/temporary/myfile.png';
window.resolveLocalFileSystemURL(url, function(fileEntry) {
  ...
});

Putting it all together

Basic example

This demo lists the files/folders in the filesystem.

HTML5 Terminal

This shell replicates some of the common operations in a UNIX filesystem (such as cd, mkdir, rm, open, and cat) by abstracting the FileSystem API. To add content, open the app, then drag and drop files from your desktop onto the terminal window.

Click to open the HTML5 Wow Terminal
Open the HTML5 Terminal

Use Cases

There are several storage options available in HTML5, but the FileSystem is different in that it aims to satisfy client-side storage use cases not well served by databases. Generally, these are applications that deal with large binary blobs and/or share data with applications outside of the context of the browser.

The specification lists several use cases:

  1. Persistent uploader
    • When a file or directory is selected for upload, it copies the files into a local sandbox and uploads a chunk at a time.
    • Uploads can be restarted after browser crashes, network interruptions, etc.
  2. Video game, music, or other app with lots of media assets
    • It downloads one or several large tarballs, and expands them locally into a directory structure.
    • The same download works on any operating system.
    • It can manage prefetching just the next-to-be-needed assets in the background, so going to the next game level or activating a new feature doesn't require waiting for a download.
    • It uses those assets directly from its local cache, by direct file reads or by handing local URIs to image or video tags, WebGL asset loaders, etc.
    • The files may be of arbitrary binary format.
    • On the server side, a compressed tarball will often be much smaller than a collection of separately-compressed files. Also, 1 tarball instead of 1000 little files will involve fewer seeks, all else being equal.
  3. Audio/Photo editor with offline access or local cache for speed
    • The data blobs are potentially quite large, and are read-write.
    • It may want to do partial writes to files (ovewriting just the ID3/EXIF tags, for example).
    • The ability to organize project files by creating directories would be useful.
    • Edited files should be accessable by client-side applications [iTunes, Picasa].
  4. Offline video viewer
    • It downloads large files (>1GB) for later viewing.
    • It needs efficient seek + streaming.
    • It must be able to hand a URI to the video tag.
    • It should enable access to partly-downloaded files e.g. to let you watch the first episode of the DVD even if your download didn't complete before you got on the plane.
    • It should be able to pull a single episode out of the middle of a download and give just that to the video tag.
  5. Offline Web Mail Client
    • Downloads attachments and stores them locally.
    • Caches user-selected attachments for later upload.
    • Needs to be able to refer to cached attachments and image thumbnails for display and upload.
    • Should be able to trigger the UA's download manager just as if talking to a server.
    • Should be able to upload an email with attachments as a multipart post, rather than sending a file at a time in an XHR.

Reference specifications

Comments

0