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