Be sure to read the notes at the end of this blog entry!
This week I had the opportunity to record a few videos on PhoneGap for Adobe TV. One of the videos covered the File API and since it was a bit difficult I figured I'd share my findings, and sample code, with others.
My main struggle with the File API was trying to wrap my head around how it worked. The docs weren't entirely clear to me and were a bit confusing. Turns out there's a good reason for that. (Although I'm working to improve the docs.) PhoneGap's File API is really an implementation of the W3 File API. The PhoneGap docs mention something similar in the database area so it makes sense for the File docs to be updated as well. (And as I said - I'm working on that. I did my first pull request to add just such a mention.)
After I figured that out, I then found an incredibly useful article on the File API over at HTML5 Rocks: Exploring the Filesystem APIs. I encourage everyone to read over Eric Bidelman's article. He's got examples for pretty much every part of the API.
At a high level, working with the File API comes down to a few basic concepts:
- First, you request a file system. You can ask for either a persistent or temporary file system. On the desktop, these both point to a sandboxed folder. On PhoneGap, your access is a bit broader, essentially the entire storage system.
- The API supports basic "CRUD" operations for both files and folders.
- The API supports reading and writing to files, both binary and plain text.
- Probably the most difficult aspect (well, not difficult, just a bit unwieldy), is that each and every operation is asynchronous. So to get and read a file involves about 3 or 4 levels of callbacks.
For my Adobe TV video, I built a simple application that demonstrates some of these principles. I began with a few simple buttons that would let me test basic file operations:
In order to do anything, I need access to the file system, and this needs to be done after PhoneGap fires the deviceready event:
//request the persistent file system
window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, onFSSuccess, onError); } function init() {
document.addEventListener("deviceready", onDeviceReady, true);
}
function onDeviceReady() {
If the file system is loaded, onFSSuccess will handle storing a pointer to it while also setting up my event handlers:
function onFSSuccess(fs) {
fileSystem = fs; getById("#dirListingButton").addEventListener("touchstart",doDirectoryListing);
getById("#addFileButton").addEventListener("touchstart",doAppendFile);
getById("#readFileButton").addEventListener("touchstart",doReadFile);
getById("#metadataFileButton").addEventListener("touchstart",doMetadataFile);
getById("#deleteFileButton").addEventListener("touchstart",doDeleteFile); logit( "Got the file system: "+fileSystem.name +"<br/>" +
"root entry name is "+fileSystem.root.name + "<p/>") doDirectoryListing();
}
As a quick aside, getById is simply a wrapper for document.getElementById. (Trying to reduce my dependency on jQuery.) Our fileSystem object has a few properties we can display, like the name for example. It also has a root property which is a pointer to the root directory. (Duh.) The logit function is simply appending to a DIV on the HTML page as a quick debugging technique.
This event handler then fires off doDirectoryListing. This is normally run by the "Show Directory Contents" button but I automatically run it after the file system is opened.
}
s+="<p/>";
logit(s);
} function doDirectoryListing(e) {
//get a directory reader from our FS
var dirReader = fileSystem.root.createReader(); dirReader.readEntries(gotFiles,onError);
}
function gotFiles(entries) {
var s = "";
for(var i=0,len=entries.length; i<len; i++) {
//entry objects include: isFile, isDirectory, name, fullPath
s+= entries[i].fullPath;
if (entries[i].isFile) {
s += " [F]";
}
else {
s += " [D]";
}
s += "<br/>";
Reading bottom to top, the event handler starts off by creating a reader object off the root property of the file system object. To get the files, you simple call readEntries, and use a callback to handle the result. The entries (which can be files or directories) are a simple array of objects. Here's an example of the output:
So what about file reading and writing? Opening a file is simple. You can simply run getFile(name) and the API can (if you want) also create the file if it doesn't exist. This simplifies things a bit. Here's the event handler and call back for clicking "Creating/Append to Test File".
f.createWriter(function(writerOb) {
writerOb.onwrite=function() {
logit("Done writing to file.<p/>");
}
//go to the end of the file...
writerOb.seek(writerOb.length);
writerOb.write("Test at "+new Date().toString() + "\n");
}) } function doAppendFile(e) {
fileSystem.root.getFile("test.txt", {create:true}, appendFile, onError);
}
function appendFile(f) {
Again - please read up from bottom to top. You can see the use of getFile here along with the options after it to ensure an error won't be thrown if it doesn't exist. Appending to a file is done by creating a writer object. Do note - and I screwed this up myself - if you don't seek to the end of the file you'll actually overwrite data as opposed to appending. Now let's look at reading:
function doReadFile(e) {
fileSystem.root.getFile("test.txt", {create:true}, readFile, onError);
}
function readFile(f) {
reader = new FileReader();
reader.onloadend = function(e) {
console.log("go to end");
logit("<pre>" + e.target.result + "</pre><p/>");
}
reader.readAsText(f);
}
As before, we begin by opening the file, and in the success callback, create a FileReader object. You can read text or binary data depending on your needs. In this example our content is all text so we readAsText and in that callback append it to our div.
Now let's look at metadata. This method doesn't return a lot of data - just the modification date of the file/directory.
function doMetadataFile(e) {
fileSystem.root.getFile("test.txt", {create:true}, function(f) {
f.getMetadata(metadataFile,onError);
}, onError);
}
function metadataFile(m) {
logit("File was last modified "+m.modificationTime+"<p/>");
}
Finally - let's look at the delete operation:
function doDeleteFile(e) {
fileSystem.root.getFile("test.txt", {create:true}, function(f) {
f.remove(function() {
logit("File removed<p/>");
});
}, onError);
}
I hope these examples make sense. If it isn't obvious, I slightly tweaked my style as I built each of the sections. Sometimes I wrote the callbacks within the API calls and sometimes I did it separately. I've included the full code below as well as an APK for those of you who want to test on Android.
<!DOCTYPE HTML>
<html> <head>
<meta name="viewport" content="width=320; user-scalable=no" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>Minimal AppLaud App</title> <script type="text/javascript" charset="utf-8" src="phonegap-1.4.1.js"></script>
<script type="text/javascript" charset="utf-8">
var fileSystem; //generic getById
function getById(id) {
return document.querySelector(id);
}
//generic content logger
function logit(s) {
getById("#content").innerHTML += s;
} //generic error handler
function onError(e) {
getById("#content").innerHTML = "<h2>Error</h2>"+e.toString();
} function doDeleteFile(e) {
fileSystem.root.getFile("test.txt", {create:true}, function(f) {
f.remove(function() {
logit("File removed<p/>");
});
}, onError);
} function metadataFile(m) {
logit("File was last modified "+m.modificationTime+"<p/>");
} function doMetadataFile(e) {
fileSystem.root.getFile("test.txt", {create:true}, function(f) {
f.getMetadata(metadataFile,onError);
}, onError);
} function readFile(f) {
reader = new FileReader();
reader.onloadend = function(e) {
console.log("go to end");
logit("<pre>" + e.target.result + "</pre><p/>");
}
reader.readAsText(f);
} function doReadFile(e) {
fileSystem.root.getFile("test.txt", {create:true}, readFile, onError);
} function appendFile(f) { f.createWriter(function(writerOb) {
writerOb.onwrite=function() {
logit("Done writing to file.<p/>");
}
//go to the end of the file...
writerOb.seek(writerOb.length);
writerOb.write("Test at "+new Date().toString() + "\n");
}) } function doAppendFile(e) {
fileSystem.root.getFile("test.txt", {create:true}, appendFile, onError);
} function gotFiles(entries) {
var s = "";
for(var i=0,len=entries.length; i<len; i++) {
//entry objects include: isFile, isDirectory, name, fullPath
s+= entries[i].fullPath;
if (entries[i].isFile) {
s += " [F]";
}
else {
s += " [D]";
}
s += "<br/>"; }
s+="<p/>";
logit(s);
} function doDirectoryListing(e) {
//get a directory reader from our FS
var dirReader = fileSystem.root.createReader(); dirReader.readEntries(gotFiles,onError);
} function onFSSuccess(fs) {
fileSystem = fs; getById("#dirListingButton").addEventListener("touchstart",doDirectoryListing);
getById("#addFileButton").addEventListener("touchstart",doAppendFile);
getById("#readFileButton").addEventListener("touchstart",doReadFile);
getById("#metadataFileButton").addEventListener("touchstart",doMetadataFile);
getById("#deleteFileButton").addEventListener("touchstart",doDeleteFile); logit( "Got the file system: "+fileSystem.name +"<br/>" +
"root entry name is "+fileSystem.root.name + "<p/>") doDirectoryListing();
} function onDeviceReady() { //request the persistent file system
window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, onFSSuccess, onError); } function init() {
document.addEventListener("deviceready", onDeviceReady, true);
} <style>
button { width: 100%; padding: 5px; }
</style>
</head> <body onload="init();" id="stage" class="theme"> <button id="addFileButton">Create/Append to Test File</button>
<button id="readFileButton">Read Test File</button>
<button id="metadataFileButton">Get Test File Metadata</button>
<button id="deleteFileButton">Delete Test File</button>
<button id="dirListingButton">Show Directory Contents</button> <div id="content"></div> </body>
</html>
</script>
Edited August 27, 2013: Just as an FYI, the File API has been updated a few times since I wrote this blog post. You will note the version link in the text above is for PhoneGap 1.5. PhoneGap is now version 3. One of the big places it changed was in the readAsText area. In my code, I call getFile from the file system object and then pass that into the reader object via the readAsText method. That does not work now. The object passed to getFile is a FileEntry object. You can think of it as a higher-level container for file data. That object has a file() method that returns the file. That thing can then be used in readAsText. You can see an example of this modification in this PhoneGap Google Group posting: Post