74
22.6.5 Reading Blobs
So far, Blobs have been opaque chunks of data that allow only indirect access, through
Blob URLs, to their contents. The FileReader object allows us read access to the
characters or bytes contained in a Blob, and you can think of it as the opposite of a
BlobBuilder. (A better name would be BlobReader, since it works with any Blob, not
just Files.) Since Blobs can be very large objects stored in the filesystem, the API for
reading them is asynchronous, much like the XMLHttpRequest API. A synchronous
version of the API, FileReaderSync, is available in worker threads, although workers
can also use the asynchronous version.
To use a FileReader, first create an instance with the
FileReader()
constructor. Next,
define event handlers. Typically you’ll define handlers for load and error events and
possibly also for progress events. You can do this with
onload
,
onerror
, and
onprogress
or with the standard
addEventListener()
method. FileReader objects also
trigger loadstart, loadend, and abort events, which are like the XMLHttpRequest events
with the same names: see §18.1.4.
Once you’ve created a FileReader and registered suitable event handlers, you must pass
the Blob you want to read to one of four methods:
readAsText()
,
readAsArray
Buffer()
,
readAsDataURL()
, and
readAsBinaryString()
. (You can, of course, call one of
these methods first and then register event handlers—the single-threaded nature of
JavaScript, described in §22.4, means that event handlers will never be called until your
function has returned and the browser is back in its event loop.) The first two methods
are the most important and are the ones covered here. Each of these read methods takes
a Blob as its first argument.
readAsText()
takes an optional second argument that
specifies the name of a text encoding. If you omit the encoding, it will automatically
work with ASCII and UTF-8 text (and also UTF-16 text with a byte-order mark
or BOM).
As the FileReader reads the Blob you’ve specified, it updates its
readyState
property.
The value starts off at 0, indicating that nothing has been read. It changes to 1 when
some data is available, and changes to 2 when the read has completed. The
result
property holds a partial or complete result as a string or ArrayBuffer. You do not nor-
mally poll the
state
and
result
properties, but instead use them from your
onprogress
or
onload
event handler.
Example 22-11 demonstrates how to use the
readAsText()
method to read local text
files that the user selects.
Example 22-11. Reading text files with FileReader
<script>
// Read the specified text file and display it in the <pre> element below
function readfile(f) {
var reader = new FileReader(); // Create a FileReader object
reader.readAsText(f); // Read the file
reader.onload = function() { // Define an event handler
var text = reader.result; // This is the file contents
698 | Chapter 22: HTML5 APIs
58
var out = document.getElementById("output"); // Find output element
out.innerHTML = ""; // Clear it
out.appendChild(document.createTextNode(text)); // Display file contents
}
reader.onerror = function(e) { // If anything goes wrong
console.log("Error", e); // Just log it
};
}
</script>
Select the file to display:
<input type="file" onchange="readfile(this.files[0])"></input>
<pre id="output"></pre>
The
readAsArrayBuffer()
method is similar to
readAsText()
, except that it generally
takes a little more effort to make use of an ArrayBuffer result than a string result.
Example 22-12 is an example that uses
readAsArrayBuffer()
to read the first four bytes
of a file as a big-endian integer.
Example 22-12. Reading the first four bytes of a file
<script>
// Examine the first 4 bytes of the specified blob. If this "magic number"
// identifies the type of the file, asynchronously set a property on the Blob.
function typefile(file) {
var slice = file.slice(0,4); // Only read the start of the file
var reader = new FileReader(); // Create an asynchronous FileReader
reader.readAsArrayBuffer(slice); // Read the slice of the file
reader.onload = function(e) {
var buffer = reader.result; // The result ArrayBuffer
var view = new DataView(buffer); // Get access to the result bytes
var magic = view.getUint32(0, false); // Read 4 bytes, big-endian
switch(magic) { // Determine file type from them
case 0x89504E47: file.verified_type = "image/png"; break;
case 0x47494638: file.verified_type = "image/gif"; break;
case 0x25504446: file.verified_type = "application/pdf"; break;
case 0x504b0304: file.verified_type = "application/zip"; break;
}
console.log(file.name, file.verified_type);
};
}
</script>
<input type="file" onchange="typefile(this.files[0])"></input>
In worker threads, you can use FileReaderSync instead of FileReader. The synchronous
API defines the same
readAsText()
and
readAsArrayBuffer()
methods that take the
same arguments as the asynchronous methods. The difference is that the synchronous
methods block until the operation is complete and return the resulting string or
ArrayBuffer directly, with no need for event handlers. Example 22-14 below uses
FileReaderSync.
22.6 Blobs | 699
Client-Side
JavaScript
47
22.7 The Filesystem API
In §22.6.5, you saw the FileReader class used to read the content of user-selected files,
or of any Blob. The File and Blob types are defined by a draft specification known as
the File API. Another draft specification, even newer than the File API, gives web ap-
plications controlled access to a private local filesystem “sandbox” in which they can
write files, read files, create directories, list directories, and so on. At the time of this
writing, this Filesystem API is implemented only by Google’s Chrome browser, but it
is a powerful and important form of local storage, so it is covered here, even though
the API is even less stable than most of the other APIs described in this chapter. This
section covers basic filesystem tasks but does not demonstrate all features of the API.
Because the API is new and unstable, it is not documented in the reference section of
this book.
Working with files in the local filesystem is a multistep process. First, you must obtain
an object that represents the filesystem itself. There is a synchronous API for doing this
in worker threads and an asynchronous API for use on the main thread:
// Obtaining a filesystem synchronously. Pass filesystem lifetime and size.
// Returns a filesystem object or raises an exception.
var fs = requestFileSystemSync(PERSISTENT, 1024*1024);
// The asynchronous version uses callback functions for success and error
requestFileSystem(TEMPORARY, // lifetime
50*1024*1024, // size: 50Mb
function(fs) { // called with the filesystem object
// Use fs here
}
function(e) { // called with an error object onerror
console.log(e); // Or handle it some other way
});
In both the synchronous and asynchronous versions of the API, you specify the lifetime
and the 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. A browser may enforce this as a quota.
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 com-
pletely distinct and disjoint filesystems. The filesystem is also walled off from the rest
of the files on the user’s hard drive: there is no way for a web application to “break out”
beyond the local root directory or otherwise access arbitrary files.
Note that these functions have “request” in their names. The first time you call one,
the browser may ask the user for permission before creating a filesystem and granting
700 | Chapter 22: HTML5 APIs
54
access.
2
Once permission has been granted, subsequent calls to the request method
should simply return an object that represents the already existing local filesystem.
The filesystem object you obtain with one of the methods above has a
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 for listing the contents of a directory.
The FileEntry class defines a method for obtaining the File object (a Blob) that repre-
sents the contents of a file. You can then use a FileReader object (as shown in
§22.6.5) to read the file. FileEntry defines another method to return a FileWriter object
that you can use to write content into a file.
Reading or writing a file with this API is a multistep process. First you obtain the file-
system object. Then you use the root directory of that object to look up (and optionally
create) the FileEntry object for the file you’re interested in. Then you use the FileEntry
object to obtain the File or FileWriter object for reading or writing. This multistep
process is particularly complex when using the asynchronous API:
// Read the text file "hello.txt" and log its contents.
// The asynchronous API nests functions four deep.
// This example doesn't include any error callbacks.
requestFileSystem(PERSISTENT, 10*1024*1024, function(fs) { // Get filesystem
fs.root.getFile("hello.txt", {}, function(entry) { // Get FileEntry
entry.file(function(file) { // Get File
var reader = new FileReader();
reader.readAsText(file);
reader.onload = function() { // Get file content
console.log(reader.result);
};
});
});
});
Example 22-13 is a more fully fleshed-out example. It demonstrates how to use the
asynchronous API to read files, write files, delete files, create directories, and list
directories.
Example 22-13. Using the asynchronous filesystem API
/*
* These functions have been tested in Google Chrome 10.0 dev.
* You may need to launch Chrome with these options:
* --unlimited-quota-for-files : enables filesystem access
* --allow-file-access-from-files : allows testing from file:// URLs
2.At the time of this writing, Chrome doesn’t ask for permission, but it requires you to launch it with the
--unlimited-quota-for-files
command-line flag.
22.7 The Filesystem API | 701
Client-Side
JavaScript
50
*/
// Lots of the asynchronous functions we use accept an optional error callback.
// This one just logs the error.
function logerr(e) { console.log(e); }
// requestFileSystem() gets us a sandboxed local filesystem accessible only
// to apps from this origin. We can read and write files at will, but
// can't get out of the sandbox to access the rest of the system.
var filesystem; // Assume this is initialized before the funcs below are called.
requestFileSystem(PERSISTENT, // Or TEMPORARY for cache files
10*1024*1024, // We'd like 10 megabytes, please
function(fs) { // When done, call this function
filesystem = fs; // Just save the filesystem into
}, // a global variable.
logerr); // Call this if an error occurs
// Read the contents of the specified file as text and pass them to callback.
function readTextFile(path, callback) {
// Call getFile() to find the FileEntry for the specified filename
filesystem.root.getFile(path, {}, function(entry) {
// This function is called with the FileEntry for the file
// Now we call the FileEntry.file() method to get the File object
entry.file(function(file) { // Call this with the File
var reader = new FileReader(); // Create a FileReader
reader.readAsText(file); // And read the file
reader.onload = function() { // When read successful
callback(reader.result); // Pass it to the callback
}
reader.onerror = logerr; // Log readAsText() errors
}, logerr); // Log file() errors
},
logerr); // Log getFile() errors
}
// Append the specified contents to the file at the specified path, creating
// a new file if no file by that name already exists. Call callback when done.
function appendToFile(path, contents, callback) {
// filesystem.root is the root directory.
filesystem.root.getFile( // Get a FileEntry object
path, // The name and path of the file we want
{create:true}, // Create it if it doesn't already exist
function(entry) { // Call this when it has been found
entry.createWriter( // Create a FileWriter object for the file
function(writer) { // Call this function when created
// By default a writer starts at the beginning of the file.
// We want to start writing at the end of the file
writer.seek(writer.length); // Move to end of file
// Convert file contents to a Blob. The contents argument
// can be a string or a Blob or an ArrayBuffer.
var bb = new BlobBuilder()
bb.append(contents);
var blob = bb.getBlob();
702 | Chapter 22: HTML5 APIs
Documents you may be interested
Documents you may be interested