18. File

The PhoneGap File API provides an application with the methods needed to locate, read, write, copy, move, and remove files in both temporary and persistent file storage on a mobile device. PhoneGap’s implementation of the File API is based in part on the W3C File API: Directories and System specification (www.w3.org/TR/file-system-api). At this time, not all of the capabilities of the W3C specification have been implemented, but the API provides the essential capabilities a mobile developer will be interested in.


Example Application

A sample application, Example 18-1, was created to help illustrate the features of the File API. Because of the length of the application, however, it was not possible to include the complete application source code in this chapter.

Relevant portions of the application’s code and screen shots of the application in action are shown within the chapter, but to see the complete code, you will need to point your browser of choice to the book’s web site at www.phonegapessentials.com and look for the example project files in the Code section of the site.


Available Storage Types

A typical smartphone operating system provides applications with two different types of file storage space it can use. To store temporary files, an application should use the temporary storage location. For content and data that is integral to the application’s operation and must remain available after the application is closed and restarted, the application should use persistent storage.

In general, an application might use temporary storage for transient data, data that’s written to the file system as part of a memory management strategy or as swap space when analyzing or manipulating a large amount of data. With temporary storage, the application can read from and write to the storage area with impunity, creating and deleting files as needed within the available storage limitations of the device. It’s even possible that the device will empty temporary storage when the application closes or the device reboots, freeing up storage space for other applications.

Persistent storage is more stable; the device OS protects it during reboots and when the application closes. An application’s persistent storage will be emptied by the OS when the application is uninstalled from the device.

Accessing the Device’s File System

If an application needs to browse the file system looking for files and directories, the application must first request a handle to it using the following code:

window.requestFileSystem(fileSystemConstant, sandboxSize,
  onSuccessFunction, onErrorFunction);

In this example, a file system sandbox is created for the application to use. The possible values for the fileSystemConstant constant are listed here; these are used to specify which type of storage will be used by the application:

LocalFileSystem.PERSISTENT

LocalFileSystem.TEMPORARY

When calling requestFileSystem, the application requests the allocation of storage it thinks it will need using the sandboxSize parameter shown in the example. There’s really not a lot of information in the documentation or forums about this parameter, but in general, you will want to make sure you allocate enough space for the temporary files your application will be creating. The size requested is checked against the free space on the device to verify there is enough space available. A FileError.QUOTA_EXCEEDED_ERR will be returned if there is not enough space available on the device. When an application writes data to files, there is no check performed to make sure you don’t use more than the requested space.

To request access to 5 MB of temporary sandbox storage, the application would execute the following code:

window.requestFileSystem(LocalFileSystem.TEMPORARY,
  5 * 1024 * 1024, onSuccessFunction, onErrorFunction);

You could also use the following code, but for me, the previous example is easier to understand what’s happening. This option is of course less work for the application since it doesn’t have to do the math every time it runs; it’s your call—readability vs. performance.

window.requestFileSystem(LocalFileSystem.TEMPORARY,
  5242880, onSuccessFunction, onErrorFunction);

The onSucessFunction and onErrorFunction parameters define the functions that are called when the request completes if there is an error encountered during the process. The onSuccessFunction will be executed when the call to requestFileSystem completes. The function is passed a file system object (fs in this case) that can be used to directly interact with the file system, as will be shown in the following section.

function onSuccessFunction(fs) {
  alert("Accessing " + fs.name + " storage (" +
    fs.root.fullPath + ")");
  //Do something with the file system (fs) here
}

The onErrorFunction is executed when there is an error with most of the methods defined in the File API. The information provided here will be relevant to most of the other examples provided in this chapter. Passed to the onErrorFunction is an error object that can be queried to determine the nature of the problem. The following list of constants define the possible values that can be returned by the File API for file and directory access problems:

FileError.ABORT_ERR

FileError.ENCODING_ERR

FileError.INVALID_MODIFICATION_ERR

FileError.INVALID_STATE_ERR

FileError.NO_MODIFICATION_ALLOWED_ERR

FileError.NOT_FOUND_ERR

FileError.NOT_READABLE_ERR

FileError.PATH_EXISTS_ERR

FileError.QUOTA_EXCEEDED_ERR

FileError.SECURITY_ERR

FileError.SYNTAX_ERR

FileError.TYPE_MISMATCH_ERR

The following is an example of a function that can be used within an application to display an error message to users. In this example, the code uses e.code property to determine the error condition (using the error constants listed earlier).

function onFileError(e) {
  var msgText;
  switch(e.code) {
    case FileError.NOT_FOUND_ERR:
      msgText = "File not found error.";
      break;
    case FileError.SECURITY_ERR:
      msgText = "Security error.";
      break;
    case FileError.ABORT_ERR:
      msgText = "Abort error.";
      break;
    case FileError.NOT_READABLE_ERR:
      msgText = "Not readable error.";
      break;
    case FileError.ENCODING_ERR:
      msgText = "Encoding error.";
      break;
    case FileError.NO_MODIFICATION_ALLOWED_ERR:
      msgText = "No modification allowed.";
      break;
    case FileError.INVALID_STATE_ERR:
      msgText = "Invalid state.";
      break;
    case FileError.SYNTAX_ERR:
      msgText = "Syntax error.";
      break;
    case FileError.INVALID_MODIFICATION_ERR:
      msgText = "Invalid modification.";
      break;
    case FileError.QUOTA_EXCEEDED_ERR:
      msgText = "Quota exceeded.";
      break;
    case FileError.TYPE_MISMATCH_ERR:
      msgText = "Type mismatch.";
      break;
    case FileError.PATH_EXISTS_ERR:
      msgText = "Path exists error.";
      break;
    default:
      msgText = "Unknown error.";
  }
  //Now tell the user what happened
  navigator.notification.alert(msgText, null, "File Error");
}

When working with individual files, like the image file path information you get back from the Camera (Chapter 11) and Capture (Chapter 12) APIs, you don’t need access to the file system directly; you can just work with the file individually.

Reading Directory Entries

Once you have access to the file system (either through the persistent or temporary storage area), you have the ability to process directory entries using the File API’s DirectoryReader object. To create a DirectoryReader object, an application must make a call to createReader, as shown in the following function:

function onGetFileSystemSuccess(fs) {
  alert("Accessing " + fs.name + " storage (" +
    fs.root.fullPath + ")");
  //Create a directory reader we'll use to list the files in
  //the directory
  var dr = fs.root.createReader();
  // Get a list of all the entries in the directory
  dr.readEntries(onDirReaderSuccess, onFileError);
}

In this example, the call to createReader is made in the callback function executed after requesting a file system object as described in the previous section. Here the application uses the file system object to create a DirectoryReader pointing at the root folder of the selected file system. The DirectoryReader (dr in the example) supports only a single method, readEntries, which is used to read all of the entries in the specified folder.

As shown in the following example function, the callback function executed when a DirectoryReader has been successfully created is passed a dirEntries object. This object is an array of FileEntry and DirectoryEntry objects that can be accessed to obtain information about all of the files and directories in the folder.

function onDirReaderSuccess(dirEntries) {
  var i, fl, len;
  len = dirEntries.length;
  if(len > 0) {
    fl = '<ul data-role="listview">';
    for( i = 0; i < len; i++) {
      if(dirEntries[i].isDirectory == true) {
        fl += '<li><a href="#" onclick="processEntry(' + i +
          '),">Directory: ' + dirEntries[i].name + '</a></li>';
      } else {
        fl += '<li><a href="#" onclick="processEntry(' + i +
          '),">File: ' + dirEntries[i].name + '</a></li>';
      }
    }
    fl += "</ul>";
  } else {
    fl = "<p>No entries found</p>";
  }
  //Update the page content with our directory list
  $('#dirEntries').html(fl);
  //Display the directory entries page
  $.mobile.changePage("#dirList", "slide", false, true);
}

In this example, the function first checks to see whether any entries were found in the directory and then loops through them building an unordered list of list items (using the HTML <ul>, </ul>, <li>, and </li> tags) that are then added to the page. The application will display different content depending on whether the entry is a file or directory.

In this example, I’m using jQuery Mobile (www.jquerymobile.com) to create a more professional-looking UI for the application, so that’s why you’ll see the data-role=”listview” attribute associated with the <ul> tag in the code. The call to $(’#dirEntries’).html(fl) at the end of the function is a function of jQuery (www.jquery.com) that provides a quick method for updating the content of the dirEntries <div> on the HTML page I’m using. Finally, the application makes a call to $.mobile.changePage(), which is a jQuery function that switches to a different page within the application.

The capabilities highlighted in the previous paragraph illustrate several of the important reasons why a developer would use jQuery and jQuery Mobile for their applications; it takes away much of the complexity of creating compelling UIs and user experiences. Using the following section of an HTML page, the code we’ve just discussed will generate the interactive screen shown in Figure 18-1.

<section id="dirList" data-role="page" data-add-back-btn="true">
  <header data-role="header">
    <h1>File API Demo</h1>
    <a onclick="writeFile();" data-icon="plus"
      class="ui-btn-right">Write</a>
  </header>
  <div data-role="content">
   <p>File system contents:</p>
   <div id="dirEntries"></div>
   <hr />
   <div id="writeInfo"></div>
  </div>
</section>

Image

Figure 18-1 Example 18-1 running on a BlackBerry Torch 9800 simulator

The Write button shown in the figure will be discussed in the section entitled “Writing Files” later in the chapter.

Accessing FileEntry and DirectoryEntry Properties

The FileEntry and DirectoryEntry objects expose several properties an application can use to obtain additional information about a file or directory entry. The properties that can be accessed by an application are as follows:

fullPath: The complete, absolute path from the root of the file system to the entry

isDirectory: Returns true for DirectoryEntry objects and false for FileEntry objects

isFile: Returns true for FileEntry objects and false for DirectoryEntry objects

name: The file name, excluding path information, for the entry

Before an application can access these properties, it must first have a handle to the entry. In the previous section, the DirectoryReader returned an array of entries, so accessing properties is not that difficult.

To obtain an entry using a file name, use the following code:

fs.root.getFile("sample.txt", { create : false },
  processEntry, onFileError);

fs refers to a FileSystem object obtained from a call to requestFileSystem described earlier in the chapter. Once you have access to a file or directory entry, you can access the properties as shown in the following function. In this example, the processEntry function was passed as a success callback parameter in the call to getFile, so it’s executed automatically as soon as getFile has a handle to the file. The file entry (theEntry in this example) is passed as a parameter to the function.

function processEntry(theEntry) {
  var fi = "";
  fi += '<p><b>Name</b>: ' + theEntry.name + '</p>';
  fi += '<p><b>Full Path</b>: ' + theEntry.fullPath + '</p>';
  fi += '<p><b>URI</b>: ' + theEntry.toURI() + '</p>';
  if(theEntry.isFile == true) {
    fi += '<p>The entry is a file</p>';
  } else {
    fi += '<p>The entry is a directory</p>';
  }
  //Update the page content with information about the file
  $('#fileInfo').html(fi);
  //Display the directory entries page
  $.mobile.changePage("#fileDetails", "slide", false, true);
}

A file can also have metadata associated with it; accessing the metadata requires another method call and a callback function, as shown in the following example:

theEntry.getMetadata(onGetMetadataSuccess, onFileError);

After the call to getMetadata, the onGetMetadataSuccess callback function is executed and passed a metadata object containing additional information about the directory or file entry. The File API currently supports only the modificationTime property, so you can access the property using the following example:

function onGetMetadataSuccess(metadata) {
  alert("File Modification Time:" + metadata.modificationTime);
}

To display any number of metadata properties that could be added in the future, the sample application for this chapter uses the following code instead:

function onGetMetadataSuccess(metadata) {
  var md = '';
  for(aKey in metadata) {
    md += '<b>' + aKey + '</b>: ' + metadata[aKey + br;
  }
  md += hr;
  //Update the page content with information about the file
  $('#fileMetadata').html(md);
}

When used in conjunction with the processEntry function described earlier in this section and the HTML page segment shown next, the application will display a screen similar to the one shown in Figure 18-2.

<section id="fileDetails" data-role="page"
  data-add-back-btn="true">
  <header data-role="header">
    <h1>File API Demo</h1>
  </header>
  <div data-role="content">
    <p><em>Directory Entry Information</em></p>
    <hr />
    <div id="fileInfo"></div>
    <hr />
    <p><em>File Metadata:</em></p>
    <div id="fileMetadata"></div>
    <input type="button" value="View File"
      onclick="viewFile();">
    <input type="button" value="Remove File"
      onclick="removeFile();">
  </div>
</section>

Image

Figure 18-2 Example 18-1: directory entry details

Writing Files

To write data to files in either persistent or temporary storage, an application uses a FileWriter object. To begin the process, the application must first get access to a file object representing the file using the getFile method, as shown in the following example:

theFileSystem.root.getFile('appdata1.txt', {create : true},
  onGetFileSuccess, onFileError);

After the call to getFile, the onGetFileSuccess function is executed and passed the file object that will be used to create the FileWriter, as shown in the following example:

function onGetFileSuccess(theFile) {
  theFile.createWriter(onCreateWriterSuccess, onFileError);
}

Once again, we have another callback to wait for; once the FileWriter has been created, the callback function is executed, and the actual file writing can happen, as illustrated in the following example:

function onCreateWriterSuccess(writer) {
  writer.onwritestart = function(e) {
    console.log("Write start");
  };

  writer.onwriteend = function(e) {
    console.log("Write end");
  };

  writer.onwrite = function(e) {
    console.log("Write completed ");
  };

  writer.onerror = function(e) {
    console.log("Write error: " + e.toString());
  };

  writer.write("File created by Example 18-1: ");
}

The function is passed a writer object, which is used to control the writing of data to the file. The FileWriter exposes several events that are triggered during the write process. An application can associate functions with those events and update the screen, a log file, or the browser console with information about the stats of the process. The following list shows the valid event types associated with the FileWriter:

onabort: Executed when the write process has been aborted through a call to writer.abort()

onerror: Executed when an error occurs during the write process

onwrite: Executed when the write process has completed successfully

onwriteend: Executed when the writer has completed a write request

onwritestart: Executed when the write process starts

There is additional functionality provided by the FileWriter object such as the ability to abort a write, seek a certain location within the file, and truncate the file. Refer to the File API documentation at http://docs.phonegap.com for additional information about these capabilities.

In my testing on the BlackBerry platform, the application couldn’t execute sequential calls to writer.write to write data to the file; only the content from the first write would be written to the file. If I placed calls to alert() between my writes to interrupt the flow of the application, all of the content would be written to the file. There’s clearly an issue when new calls are made to write when previous writes are in process. This is happening because calls to the FileWriter are asynchronous; you can’t make calls to write until the previous write has completed. To get around these issues, one PhoneGap developer has created a useful wrapper that solves the problem; you can find information about the solution here: http://tinyurl.com/bt3kyrl.

Reading Files

The process to read content from files is very similar to what was demonstrated in the previous section. To read files, an application uses a FileReader object. To begin the process, the application must first get access to a file object representing the file using the getFile method, as shown in the following example:

theFileSystem.root.getFile('appdata1.txt', {create : false},
  onGetFileSuccess, onFileError);

If the application already has a handle to a file entry object pointing to the file, it can use the following code:

theEntry.file(onGetFileSuccess, onFileError);

In the onGetFileSuccess callback function, the application creates the FileReader object and then uses it to read the file, as shown in the following example:

function onGetFileSuccess(file) {
  var reader = new FileReader();

  reader.onloadend = function(e) {
    console.log("Read end");
    alert(e.target.result);
  };

  reader.onloadstart = function(e) {
    console.log("Read start");
  };

  reader.onloaderror = function(e) {
    console.log("Read error: " + e.target.error.code);
  };

  reader.readAsText(file);
}

As with the FileWriter, the FileReader object exposes several events that are triggered during the read process. An application can associate functions with those events and update the screen, a log file, or the browser console with information about the stats of the process. The following list shows the valid event types associated with the FileReader:

onabort: Executed when the read process has been aborted through a call to reader.abort()

onerror: Executed when an error occurs during the read process

onload: Executed when the read has completed successfully

onloadend: Executed when the reader has completed the read request

onloadstart: Executed when the read process starts

In this example, the contents of the file are read as text using a call to reader.readAsText(). Once the read has completed, the value stored in e.target.result contains the contents of the file. The FileReader also supports the readAsDataURL method, which reads the file and returns the file’s data as a base64-encoded data URL. Don’t forget what you learned in Chapter 11—retrieving a large file’s contents as raw data may overload the device’s JavaScript processor and crash a PhoneGap application.

Deleting Files or Directories

To remove a file from local storage, an application must first obtain a FileEntry or DirectoryEntry object pointing to the file or directory and then can call the following code to delete it:

theEntry.remove(onRemoveFileSuccess, onFileError);

function onRemoveFileSuccess(entry) {
  var msgText = "Successfully removed " + entry.name;
  console.log(msgText);
  alert(msgText);
}

When deleting a directory, the directory must be empty or the remove operation will fail. To remove a directory that contains files, use the removeRecursively method, which will empty the directory before removing it.

In my testing of the sample application, I was able to successfully remove files, but the application would call the onFileError function and return a FileError.INVALID_MODIFICATION_ERR error code. In Bryce’s testing on an Android device, it worked without error, so there’s likely a bug somewhere that needs to be addressed.

Copying Files or Directories

To copy a file or directory, an application must first obtain a FileEntry or DirectoryEntry object pointing to the file or directory and then call the following code to copy it to a new location:

theEntry.copyTo(parentEntry, newName, onSuccessFunction,
  onErrorFunction);

The parentEntry parameter refers to the directory where the file or directory will be copied. Directory copies are recursive, so the process will copy the directory as well as the contents of the directory.

The newName parameter defines the name for the file or directory in the destination directory. This parameter is optional; if you don’t include it, the file or directory’s current name will be used. This parameter is required if copying a file to the same directory.

The onSuccessFunction and onErrorFunction used here are the same as you’ve seen in many other examples; the onSuccesFunction is the function that is executed when the copy process completes, and the onErrorFunction is the function that is executed when an error occurs during the copy process.

The standard limitations you would expect from any file action apply here. When copying a file or directory to the same directory (essentially renaming it), you must supply a new name for the file or directory; otherwise, the copy process will fail. Also, you cannot copy a directory inside itself.

Moving Files or Directories

To move a file or directory, an application must first obtain a FileEntry or DirectoryEntry object pointing to the file or directory and then call the following code to move the file to a new location:

theEntry.moveTo(parentEntry, newName, onSuccessFunction,
  onErrorFunction);

The parentEntry parameter refers to the directory where the file or directory will be moved. Directory moves are recursive, so the process will move the directory as well as the contents of the directory.

The newName parameter defines the name for the file or directory in the destination directory. This parameter is optional, if you don’t include it, the file or directory’s current name will be used. This parameter is required if you are moving a file to the same directory, which is essentially renaming the file.

The onSuccessFunction and onErrorFunction used here are the same as you’ve seen in many other examples; the onSuccessFunction is the function that is executed when the move process completes, and the onErrorFunction is the function that is executed when an error occurs during the move process.

The standard limitations you would expect from any file action apply here. When moving a file or directory to the same directory (essentially renaming it), you must supply a new name for the file or directory; otherwise, the move process will fail. Also, you cannot move a directory to a directory inside itself.

Uploading Files to a Server

The PhoneGap File API includes a FileTransfer object that allows applications to upload files to a remote server. An application must first create a new FileTransfer object and then call the object’s upload method to begin the data transfer to the server. An example of this is illustrated in the following code:

var ft = new FileTransfer();
ft.upload(fileURI, serverURL, onUploadSuccess, onUploadError,
  fileUploadOptions);

The fileURI parameter references the file path pointing to the file that will be uploaded to the server. The serverURL parameter refers to the server URL that will be accessed to upload the file. The onUploadSucess and onUploadError are the callback functions executed on success and failure of the upload activity.

The fileUploadOptions parameter refers to an object that defines the following option settings that control the upload process:

chunkedMode: Boolean value that controls whether streaming of the HTTP request is performed without internal buffering. If this value is not set, it defaults to true. Apparently ColdFusion doesn’t like this parameter enabled (http://tinyurl.com/7nbpwb3).

fileKey: Defines the name of the form element the file is uploaded to on the server. If this value is not set, it defaults to file.

fileName: The file name for the uploaded file on the remote server. If this value is not set, it defaults to image.jpg.

mimeType: The MIME type of the data you are uploading. If this value is not set, it defaults to image/jpeg.

params: An optional set of key/value pairs that are included in the HTTP request header.

The onUploadSuccess function is passed a result object that can be used to determine the status of the upload. The result object supports the following properties:

bytesSent: The number of bytes sent to the server as part of the upload

responseCode: The HTTP response code returned by the server

response: The HTTP response returned by the server

The following function illustrates how to access these properties in an application:

function onUploadSuccess(ur) {
  console.log("Upload Response Code: " + ur.responseCode);
  console.log("Upload Response: " + ur.response);
  console.log("Upload Bytes Sent: " + ur.bytesSent);
}

Currently iOS does not set values for the responseCode and bytesSent properties.

The FileTransfer object passes an error object to the onUploadError callback function; the code property can be queried to determine the cause of the error as illustrated, in the following example:

function onUploadError(e) {
  var msgText;
  switch(e.code) {
    case FileTransferError.FILE_NOT_FOUND_ERR:
      msgText = "File not found.";
      break;
    case FileTransferError.INVALID_URL_ERR:
      msgText = "Invalid URL.";
      break;
    case FileTransferError.CONNECTION_ERR:
      msgText = "Connection error.";
      break;
    default:
      msgText = "Unknown error.";
  }
  //Now tell the user what happened
  navigator.notification.alert(msgText, null,
     "File Transfer Error");
}

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset