Filesystem Access and Storage
Edit

Filesystem Access and Storage

Objective

In this chapter, you will learn how to manipulate files and directories by using the Titanium.Filesystem module.

Contents

Depending on your app's needs, storing data as a file might make more sense than storing the information in the database or in a property. Got a few details to store? Put them in the app's properties. Got a lot of structured, related data? That's what a database is for. But if you're storing binary data, such as images, we recommend you store it in files. Fortunately, Titanium makes it simple to perform basic filesystem CRUD operations.

Let's start by examining the modules available to you:

  • Titanium.Filesystem is the top level Filesystem module. You use it to create files and directories, and to get handles to existing files (which you'll then use to read from or write to those files). This module also contains methods that let you determine if storage space is available or if external (SD card) storage is present. With Ti.Filesystem, you can also obtain handles to the various directories accessible by your application.

  • Titanium.Filesystem.File is the file-level object, which includes properties and methods that support common filesystem based operations such as reading, writing, and more.

A few of the ways you might use the filesystem include:

  • Accessing files that ship with your application

  • Saving data your app downloads from a web service

  • Saving data generated by the user to a file, which you might access later or upload to a service

  • And novel uses, such as saving the contents and state of a view as a blob in a file

Storage locations

Before we get into the mechanics of accessing the file system, let's talk about where on the device you can access files. The following locations are potentially accessible:

  • Ti.Filesystem.applicationDataDirectory: A read/write directory accessible by your app. Place your application-specific files in this directory. The contents of this directory persist until you remove the files or until the user uninstalls the application.

  • Ti.Filesystem.resourcesDirectory: A read-only directory where your application resources are located; this directory corresponds to the project/Resources directory in Studio. The contents of this directory persist until the user uninstalls the application.

  • Ti.Filesystem.tempDirectory: A read-write directory where your application can place temporary files. The contents of this directory persist until your application fully closes, at which time the operating system could delete your files.

  • Ti.Filesystem.externalStorageDirectory: A read-write directory on the external storage device (SD card) accessible by your app, if such a location exists. Check first with Ti.Filesystem.isExternalStoragePresent() (which returns a Boolean). Available only for the Android platform.

  • Ti.Filesystem.applicationCacheDirectory: A read-write directory where your application can cache data. The contents of this directory persist after your application fully closes but at the discretion of the operating system. For the Android platform, the cache is limited to 25 MB and the files remain for the lifetime of the application. For the iOS platform, there is no size limit but the data only remains there until iOS cleans the directory if it requires the disk space.

The Ti.Filesystem.resourcesDirectory is read-only on a device, but is read/write in the simulator/emulator.

File operations

In the upcoming sections, we'll look at the various ways you can interact with files. We'll start with the most basic, which is simply getting a reference to the file. Then we'll move on to reading, writing, and 'rithmetic, er, other operations.

Getting a file handle

To work with a file, you need a reference to it, otherwise known as a handle. You do this with the Ti.Filesystem.getFile() method, passing to it the path to and name of the file. What you get back is an instance of the Ti.Filesystem.File object. For example:

var f = Ti.Filesystem.getFile(Ti.Filesystem.applicationDataDirectory,'yourfile.txt');

Reading

Reading from a file is simple enough: get a handle then call the read() method. Keep in mind that the read() method returns the contents of the file as a blob. That's great if your file contains binary data. But if you're working with a text file, grab the text property of that blob to get the plain text contents of the file. Or, you can use the mimeType property to determine the file's MIME type. Like this:

var f = Ti.Filesystem.getFile(Ti.Filesystem.applicationDataDirectory,'yourfile.txt');
var contents = f.read();
Ti.API.info('Output as a blob: '+contents); // useful if contents are binary
Ti.API.info('Output text of the file: '+contents.text);
Ti.API.info('Output the file\'s MIME type: '+contents.mimeType); // e.g. text/plain

Writing

Again, writing to files is straightforward. Get a handle, call write(). Depending on your app, what comes before that call that might be a bit more involved. For example, when saving the state of a JavaScript object, you'll call JSON.stringify() first. Later, you can read in the file and rehydrate the object with JSON.parse().

var f = Ti.Filesystem.getFile(Ti.Filesystem.applicationDataDirectory,'emptyfile.txt');
f.write('The file is no longer empty!'); // write to the file

images/download/attachments/29004902/Screen_shot_2012-01-04_at_10.33.34_AM.png

Appending

There is no distinct (cross-platform) append method, but you can use write() to append to a file. In your statement, pass true as the second argument, as shown in the following code. The results of which are shown in the graphic to the right.

var log = Ti.Filesystem.getFile(Ti.Filesystem.applicationDataDirectory,'logfile.txt');
log.write('My log file\n');
for(var i=0; i<10; i++) {
log.write(i+': new log statement\n', true); // Boolean argument causes write() to append
}
alert(log.read().text);

Creating and copying

Titanium makes it pretty easy to create a file. Grab a file handle, then write to the file. If it doesn't already exist, Titanium will create it for you. There are some specific methods you can use if you want to explicitly create the files. But you don't need to.

var f = Ti.Filesystem.getFile(Ti.Filesystem.applicationDataDirectory,'nonexistent_file.txt');
if(f.exists()===false) {
// you don't need to do this, but you could...
f.createFile();
}
f.write('writing to the file would be enough to create it');

Titanium doesn't include a specific copy() method. Instead, you copy a file by combining reading and writing, like this:

var oldfile = Ti.Filesystem.getFile(Ti.Filesystem.applicationDataDirectory,'old.txt');
var newfile = Ti.Filesystem.getFile(Ti.Filesystem.applicationDataDirectory, 'new.txt');
newfile.write(oldfile.read()); // both old.txt and new.txt exist now

Renaming files

Renaming files follows the same format as above: get a handle, do the operation. But, we need to keep in mind how the file handles are, er, handled. After renaming the file, our file handle will still point to the old name. If you expect it to be automatically updated to point to the new file name, you could be in for some unexpected To demonstrate rename() and this file handle behavior, the following code example previews directories and how you can output a directory listing.

// get a handle to the directory
var dir = Titanium.Filesystem.getFile(Titanium.Filesystem.applicationDataDirectory);
// create our starting file
var f = Ti.Filesystem.getFile(Ti.Filesystem.applicationDataDirectory,'original_name.txt');
f.write('foo');
// rename the file
var success = f.rename('fluffernutter.txt');
if(success==true) {
Ti.API.info('File has been renamed');
} else {
Ti.API.info('File has NOT been renamed');
}
// output a directory listing
Ti.API.info('Dir list after rename = ' + dir.getDirectoryListing());
// But f still points to the old, now non-existent file
Ti.API.info('f.name = ' + f.name); // = 'original_name.txt'
f.write('new information');
Ti.API.info('f contains: ' + f.read());
Ti.API.info('Dir list after writing to f again = ' + dir.getDirectoryListing());
// grab a handle to the copy
var newf = Ti.Filesystem.getFile(Ti.Filesystem.applicationDataDirectory,'fluffernutter.txt');
Ti.API.info('The copy is named ' + newf.name); // = 'fluffernutter.txt'
Ti.API.info(newf.read()); // = 'foo'

Deleting files

We'll end our discussion of file with a look at deleting files. As before, grab a handle and do the operation. The deleteFile() returns a Boolean value indicating whether the operation succeed. This means it won't throw an error if the file doesn't exist or is read-only. You'll just get false back.

var f = Ti.Filesystem.getFile(Ti.Filesystem.applicationDataDirectory,'delete_me.txt');
f.write('foo'); // make sure there's content there so we're sure the file exists
// Before deleting, maybe we could confirm the file exists and is writable
// but we don't really need to as deleteFile() would just return false if it failed
if(f.exists() && f.writeable) {
var success = f.deleteFile();
Ti.API.info((success==true) ? 'success' : 'fail'); // outputs 'success'
}

Directories

We looked at how to list the files in a directory in the preceding section on renaming files. In this section, we'll look at how to create directories, delete directories, and move files into directories. Since the operations are pretty straightforward at this point, we'll do it all in one example:

// create our starting file
var f = Ti.Filesystem.getFile(Ti.Filesystem.applicationDataDirectory,'myfile.txt');
f.write('foo');
// get a handle to the as-yet non-existent directory
var dir = Titanium.Filesystem.getFile(Titanium.Filesystem.applicationDataDirectory,'mysubdir');
dir.createDirectory(); // this creates the directory
Ti.API.info('Directory list to start: ' + dir.getDirectoryListing()); // it's empty
// let's move myfile.txt to our directory
f.move('mysubdir/myfile.txt');
// output a directory listing
Ti.API.info('Dir list after move: ' + dir.getDirectoryListing());
// delete the directory
if(dir.deleteDirectory()==false) {
Ti.API.info('You cannot delete a directory containing files');
dir.deleteDirectory(true); // force a recursive directory, which will delete contents
}
// if we try to list the directory, the output is null because the directory doesn't exist
Ti.API.info('Dir list after deleteDirectory(): ' + dir.getDirectoryListing());
 
// clean the cache directory
var cacheDir = Ti.Filesystem.getFile(Ti.Filesystem.applicationCacheDirectory, "/");
cacheDir.deleteDirectory(true);

Case Sensitivity Note

Transitioning from case-insensitive filesystems, such as FAT32, NTFS and HFS+, to case-sensitive filesystems on Android and Mobile Web devices means that a file name referenced in the source code may not match the case of the file on the device's filesystem. For example, an application may work on the Android emulator but may not work on an Android device, throwing a runtime error or not displaying an image. It is recommended to lowercase all file names. If you change the name of a file, clean your project's build directory before building the application.

Mobile Web Notes

Mobile Web supports file storage, though is subject to limitations placed by the device operating systems. In many cases, Mobile Web apps are limited to 5 MB of total storage for local files imposed by the browser framework.

The Resources directory serves as the "web root" folder for a Mobile Web application. Ti.Filesystem can see all files in the Resources directory except the index.html and titanium/filesystem.registry files. If Ti.Filesystem.getFile() is called with a relative path (i.e. "myimage.jpg"), the Resource directory is automatically prepended to the path.

Files with a MIME type of application/*, image/*, audio/*, or video/* are automatically handled as "binary" where the data internally is stored Base64-encoded.

Hands-on Practice

Goal

In this activity, you will update the local data sample app you worked on in the 5.2 and 5.3 tutorials to save the weather icons to the local filesystem.

Resources

This activity builds upon the app you wrote in sections 5.2 and 5.3. If you don't have a working version of the localdata app representing the end-state of the 5.3 activity, grab this starting point code.

Steps

  1. If necessary, download the starting point code, extract the zip, and import the project.

  2. In app.js, add the filesystem code necessary to create an 'icons' directory within the app's data directory. You should check that the directory exists and create it only if necessary. You'll store cached icon image files in this directory.

  3. Add a myapp.getImage()function that accepts the icon's name as a string. This function will return either a cached image file from the filesystem or load the image from the remote URL and cache it for future use. Your function should implement this general logic:

    • Add liberal logging statements with Ti.API.info() so that you can monitor the actions of your function.

    • Declare a file handle that points to the icon file in the icons directory.

    • If the cached image file exists, return its native path.

    • Otherwise, cache the image but return the icon's absolute URL by implementing this logic:

      • Create an ImageView that loads the image from the remote URL, which would be http://www.worldweather.org/img_cartoon/ plus the icon's name.

      • Because it will take a few seconds to download that image, use setTimeout() to wait 5 seconds. Then, write the results of imageView.toImage() to the file. A successfully retrieved image will be 35 pixels wide. Any other width indicates an error. Cache only successfully retrieved files.

      • To account for the delay in loading and caching, return the icon's absolute URL so that an image is displayed the first time the app is run.

  4. Update the image object in the table so that its image property is set to the return value of your getImage() function.

  5. Build and test the app. Check the logs to confirm that the function loads images from the cache (assuming you logged out appropriate info).

You should notice a momentary pause the first time you run the app. That's because the icon is being loaded from the remote URL. Close the simulator/emulator, launch it again and open your app. The icons should be displayed immediately because they are read from the filesystem this time. If you're testing in the iOS simulator, you can view the icons directory by opening this path: yourHomeDirectory/Library/Application Support/iPhoneSimulator/version/Applications/guid/Documents/icons. You can determine that guid by logging out the iconsfolder.nativePath.

Summary

In this chapter, you learned how to manipulate files and directories by using the Titanium.Filesystem module. You put that knowledge to work by implementing image caching in an app through which remote images are saved to the filesystem for later retrieval.