PageRenderTime 143ms CodeModel.GetById 57ms app.highlight 81ms RepoModel.GetById 1ms app.codeStats 0ms

/filesystems-objc/SpotlightFS/Source/SpotlightFS.m

http://macfuse.googlecode.com/
Objective C | 536 lines | 292 code | 89 blank | 155 comment | 65 complexity | f643b20446b3367400038483ccdfef88 MD5 | raw file
  1// ================================================================
  2// Copyright (C) 2007 Google Inc.
  3// 
  4// Licensed under the Apache License, Version 2.0 (the "License");
  5// you may not use this file except in compliance with the License.
  6// You may obtain a copy of the License at
  7// 
  8//      http://www.apache.org/licenses/LICENSE-2.0
  9// 
 10// Unless required by applicable law or agreed to in writing, software
 11// distributed under the License is distributed on an "AS IS" BASIS,
 12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13// See the License for the specific language governing permissions and
 14// limitations under the License.
 15// ================================================================
 16//
 17//  SpotlightFS.m
 18//  SpotlightFS
 19//
 20//  Created by Greg Miller <jgm@> on 1/19/07.
 21//
 22
 23// The SpotlightFS file system looks roughly like:
 24// 
 25// /Volumes/SpotlightFS/
 26//                |
 27//                `-> SmarterFolder/
 28//                             |
 29//                             `-> ...
 30//                                  `-> :Users:blah:blah -> /Users/blah/blah
 31//                                  `-> :Users:blah:google -> /Users/blah/google
 32//                |
 33//                `-> SpotlightSavedSearch1/
 34//                             `-> :Users:foo:result -> /Users/foo/result
 35//                             `-> :Users:foo:blah -> /Users/foo/blah
 36//                |
 37//                `-> SpotlightSavedSearch2/
 38//                             `-> ...
 39//
 40//                |
 41//                `-> FooBar
 42//                      `-> :Users:foo:foobar -> /Users/foo/foobar
 43//                      `-> ...
 44//
 45
 46#import <sys/types.h>
 47#import <unistd.h>
 48#import <CoreServices/CoreServices.h>
 49#import <Foundation/Foundation.h>
 50#import <MacFUSE/GMUserFileSystem.h>
 51#import "SpotlightFS.h"
 52#import "NSError+POSIX.h"
 53
 54// Key name for use in NSUserDefaults
 55static NSString* const kDefaultsSearchDirectories = @"SearchDirectories";
 56
 57// The name of the top-level "smarter folder" that can be used to view the 
 58// contents of any random folder (and thus, Spotlight search)
 59static NSString* const kSmarterFolder = @"SmarterFolder";
 60
 61// Path and file extension used to lookup Spotlight's saved searches
 62static NSString* const kSpotlightSavedSearchesPath = @"~/Library/Saved Searches";
 63static NSString* const kSpotlightSavedSearchesExtension = @"savedSearch";
 64
 65
 66// EncodePath
 67//
 68// Given a path of the form /Users/foo/bar, returns the string in the form
 69// :Users:foo:bar.  Before this encoding takes place, all colons in the path
 70// are replaced with the '|' character.  This means that paths which actually
 71// have the '|' character in them won't decode correctly, but that's fine for
 72// this little example file system.
 73//
 74static NSString *EncodePath(NSString *path) {
 75  path = [[path componentsSeparatedByString:@":"]
 76                   componentsJoinedByString:@"|"];
 77  return [[path componentsSeparatedByString:@"/"]
 78                   componentsJoinedByString:@":"];
 79}
 80
 81// DecodePath
 82//
 83// Given a path of the form :Users:foo:bar, returns the path in the form
 84// /Users/foo/bar.
 85//
 86static NSString *DecodePath(NSString *path) {
 87  path = [[path componentsSeparatedByString:@":"]
 88                   componentsJoinedByString:@"/"];
 89  return [[path componentsSeparatedByString:@"|"]
 90                   componentsJoinedByString:@":"];
 91}
 92
 93
 94@implementation SpotlightFS
 95
 96// -spotlightSavedSearches
 97//
 98// Returns an NSArray of filenames matching
 99// ~/Library/Saved Searches/*.savedSearch
100//
101- (NSArray *)spotlightSavedSearches {
102  NSString *savedSearchesPath = [kSpotlightSavedSearchesPath stringByStandardizingPath];
103  NSMutableArray *savedSearches = [NSMutableArray array];
104  NSArray *files = [[NSFileManager defaultManager] directoryContentsAtPath:savedSearchesPath];
105  NSEnumerator *fileEnumerator = [files objectEnumerator];
106  NSString *filename = nil;
107  while ((filename = [fileEnumerator nextObject])) {
108    if ([[filename pathExtension] isEqualToString:kSpotlightSavedSearchesExtension])
109      [savedSearches addObject:[filename stringByDeletingPathExtension]];
110  }
111  return savedSearches;
112}
113
114// -contentsOfSpotlightSavedSearchNamed:
115// 
116// Returns the named Spotlight saved search plist parsed as an NSDictionary.
117//
118- (NSDictionary *)contentsOfSpotlightSavedSearchNamed:(NSString *)name {
119  if (!name)
120    return nil;
121  
122  NSString *savedSearchesPath = [kSpotlightSavedSearchesPath stringByStandardizingPath];
123  NSDictionary *contents = nil;
124  
125  // Append the .savedSearch extension if necessary
126  if (![[name pathExtension] isEqualToString:kSpotlightSavedSearchesExtension])
127    name = [name stringByAppendingPathExtension:kSpotlightSavedSearchesExtension];
128      
129  NSString *fullpath = [savedSearchesPath stringByAppendingPathComponent:name];
130  contents = [NSDictionary dictionaryWithContentsOfFile:fullpath];
131  
132  return contents;
133}
134
135// -userCreatedFolders
136//
137// Returns all the top-level folders that the user explicitly craeted.  These
138// folders are stored in the NSUserDefaults databaes for the running app.
139//
140- (NSArray *)userCreatedFolders {
141  NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
142  NSArray *userCreatedFolders = [defaults stringArrayForKey:kDefaultsSearchDirectories];
143  if (userCreatedFolders == nil)
144    userCreatedFolders = [NSArray array];
145  return userCreatedFolders;
146}
147
148// -isUserCreatedFolder:
149//
150// Returns YES if the specified folder is a user created folder. Also try
151// prepending a leading slash to the user created folders, incase |path| starts
152// with a slash.
153//
154- (BOOL)isUserCreatedFolder:(NSString *)path {
155  NSArray *folders = [self userCreatedFolders];
156  NSString *folder = nil;
157  NSEnumerator *folderEnumerator = [folders objectEnumerator];
158  while ((folder = [folderEnumerator nextObject])) {
159    if ([folder isEqualToString:path])
160      return YES;
161    if ([[@"/" stringByAppendingPathComponent:folder] isEqualToString:path])
162      return YES;
163  }
164  return NO;
165}
166
167// -setUserCreatedFolders:
168//
169// Sets the folder names to use for the top-level user-created folders.
170//
171- (void)setUserCreatedFolders:(NSArray *)folders {
172  [[NSUserDefaults standardUserDefaults] setObject:folders
173                                            forKey:kDefaultsSearchDirectories];
174}
175
176// -addUserCreatedFolder:
177//
178// Adds the specified user-created folder to the list of all user-created folders.
179//
180- (void)addUserCreatedFolder:(NSString *)folder {
181  if (!folder)
182    return;
183  
184  NSArray *currentFolders = [self userCreatedFolders];
185  
186  if ([currentFolders containsObject:folder])
187    return;
188  
189  NSMutableArray *folders = [[currentFolders mutableCopy] autorelease];
190  [folders addObject:folder];
191  [self setUserCreatedFolders:folders];
192}
193
194// -removeUserCreatedFolder:
195//
196// Removes the specified folder from the list of user-created folders.
197//
198- (void)removeUserCreatedFolder:(NSString *)folder {
199  if (!folder)
200    return;
201  NSArray *currentFolders = [self userCreatedFolders];
202  NSMutableArray *folders = [[currentFolders mutableCopy] autorelease];
203  [folders removeObject:folder];
204  [self setUserCreatedFolders:folders];
205}
206
207// -topLevelDirectories
208//
209// Returns an NSArray of all top-level folders.  This includes all Spotlight's
210// saved search folders, user-created smart folders, and our "SmarterFolder".
211//
212- (NSArray *)topLevelDirectories {
213  NSArray *spotlightSavedSearches = [self spotlightSavedSearches];
214  NSArray *userCreatedFolders = [self userCreatedFolders];
215  return [[spotlightSavedSearches arrayByAddingObjectsFromArray:userCreatedFolders]
216                                  arrayByAddingObject:kSmarterFolder];
217}
218
219// -encodedPathResultsForSpotlightQuery:
220//
221// This method is what actually runs the given spotlight query.  We first try to
222// create an MDQuery from the given query directly.  If this fails, we try to
223// create a query using the given query string as the text to match.  Once we
224// have a valid MDQuery, we execute it synchronously, then we create and return
225// an NSArray of all the matching file paths (encoded).
226//
227- (NSArray *)encodedPathResultsForSpotlightQuery:(NSString *)queryString
228                                           scope:(NSArray *)scopeDirectories {
229  // Try to create an MDQueryRef from the given queryString.
230  //  MDQueryRef query = MDQueryCreate(kCFAllocatorDefault,
231  //                                   (CFStringRef)queryString,
232  //                                   NULL, NULL);
233  
234  // 10/8/2007 Apple bug - radar 5529459
235  // This does not work on Leopard, because MDQueryCreate() does not return NULL
236  // when given an improperly formatted query string, as it's documented to do.
237  // One way to work around this is to see if the query string contains an "="
238  // and if so assume the query string is properly formatted, otherwise, format
239  // the query ourselves, using the logic in the if body below. This is a hack
240  // and hopefully will be fixed soon (10.5.2???) in which case we will remove 
241  // this.
242  
243  // The previous MDQueryCreate will fail if queryString isn't a validly
244  // formatted MDQuery.  In this case, we'll create a valid MDQuery and try
245  // again.
246  
247  if ([queryString rangeOfString:@"="].location == NSNotFound) {
248    queryString = [NSString stringWithFormat:
249                   @"* == \"%@\"wcd || kMDItemTextContent = \"%@\"c",
250                   queryString, queryString
251                   ];
252    //
253    //    query = MDQueryCreate(kCFAllocatorDefault,
254    //                          (CFStringRef)queryString,
255    //                          NULL, NULL);
256  }
257  
258  MDQueryRef query = MDQueryCreate(kCFAllocatorDefault,
259                                   (CFStringRef)queryString,
260                                   NULL, NULL);  
261  if (query == NULL)
262    return nil;
263  
264  if (scopeDirectories)
265    MDQuerySetSearchScope(query, (CFArrayRef)scopeDirectories, 0 /* options */);
266  
267  // Create and execute the query synchronously.
268  Boolean ok = MDQueryExecute(query, kMDQuerySynchronous);
269  if (!ok) {
270    NSLog(@"failed to execute query\n");
271    CFRelease(query);
272    return nil;
273  }
274  
275  int count = MDQueryGetResultCount(query);
276  NSMutableArray *symlinkNames = [NSMutableArray array];
277  
278  for (int i = 0; i < count; i++) {
279    MDItemRef item = (MDItemRef)MDQueryGetResultAtIndex(query, i);
280    NSString *name = (NSString *)MDItemCopyAttribute(item, kMDItemPath);
281    [name autorelease];
282    [symlinkNames addObject:EncodePath(name)];
283  }
284  
285  CFRelease(query);
286  
287  return symlinkNames;
288}
289
290
291#pragma mark == Overridden GMUserFileSystem Delegate Methods
292
293
294- (NSArray *)contentsOfDirectoryAtPath:(NSString *)path error:(NSError **)error {
295  if (!path) {
296    *error = [NSError errorWithPOSIXCode:EINVAL];
297    return nil;
298  }
299  
300  NSString *lastComponent = [path lastPathComponent];
301  
302  if ([lastComponent isEqualToString:@"/"]) {
303    return [self topLevelDirectories];
304  }
305  
306  // Special case the /SmarterSearches folder to have it appear empty
307  if ([lastComponent isEqualToString:kSmarterFolder])
308    return nil;
309  
310  NSString *query = lastComponent;
311  NSArray *scopeDirectories = nil;
312  
313  // If we're supposed to display the contents for a spotlight saved search
314  // directory, then we want to use the RawQuery from the saved search's plist.
315  // Otherwise, we just use the directory name itself as the query.
316  if ([[self spotlightSavedSearches] containsObject:lastComponent]) {
317    NSDictionary *ssPlist = [self contentsOfSpotlightSavedSearchNamed:lastComponent];
318    query = [ssPlist objectForKey:@"RawQuery"];
319    scopeDirectories = [[ssPlist objectForKey:@"SearchCriteria"]
320                                 objectForKey:@"FXScopeArrayOfPaths"];
321  }
322  
323  return [self encodedPathResultsForSpotlightQuery:query scope:scopeDirectories];
324}
325
326- (BOOL)createDirectoryAtPath:(NSString *)path
327                   attributes:(NSDictionary *)attributes
328                        error:(NSError **)error {
329  if (!path) {
330    *error = [NSError errorWithPOSIXCode:EINVAL];
331    return NO;
332  }
333  
334  // We only allow directories to be created at the very top level
335  NSString *dirname = [path stringByDeletingLastPathComponent];
336  if ([dirname isEqualToString:@"/"]) {
337    [self addUserCreatedFolder:[path lastPathComponent]];
338    return YES;
339  }
340  
341  return NO;
342}
343
344- (BOOL)fileExistsAtPath:(NSString *)path isDirectory:(BOOL *)isDirectory {
345  if (!path || !isDirectory)
346    return NO;
347
348  NSArray *tlds = [self topLevelDirectories];
349  int numComponents = [[path pathComponents] count];
350  
351  // Handle the top level root directory
352  if ([path isEqualToString:@"/"]) {
353    *isDirectory = YES;
354    return YES;
355  }
356
357  // Handle "._" and "Icon\r" that we don't deal with.
358  NSString* lastComponent = [path lastPathComponent];
359  if ([lastComponent hasPrefix:@"._"] ||
360      [lastComponent isEqualToString:@"Icon\r"]) {
361    return NO;
362  }
363
364  // Handle stuff in the /SmarterFolder
365  if ([path hasPrefix:[@"/" stringByAppendingString:kSmarterFolder]]) {
366    // We don't allow the creation of folders in the smarter folder.  But
367    // before the Finder actually attempts to create a folder, it checks for 
368    // existence.  So, we always report that a folder named "untitled folder"
369    // does *not* exist.  That way, Finder will then try to create that folder,
370    // we'll return an error, and the user will get a reasonable error message.
371    if ([lastComponent hasPrefix:@"untitled folder"])
372      return NO;
373    
374    // We report all other directories as existing
375    *isDirectory = YES;
376    return YES;
377  }
378  
379  // Handle other top-level directories, which may contain spotlight's saved
380  // searches, as well as other user-created folders.
381  if (numComponents == 2 && [tlds containsObject:lastComponent]) {
382    *isDirectory = YES;
383    return YES;
384  }
385  
386  // Handle symlinks in any of the top level directories, e.g. 
387  // /foo/symlink
388  if (numComponents == 3) {
389    // See the comments above for why we have to special case "untitled folder"
390    if ([[path lastPathComponent] hasPrefix:@"untitled folder"])
391      return NO;
392    
393    *isDirectory = NO;
394    return YES;
395  }
396  
397  // If the default is YES then finder will hang when trying to create a new 
398  // Folder (because it will keep probing to try to find an unused Folder name)
399  return NO;
400}
401
402// By default, directories are not writeable, with the notable exceptions below:
403// - Slash is writable
404// - User created directories in slash are writable
405- (NSDictionary *)attributesOfItemAtPath:(NSString *)path error:(NSError **)error {
406  if (!path) {
407    *error = [NSError errorWithPOSIXCode:EINVAL];
408    return nil;
409  }
410  BOOL isDirectory;
411  if (![self fileExistsAtPath:path isDirectory:&isDirectory]) {
412    *error = [NSError errorWithPOSIXCode:ENOENT];
413    return nil;
414  }
415  
416  NSMutableDictionary *attr = nil;
417  int mode = 0500;
418  
419  NSString *pathdir = [path stringByDeletingLastPathComponent];
420  NSString *smarter = [@"/" stringByAppendingString:kSmarterFolder];
421  
422  if ([pathdir isEqualToString:@"/"] || [pathdir isEqualToString:smarter]) {
423    
424    if ([path isEqualToString:@"/"] || [self isUserCreatedFolder:path]) {
425      mode = 0700;
426    }
427    
428    attr = [NSDictionary dictionaryWithObjectsAndKeys:
429      [NSNumber numberWithInt:mode], NSFilePosixPermissions,
430      [NSNumber numberWithInt:geteuid()], NSFileOwnerAccountID,
431      [NSNumber numberWithInt:getegid()], NSFileGroupOwnerAccountID,
432      [NSDate date], NSFileCreationDate,
433      [NSDate date], NSFileModificationDate,
434      (isDirectory ? NSFileTypeDirectory : NSFileTypeRegular), NSFileType,
435      nil];
436    
437  } else {
438    
439    NSString *decodedPath = DecodePath([path lastPathComponent]);
440    NSFileManager *fm = [NSFileManager defaultManager];
441    attr = [[[fm fileAttributesAtPath:decodedPath traverseLink:NO] mutableCopy] autorelease];
442    if (!attr)
443      attr = [NSMutableDictionary dictionary];
444    [attr setObject:NSFileTypeSymbolicLink forKey:NSFileType];
445    
446  } 
447  if (!attr) {
448    *error = [NSError errorWithPOSIXCode:ENOENT];
449  }
450  return attr;
451}
452
453- (NSString *)destinationOfSymbolicLinkAtPath:(NSString *)path error:(NSError **)error {
454  if (!path) {
455    *error = [NSError errorWithPOSIXCode:EINVAL];
456    return nil;
457  }
458  
459  NSString *lastComponent = [path lastPathComponent];
460  
461  if ([lastComponent hasPrefix:@":"])
462    return DecodePath(lastComponent);
463  
464  *error = [NSError errorWithPOSIXCode:ENOENT];
465  return nil;
466}
467
468- (BOOL)moveItemAtPath:(NSString *)source toPath:(NSString *)destination error:(NSError **)error {  
469  if (!source || !destination) {
470    *error = [NSError errorWithPOSIXCode:EINVAL];
471    return NO;
472  }
473  
474  NSArray *moveableDirs = [self userCreatedFolders];
475  NSString *sourceBasename = [source lastPathComponent];
476  
477  // You can only rename user created directories at the top level, i.e., a 
478  // directory that would have been created through a mkdir to this FS
479  if (![moveableDirs containsObject:sourceBasename])
480    return NO;
481  
482  NSString *destBasename = [destination lastPathComponent];
483  
484  // On this FS, you can't rename to a dir that already exists because we only
485  // allow one level of directories
486  if ([moveableDirs containsObject:destBasename])
487    return NO;
488  
489  // OK, do the move
490  [self removeUserCreatedFolder:sourceBasename];
491  [self addUserCreatedFolder:destBasename];
492  
493  return YES;
494}
495
496- (BOOL)removeItemAtPath:(NSString *)path error:(NSError **)error {
497  if (!path) {
498    *error = [NSError errorWithPOSIXCode:EINVAL];
499    return NO;
500  }
501  
502  NSArray *components = [path pathComponents];
503  int ncomp = [components count];
504  if (ncomp < 2)
505    return NO;
506  
507  NSArray *savedSearches = [self spotlightSavedSearches];
508  NSString *firstDir = [components objectAtIndex:1];
509  
510  if ([firstDir isEqualToString:kSmarterFolder])
511    return NO;
512  else if ([savedSearches containsObject:firstDir])
513    return NO;
514
515  if (ncomp == 2)
516    [self removeUserCreatedFolder:firstDir];
517  
518  return YES;
519}
520
521- (NSString *)iconDataAtPath:(NSString *)path {
522  NSString *lastComponent = [path lastPathComponent];
523  NSBundle *mainBundle = [NSBundle mainBundle];
524  NSString *iconPath = [mainBundle pathForResource:@"SmartFolderBlue" ofType:@"icns"];
525  
526  if ([path isEqualToString:@"/"])
527    return nil;  // Custom volume icon is handled by options to filesystem mount.
528  else if ([path isEqualToString:[@"/" stringByAppendingPathComponent:kSmarterFolder]])
529    iconPath = [mainBundle pathForResource:@"DynamicFolderBlue" ofType:@"icns"];
530  else if ([[self spotlightSavedSearches] containsObject:lastComponent])
531    iconPath = [mainBundle pathForResource:@"SmartFolder" ofType:@"icns"];
532  
533  return [NSData dataWithContentsOfFile:iconPath];
534}
535
536@end