AzoftSpotlightHTML5: How to use FileSystem API&File API

HTML5: How to use FileSystem API&File API

By Andrey Rebrov on February 2, 2011

The following article is devoted to  FileSystem API and File API, as well as to their methods and some useful stuff about how to utilize them. The article is a compilation of materials from developers.google.com. Let's start with studying the material.

Introduction

The web application can create, read, preview and record files in the area of users Sandbox with the help of  FileSystem API and File API. The  API despription is devided to the following sections:

Some words about the objects that you will have to work with:

  1. File — allows accessing such reading-only information as name, size, mimetype etc.
  2. FileList — «array» of File objects.
  3. Blob — allows to analyse the file by bites.
 
Browsers support and storage restriction

When I was writing the article only Google Chrome 9+ had the working FileSystem API realization. Up-to-date there are still no dialog boxes for managing files and quotes on storage, furthermore you will have to use the flag –unlimited-quota-for-files (A manifest with the unlimited Storage extension will be enough for applications development for Chrome Web Store). But changes are coming and soon users will receive a possibility to manage the rights for working with files required by the application. Probably you will have to use the flag –allow-file-access-from-files, if you are debugging the application utilizing the file://. If you don’t use this flag the exceptions from type SECURITY_ERR or QUOTA_EXCEEDED_ERR will be dropped.

File system callback

Check that the browser supports the needed functions

// Check the File API support
if (window.File && window.FileReader && window.FileList && window.Blob) {
  // in process
} else {
  alert('File API is not supported by this browser');
}

Web application can call back file system (only in limited «sandbox») calling the following method: window.requestFileSystem():

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

 

type

Storage rules, available values window.TEMPORARY and window.PERSISTENT. data kept with the help of  the TEMPORARY key can be removed at browser's discretion (e.g. if there is no space). If the PERSISTENT key is used, the data can be removed only after actions of user or of application.

size

The storage size in bites that the application will need.

successCallback

Callback-function, is carried on in case of successful call to the filesystem. Its arguments are objects from type FileSystem.

opt_errorCallback

Optional callback-function for correcting errors. It's also called when errors appear in calling the file system. The parameter is an object from type FileError.

If you call the method requestFileSystem() within the bounds of your application for the first time exactly in this moment the storage will be created. It's important to remember that this storage is closed and another application will have no access to it. It also means that the application can not change other files and folders located on the hard drive. Usage example

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

window.requestFileSystem(window.PERSISTENT, 5*1024*1024 /*5MB*/, onInitFs, errorHandler);

 

The FileSystem specification also describes the API for synchronous work, namely the LocalFileSystemSync interface that is supposed to use with Web Workers altogether. But we won't go into detail on this API within this article. Returning to the requestFileSystem() method it's worth describing the probably errors variations:

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);
}

The example above is very easy but it actionaly is a workpiece for the following errors correction.

Working with files

The FileEntry interface is provided for working with files. It has certain methods and features that we are used to associate with the usual files. Here are some examples:

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.toURI(opt_mimeType);  // Currently not implemented in Google Chrome 9.

fileEntry.file(successCallback, opt_errorCallback);
fileEntry.createWriter(successCallback, opt_errorCallback);
...

Let's see some examples of work with the FileEntry.

Creating file

You can get or create a file with the help of the getFile() method in the DirectoryEntry interface. After calling the storage, callback returns us the FileSystem object containing the DirectoryEntry (fs.root), referring to the storage rout folder. The following code will create an empty file «log.txt»:

function onInitFs(fs) {

  fs.root.getFile('log.txt', {create: true, exclusive: true}, function(fileEntry) {
    // fileEntry will have the following functions
    // fileEntry.isFile === true
    // fileEntry.name == 'log.txt'
    // fileEntry.fullPath == '/log.txt'

  }, errorHandler);

}
window.requestFileSystem(window.PERSISTENT, 1024*1024, onInitFs, errorHandler);

 

Well after calling back the file storage we get the FileSystem. In the —Āallback-function we can call back the fs.root.getFile() method, giving the file name that you want to create. You can give relative directory, as well as absolute directory — the core point is it must be correct. E.g. Creating a file will be false if its parent folder doesn’t exist. The second argument of the getFile() method is an object describing the parameters of the object that will be applied to it, if it isn’t created yet. You can get more info in documentation.

Reading file by name

The following code calls back the «log.txt» file and reads its content with the help of the FileReader API, after that it stores all this content into the <textarea> block. If the file doesn't exist an error will be displayed.

function onInitFs(fs) {

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

    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.PERSISTENT, 1024*1024, onInitFs, errorHandler);

 

FileReader also provides the following reading methods:

  • FileReader.readAsBinaryString(Blob|File) — the result will contain a bite-string.
  • FileReader.readAsText(Blob|File, opt_encoding) — the result will contain a text-string. Coding by default — 'UTF-8', is changed by setting an optional parameter.
  • FileReader.readAsDataURL(Blob|File) — output is data URL.
  • FileReader.readAsArrayBuffer(Blob|File) — we get the data in form ArrayBuffer.

In the following example we will read an image and will display its thumbnail:

<style>
  .thumb {
    height: 75px;
    border: 1px solid #000;
    margin: 10px 5px 0 0;
  }
</style>

<input type="file" id="files" name="files[]" multiple />
<output id="list"></output>

<script>
  function handleFileSelect(evt) {
    var files = evt.target.files; // FileList object

    // Loop through the FileList and render image files as thumbnails.
    for (var i = 0, f; f = files[i]; i++) {

      // Only process image files.
      if (!f.type.match('image.*')) {
        continue;
      }

      var reader = new FileReader();

      // Closure to capture the file information.
      reader.onload = (function(theFile) {
        return function(e) {
          // Render thumbnail.
          var span = document.createElement('span');
          span.innerHTML = ['<img class="thumb" src="', e.target.result,
                            '" title="', theFile.name, '"/>'].join('');
          document.getElementById('list').insertBefore(span, null);
        };
      })(f);

      // Read in the image file as a data URL.
      reader.readAsDataURL(f);
    }
  }

  document.getElementById('files').addEventListener('change', handleFileSelect, false);
</script>

 

Sometimes we'll need not the whole file but only a fragment, for this purpose the File.slice(start_byte,length) is very convenient. How it looks:

var blob = file.slice(startingByte, length);
reader.readAsBinaryString(blob);

 

In the following example we can read wether the needed bites or the whole file. Pay special attention to onloadend and evt.target.readyState that will diplace the onload event in this case. (See below about events).

<style>
  #byte_content {
    margin: 5px 0;
    max-height: 100px;
    overflow-y: auto;
    overflow-x: hidden;
  }
  #byte_range { margin-top: 5px; }
</style>

<input type="file" id="file" name="file" /> Read bytes:
<span class="readBytesButtons">
  <button data-startbyte="0" data-endbyte="4">1-5</button>
  <button data-startbyte="5" data-endbyte="14">6-15</button>
  <button data-startbyte="6" data-endbyte="7">7-8</button>
  <button>entire file</button>
</span>
<div id="byte_range"></div>
<div id="byte_content"></div>

<script>
  function readBlob(opt_startByte, opt_stopByte) {

    var files = document.getElementById('files').files;
    if (!files.length) {
      alert('Please select a file!');
      return;
    }

    var file = files[0];
    var start = opt_startByte || 0;
    var stop = opt_stopByte || file.size - 1;

    var reader = new FileReader();

    // If we use onloadend, we need to check the readyState.
    reader.onloadend = function(evt) {
      if (evt.target.readyState == FileReader.DONE) { // DONE == 2
        document.getElementById('byte_content').textContent = evt.target.result;
        document.getElementById('byte_range').textContent =
            ['Read bytes: ', start + 1, ' - ', stop + 1,
             ' of ', file.size, ' byte file'].join('');
      }
    };
    var length =  (stop - start) + 1;
    var blob = file.slice(start, length);
    reader.readAsBinaryString(blob);
  }

  document.querySelector('.readBytesButtons').addEventListener('click', function(evt) {
    if (evt.target.tagName.toLowerCase() == 'button') {
      var startByte = evt.target.getAttribute('data-startbyte');
      var endByte = evt.target.getAttribute('data-endbyte');
      readBlob(startByte, endByte);
    }
  }, false);
</script>

 

About events. FileReader provides the following events types:

  • onloadstart
  • onprogress
  • onload,
  • onabort
  • onerror
  • onloadend

They can be used when we nee to display the file loading process. E.g. like this:

<style>
  #progress_bar {
    margin: 10px 0;
    padding: 3px;
    border: 1px solid #000;
    font-size: 14px;
    clear: both;
    opacity: 0;
    -moz-transition: opacity 1s linear;
    -o-transition: opacity 1s linear;
    -webkit-transition: opacity 1s linear;
  }
  #progress_bar.loading {
    opacity: 1.0;
  }
  #progress_bar .percent {
    background-color: #99ccff;
    height: auto;
    width: 0;
  }
</style>

<input type="file" id="file" name="file" />
<button onclick="abortRead();">Cancel read</button>
<div id="progress_bar"><div class="percent">0%</div></div>

<script>
  var reader;
  var progress = document.querySelector('.percent');

  function abortRead() {
    reader.abort();
  }

  function errorHandler(evt) {
    switch(evt.target.error.code) {
      case evt.target.error.NOT_FOUND_ERR:
        alert('File Not Found!');
        break;
      case evt.target.error.NOT_READABLE_ERR:
        alert('File is not readable');
        break;
      case evt.target.error.ABORT_ERR:
        break; // noop
      default:
        alert('An error occurred reading this file.');
    };
  }

  function updateProgress(evt) {
    // evt is an ProgressEvent.
    if (evt.lengthComputable) {
      var percentLoaded = Math.round((evt.loaded / evt.total) * 100);
      // Increase the progress bar length.
      if (percentLoaded < 100) {
        progress.style.width = percentLoaded + '%';
        progress.textContent = percentLoaded + '%';
      }
    }
  }

  function handleFileSelect(evt) {
    // Reset progress indicator on new file selection.
    progress.style.width = '0%';
    progress.textContent = '0%';

    reader = new FileReader();
    reader.onerror = errorHandler;
    reader.onprogress = updateProgress;
    reader.onabort = function(e) {
      alert('File read cancelled');
    };
    reader.onloadstart = function(e) {
      document.getElementById('progress_bar').className = 'loading';
    };
    reader.onload = function(e) {
      // Ensure that the progress bar displays 100% at the end.
      progress.style.width = '100%';
      progress.textContent = '100%';
      setTimeout("document.getElementById('progress_bar').className='';", 2000);
    }

    // Read in the image file as a binary string.
    reader.readAsBinaryString(evt.target.files[0]);
  }

  document.getElementById('files').addEventListener('change', handleFileSelect, false);
</script>
 
Writing in the  file

With the help of the following code we will create the «log.txt» file (if it doesn't exist yet) and we will write into it 'Ipsum Lorem'.

function onInitFs(fs) {

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

    fileEntry.createWriter(function(fileWriter) {

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

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

      var bb = new BlobBuilder();
      bb.append('Ipsum Lorem');
      fileWriter.write(bb.getBlob('text/plain'));

    }, errorHandler);

  }, errorHandler);

}

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

 

Obviousely we call back the createWriter() method to get the object. Besides we process the events of finishing the enry in the file and the possible creating an error.

Add data to the file

The following code will add 'Hello World' into the end of the file. If there is no file an error will be displayed.

function onInitFs(fs) {

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

    fileEntry.createWriter(function(fileWriter) {

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

      var bb = new BlobBuilder();
      bb.append('Hello World');
      fileWriter.write(bb.getBlob('text/plain'));

    }, errorHandler);

  }, errorHandler);

}

window.requestFileSystem(window.PERSISTENT, 1024*1024, onInitFs, errorHandler);
 
Creating copies of selected files

The following code enables user to select some files using <input type=«file» multiple> and creates copies of these files.

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

  window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
    for (var i = 0, file; file = files[i]; ++i) {

      (function(f) {
        fs.root.getFile(file.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);

};

 

You can use HTML5 Drag and Drop for facilitating the files selection.

 

<div id="drop_zone">Drop files here</div>
<output id="list"></output>

<script>
  function handleFileSelect(evt) {
    evt.stopPropagation();
    evt.preventDefault();

    var files = evt.dataTransfer.files; // FileList object.

    // files is a FileList of File objects. List some properties.
    var output = [];
    for (var i = 0, f; f = files[i]; i++) {
      output.push('<li><strong>', f.name, '</strong> (', f.type || 'n/a', ') - ',
                  f.size, ' bytes, last modified: ',
                  f.lastModifiedDate.toLocaleDateString(), '</li>');
    }
    document.getElementById('list').innerHTML = '<ul>' + output.join('') + '</ul>';
  }

  function handleDragOver(evt) {
    evt.stopPropagation();
    evt.preventDefault();
  }

  // Setup the dnd listeners.
  var dropZone = document.getElementById('drop_zone');
  dropZone.addEventListener('dragover', handleDragOver, false);
  dropZone.addEventListener('drop', handleFileSelect, false);
</script>
 
Files delition

The following code will delite the 'log.txt'.

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

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

  }, errorHandler);
}, errorHandler);

 

The work with directories is carried via the DirectoryEntry, that has many features of the FileEntry ( both have the Entry interface). Here are the methods and features of the DirectoryEntry.

 

dirEntry.isDirectory === true
// The rest is like in the FileEntry
...

var dirReader = dirEntry.createReader(successCallback, opt_errorCallback);
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

For creating and calling directories use getDirectory() interface DirectoryEntry. You can give both name and the way to the directory. Create the MyPictures» directory in the root:

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

Creating subdirectories is very similar to creating directories, though you should remember that if the parent directory doesn't exist an error will be displayed. The following code shows how you can avoid this restriction:

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

function createDir(rootDirEntry, folders) {
  //filter'./' and '/'
  if (folders[0] == '.' || folders[0] == '') {
    folders = folders.slice(1);
  }
  rootDirEntry.getDirectory(folders[0], {create: true}, function(dirEntry) {
    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.PERSISTENT, 1024*1024, onInitFs, errorHandler);

 

Now we have the «music/genres/jazz» directory and we cann call any of its levels and creates new files there. For instance:

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

To know what is there in the directory you should create the DirectoryReader and call its readEntries() method, though it doesn't give the guarantee that the whole directory content will be called (!!!). It means that you should continue calling back the DirectoryReader.readEntries() method, till the result won't become empty. E.g.:

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

function listResults(entries) {
  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 = [];

  var readEntries = function() {
     dirReader.readEntries (function(results) {
      if (!results.length) {
        listResults(entries.sort());
      } else {
        entries = entries.concat(toArray(results));
        readEntries();
      }
    }, errorHandler);
  };

  readEntries();

}

window.requestFileSystem(window.PERSISTENT, 1024*1024, onInitFs, errorHandler);
 
Remove directory

To remove directory use the DirectoryEntry.remove() method. Note that if you will try to remove a directory which is not empty an error will be displayed. Lets remove an empty directory «jazz» from the "/music/genres/":

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

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

  }, errorHandler);
}, errorHandler);
 
Remove directories recursively

If you have a directory which isn't empty but you still want to remove it the method removeRecursively() can be very helpful. It will remove the directory and its whole content. Let's do this operation with the directory «music»:

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

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

  }, errorHandler);
}, errorHandler);
 
Copy, rename and replace

FileEntry and DirectoryEntry are completelly identical in this aspect.

Copy

Both FileEntry and DirectoryEntry have the copyTo() method for copying. In case of directories the method will recursively also create the whole content. Copy «me.png» from one directory into another one:

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

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

  }, errorHandler);
}

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

The FileEntry and the DirectoryEntry have the moveTo() method that allows replacing and renaming directories. The first argument is the parent folder, the second (optional) parameter is a new name. Rename «me.png» to «you.png»:

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

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

 

Move «me.png» from the root directory to the «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.PERSISTENT, 1024*1024, function(fs) {
  move('/me.png', 'newfolder/');
}, errorHandler);
 
Use Cases

HTML5 provides some options how you can keep data locally, but the FileSystem  satisfies needs and wants that databases don't. Usually these are applications that keep lots of binar data or/and give an access to file applications out of browser. Here is the list of possible applications:

  1. Files Uploader
  2. Games and applications working with media files
  3. Audio/photo editor with an off-line mode
  4. Off-line video player
  5. Off-line mail client

 

You will also find a plenty of useful materials here: html-cheatsheet.

Enhanced by Zemanta