|
|
Introduction
Starting with version 1.3, MacFUSE Core comes with a framework to assist in developing user-space file systems with Objective-C. The framework resides in /Library/Frameworks/MacFUSE.framework. This HOWTO walks through an example of creating a simple Cocoa-app file system in Objective-C that uses MacFUSE.framework. The author of this HOWTO gave a presentation at CocoaHeads that may be another useful source of information on using MacFUSE.framework. The video for that presentation, which includes a walkthrough of this HOWTO, can be found here.
Requirements
In order to go through this HOWTO, you'll need to:
- Be running XCode 2.5+ on Tiger or Leopard.
- If you haven't already, then install MacFUSE Core.
- Check out the ytfs project that will serve as our starting point as follows (you can do all the work in your /tmp directory if you'd like):
cd /tmp svn checkout http://macfuse.googlecode.com/svn/trunk/filesystems-objc/ytfs-tutorial ytfs
The Tutorial Project
We'll create a simple file system that downloads a feed of top-rated YouTube videos and presents them in the root directory as a clickable webloc to the video. Please open the ytfs.xcodeproj and start looking around.
At this point you should notice that the ytfs project has already been setup to build and run as an application and that there are some methods that have already been completed. This is simply to make the tutorial a bit simpler; we won't miss out on the fun part. The project is a standard Cocoa Application. We've also already opened mainmenu.nib, instantiated a YTController object, and made that the delegate for File's Owner.
If you inspect the files then you'll see the YTFS class and the YTController class. The YTController class will control the lifecycle of both our app and the file system. If you look at YTController.m, you'll see the - (NSDictionary *)fetchVideos method. This simply fetches the xml feed for eleven of the top rated videos. It then populates a dictionary with the keys being the name of the video with .webloc appended and the contents being the xml for that video. The YTFS class will act as a delegate that serves up the file system contents. In there you will find some convenience methods for getting at the xml data for a video given a path string.
Time To Code
I recommend as you follow along in this that you take the time to compile and run the app at each step. This iterative process will give you the best idea about what each step is accomplishing.
Part 1: YTController.m
For the first part, we'll make changes to the YTController.m file.
Add MacFUSE.framework
- Add /Library/Frameworks/MacFUSE.framework to the set of linked frameworks.
- Uncomment the //#import <MacFUSE/GMUserFileSystem.h> line.
Create GMUserFileSystem and Mount
Where it says INSERT CODE HERE, we'll create a GMUserFileSystem object that controls the lifecycle of a file system mount. For now we'll leave the delegate nil, which will essentially mount an empty file system, and we'll claim that our delegate will be thread-safe. We've already declared the fs_ member variable in YTController.h for you.
fs_ = [[GMUserFileSystem alloc] initWithDelegate:nil isThreadSafe:YES]; [fs_ mountAtPath:@"/Volumes/ytfs" withOptions:nil];
If you run the app now, you should find an empty /Volumes/ytfs mount. Notice that if you quit the app it stays mounted; for now you should unmount/eject by hand. Overall it doesn't yet behave very well with respect to your application.
Register For Notifications
If we register for notifications provided by GMUserFileSystem, then we can react in a better fashion when things mount/unmount. We've already provided the implementations for didMount: and didUnmount:. They simply show the mount point and quit the app. Add this code right above where you instantiated the fs_ member variable.
NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
[center addObserver:self selector:@selector(didMount:)
name:kGMUserFileSystemDidMount object:nil];
[center addObserver:self selector:@selector(didUnmount:)
name:kGMUserFileSystemDidUnmount object:nil];Now if you run the app and eject the file system then it'll quit the app. That's nice, but if you quit the app you'll still have the problem where the file system stays mounted.
Terminate Properly
When we quit the app, we probably want to unmount the file system and clean things up. Be sure to stop unregister for notifications before you unmount the file system yourself. It is ok to umount a GMUserFileSystem that is not currently mounted. Where it says INSERT MORE CODE HERE, add the following:
[[NSNotificationCenter defaultCenter] removeObserver:self]; [fs_ unmount]; id delegate = [fs_ delegate]; [fs_ release]; [delegate release];
Mount Options
You always need to carefully consider what options to use when mounting your file system. For ytfs, we'll mount read-only (ro) and use a custom name and icon so that things look nice. Right above where you instantiate the GMUserFileSystem object, add the following code:
NSMutableArray* options = [NSMutableArray array]; [options addObject:@"ro"]; [options addObject:@"volname=YTFS"]; [options addObject: [NSString stringWithFormat:@"volicon=%@", [[NSBundle mainBundle] pathForResource:@"ytfs" ofType:@"icns"]]];
Don't forget to update the options: argument of the [fs_ mountAtPath... to be our new options array!
If you restart now, you should see that the mounted volume is called YTFS and has a nicer icon.
Create the Delegate
As our final step in YTController.m, we need to instantiate our file system delegate. The delegate will serve up the actual file system contents. Right above where you instantiate the GMUserFileSystem, add the following:
YTFS* ytfs = [[YTFS alloc] initWithVideos:[self fetchVideos]];
Don't forget to update the fs_ = [[GMUserFileSystem alloc]... to change from a nil initWithDelegate to initWithDelegate:ytfs.
Part 2: YTFS.m
The previous steps apply to pretty much all file systems that use MacFUSE.framework. Now we'll be fleshing out the ytfs file system itself in YTFS.m where it says INSERT CODE HERE.
Directory Contents
Add the following to show our root directory contents:
- (NSArray *)contentsOfDirectoryAtPath:(NSString *)path
error:(NSError **)error {
return [videos_ allKeys];
}If you run it now you should see a bunch of .webloc files. If you click on them then you'll be disappointed.
Webloc URLs
What good are weblocs if you can't click on them? Add the following:
- (NSURL *)URLOfWeblocAtPath:(NSString *)path {
return [self URLFromQuery:kPlayerURLQuery atPath:path];
}This looks up the video in our list of videos and, if found, returns the url of the video. MacFUSE.framework will automatically create a resource fork for us so that clicking on the video via the Finder will open up the URL in the default browser. If you run the app now you should be able to double-click on a video and watch it!
Custom Icons
Now that we can view our videos, we'll need some help in figuring out which ones look interesting. Using the following code, we can display a custom icon thumbnail for each of our videos:
- (NSData *)iconDataAtPath:(NSString *)path {
NSURL* url = [self URLFromQuery:kThumbURLQuery atPath:path];
if (url) {
NSImage* image = [[[NSImage alloc] initWithContentsOfURL:url] autorelease];
return [image icnsDataWithWidth:256];
}
return nil;
}If we implement this method then MacFUSE.framework will populate FinderInfo and ResourceFork information in order to show a custom icon. We can return nil in the case that we don't want to show a custom icon for the given path.
You'll notice that we download the thumbnail every time this routine is called. In practice this is not a very good idea and caching the .icns data would be preferred. For this tutorial the system url cache should be sufficient in preventing us from actually downloading from the server repeatedly.
We take advantage of a category on NSImage (see NSImage+icnsData.{h,m} in the project) to create .icns data for us from an NSImage on the fly. Note: If you want to use this NSImage category in your own stuff then you should probably use the latest version in filesystems-objc/Support. There are other useful classes there as well.
Not Quite Done
Since the Finder will ask about a lot of files that probably don't exist, every file system should implement attributesOfItemAtPath::
- (NSDictionary *)attributesOfItemAtPath:(NSString *)path
error:(NSError **)error {
if ([self nodeAtPath:path]) {
return [NSDictionary dictionary];
}
return nil;
}The framework will translate a nil return value into ENOENT. Another option would be to explicitly set the error out parameter to an NSError in the NSPOSIXErrorDomain.
File Contents For Fun
Just to show you how to do it, let's return the xml data for the video as the file contents. A .webloc file can actually be 0 bytes, since the url is stored in the resource fork, and up until now that is what we've done. If we implement contentsAtPath: then you can cat the file and see the xml data we've been using:
- (NSData *)contentsAtPath:(NSString *)path {
NSXMLNode* node = [self nodeAtPath:path];
if (node) {
NSString* xml = [node XMLStringWithOptions:NSXMLNodePrettyPrint];
return [xml dataUsingEncoding:NSUTF8StringEncoding];
}
return nil;
}Optimization
As it stands, our iconDataAtPath: is actually being called at least twice for every file since the framework needs to generate FinderInfo and ResourceFork for that file. This is expensive since we are resizing the image. If we implement finderFlagsAtPath: then we can reduce the number of times iconDataAtPath: is called:
- (UInt16)finderFlagsAtPath:(NSString *)path {
return ([self nodeAtPath:path] ? kHasCustomIcon : 0);
}Conclusion
If you've gone through this HOWTO then you've created a working and marginally useful user-space file system! Don't forget to try dragging a video you like onto the desktop to see what happens.
There is a lot more to using MacFUSE.framework, so please read the documentation in GMUserFileSystem.h and feel free to ask questions on macfuse-devel@googlegroups.com.
