|
DEVELOPER_OBJC_HOWTO
Writing a File System in Objective-C Using MacFUSE.framework
NOTEThis page is currently being updated to reflect the MacFUSE 2.0 release. The update is almost done, so feel free to try it out. This message will go away when the update is complete. IntroductionMacFUSE comes with a framework to assist in developing user-space file systems written in 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 writing file system using Objective-C. The video for that presentation can be found here, but keep in mind that this HOWTO has been revised since the presentation. RequirementsIn order to go through this HOWTO, you'll need to:
cd /tmp svn checkout http://macfuse.googlecode.com/svn/trunk/filesystems-objc/Support/ Support The Tutorial ProjectWe'll create a simple read-only 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. A completed version of this tutorial can be found here, but it is highly recommended that you follow along with this tutorial to get an understanding of how things work. It should be fun and painless! Project SetupStart Xcode and choose File -> New Project. From here select MacFUSE under User Templates on the left and then choose the Objective-C File System (Read-Only) project template to create your project. We'll refer to this project as YTFS from now on. At this point you should have a working read-only file system! If you Build and Go from Xcode then your YTFS file system should mount and a Finder window will appear showing an empty root directory. If you quit the YTFS application then the file system will unmount; conversely if you unmount the file system the application will terminate. If you inspect the files in your project then you'll see the YTFS_FileSystem and YTFS_Controller classes. The YTFS_Controller class will control the life cycle of both our app and the file system, while the YTFS_FileSystem class will serve the contents of our file system. We still need a few more classes in order to complete YTFS. From the Support directory that you checked out earlier, add the NSImage+IconData.{h,m} and YTVideo.{h,m} files to the project. The former will be used for our thumbnails; it provides handy code to turn an NSImage into NSData for the .icns file that represents the image. The latter is a small class that fetches an xml feed of top videos from YouTube and provides a class to access information about the videos. Finally, in order to use NSImage+IconData you'll need to add Accelerate.framework to the project. Time To CodeI recommend that as you follow along in this 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: YTFS_Controller.mFor the first part, we'll make changes to the YTFS_Controller.m file that handles the life cycle of our file system. Include YTVideo.h Header File#import YTVideo.h Initialize the File System With VideosChange the following: fs_delegate_ = [[YTFS_Filesystem alloc] init]; to: fs_delegate_ = [[YTFS_Filesystem alloc] initWithVideos:[YTVideo fetchTopRatedVideos]]; Note: The code won't compile cleanly any more until we do a bit of work in YTFS_Filesystem.m. GMUserFileSystem, Mounting, and NotificationsIf you look at the template-generated controller code in applicationDidFinishLaunching:, you will see that it starts off by registering for MacFUSE file system notifications. This is how it handles quitting the application on unmount and showing you a Finder window when the file system finishes mounting. It then allocates a GMUserFileSystem instance to handle the file system and gives it a YTFS_Filesystem instance as the delegate that serves file system contents. By the way, since our file system is thread-safe, you may optionally change isThreadSafe:NO to isThreadSafe:YES in the line that initializes fs_. Finally, it sets up the MacFUSE options and instructs the GMUserFileSystem instance to mount. You always need to carefully consider what options to use when mounting your file system. By default, with this project template the file system is mounted read-only (rdonly) and uses a custom name and icon so that things look nice. Part 2: YTFS_Filesystem.hAdd a member variable to hold our video dictionary: @interface YTFS_Filesystem : NSObject {
NSDictionary* videos_;
}We'll also need an iniitalizer: - (id)initWithVideos:(NSDictionary *)videos; Part 3: YTFS_Filesystem.mNow we'll be fleshing out the code that serves the file system contents. Open YTFS_Filesystem.m and take a look around. You should see that the complete set of delegate methods that you can implement to serve read-only file system data are already stubbed out for you. Add Headers For Utility Classes#import "YTVideo.h" #import "NSImage+IconData.h" Remove Unused Delegate SelectorsWe actually don't need to use all of the delegate methods for this tutorial, so feel free to remove these selectors if you'd like:
Add Init and Helper MethodsWe'll add our initialization and cleanup methods as well as a helper method that looks up a video based on a given path. The should go near the top of the YTFS_Filesystem implementation, just above contentsOfDirectoryAtPath:error:. - (id)initWithVideos:(NSDictionary *)videos {
if ((self = [super init])) {
videos_ = [videos retain];
}
return self;
}
- (void)dealloc {
[videos_ release];
[super dealloc];
}
- (YTVideo *)videoAtPath:(NSString *)path {
NSArray* components = [path pathComponents];
if ([components count] != 2) {
return nil;
}
YTVideo* video = [videos_ objectForKey:[components objectAtIndex:1]];
return video;
}Things should compile cleanly again at this point. Directory ContentsEdit contentsOfDirectoryAtPath:error 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. File AttributesSince the Finder will ask about a lot of files that probably don't exist, every file system should implement attributesOfItemAtPath:userData:error:. Edit it to be as follows: - (NSDictionary *)attributesOfItemAtPath:(NSString *)path
error:(NSError **)error {
if ([self videoAtPath: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. You can only return POSIX errors from your delegate methods in the NSError out parameter. The good news is that the project template provides a category on NSError to make this easier. File ContentsJust 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. If you prefer you can return an empty NSData as the contents or remove the contentsAtPath: delegate method altogether. However, let's implement it so that you can drag the file to TextEdit.app and see the xml data that we've been using: - (NSData *)contentsAtPath:(NSString *)path {
YTVideo* video = [self videoAtPath:path];
if (video) {
return [video xmlData];
}
return nil;
}Webloc URLsWhat good are weblocs if you can't click on them? Change resourceAttributesAtPath:error: to be the following: - (NSDictionary *)resourceAttributesAtPath:(NSString *)path
error:(NSError **)error {
NSMutableDictionary* attribs = nil;
YTVideo* video = [self videoAtPath:path];
if (video) {
attribs = [NSMutableDictionary dictionary];
NSURL* url = [video playerURL];
if (url) {
[attribs setObject:url forKey:kGMUserFileSystemWeblocURLKey];
}
}
return attribs;
}This looks up the video in our list of videos and, if found, returns the url of the video as a resource fork attribute. 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 IconsNow that we can view our videos, we'll need some help in figuring out which ones look interesting. We can display a custom icon thumbnail for each of our videos. First change finderAttributesAtPath:error: to notify that we have a custom icon in our resource fork: - (NSDictionary *)finderAttributesAtPath:(NSString *)path
error:(NSError **)error {
NSDictionary* attribs = nil;
if ([self videoAtPath:path]) {
NSNumber* finderFlags = [NSNumber numberWithLong:kHasCustomIcon];
attribs = [NSDictionary dictionaryWithObject:finderFlags
forKey:kGMUserFileSystemFinderFlagsKey];
}
return attribs;
}If we implement this method then MacFUSE.framework will populate Finder Info data with the attributes we give it. We still need to add the custom icon to the file's resource fork, so update resourceAttributesAtPath:error: to look like: - (NSDictionary *)resourceAttributesAtPath:(NSString *)path
error:(NSError **)error {
NSMutableDictionary* attribs = nil;
YTVideo* video = [self videoAtPath:path];
if (video) {
attribs = [NSMutableDictionary dictionary];
NSURL* url = [video playerURL];
if (url) {
[attribs setObject:url forKey:kGMUserFileSystemWeblocURLKey];
}
url = [video thumbnailURL];
if (url) {
NSImage* image = [[[NSImage alloc] initWithContentsOfURL:url] autorelease];
NSData* icnsData = [image icnsDataWithWidth:256];
[attribs setObject:icnsData forKey:kGMUserFileSystemCustomIconDataKey];
}
}
return attribs;
}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+IconData.{h,m} in the project) to create .icns data for us from an NSImage on the fly. ConclusionIf 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 that you like onto the desktop to see what happens. There is a lot more to using MacFUSE.framework, so please read the documentation and feel free to ask questions on the MacFUSE mailing list. |