cocoansurlsecurity-scoped-bookmarks

What is the correct way to handle stale NSURL bookmarks?


When resolving an NSURL from a security scoped bookmark, if the user has renamed or moved that file or folder, the bookmark will be stale. Apple's document says this regarding staleness:

isStale

On return, if YES, the bookmark data is stale. Your app should create a new bookmark using the returned URL and use it in place of any stored copies of the existing bookmark.

Unfortunately, this rarely works for me. It may work 5% of the time. Attempting to create a new bookmark using the returned URL results in an error, code 256, and looking in Console reveals a message from sandboxd saying deny file-read-data on the updated URL.

Note If regenerating the bookmark does work, it seems to only work the first time it is regenerated. It seems to never work should the folder/file be moved/renamed again.

How I initially create & store the bookmark

-(IBAction)bookmarkFolder:(id)sender {
  _openPanel = [NSOpenPanel openPanel];
  _openPanel.canChooseFiles = NO;
  _openPanel.canChooseDirectories = YES;
  _openPanel.canCreateDirectories = YES;
  [_openPanel beginSheetModalForWindow:self.window completionHandler:^(NSInteger result) {
    if (_openPanel.URL != nil) {
      NSError *error;
      NSData *bookmark = [_openPanel.URL bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope
                                  includingResourceValuesForKeys:nil
                                                   relativeToURL:nil
                                                           error:&error];
      if (error != nil) {
        NSLog(@"Error bookmarking selected URL: %@", error);
        return;
      }
      NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
      [userDefaults setObject:bookmark forKey:@"bookmark"];
    }
  }];
}

Code that resolves the bookmark

-(void)resolveStoredBookmark {
  NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
  NSData *bookmark = [userDefaults objectForKey:@"bookmark"];
  if (bookmark == nil) {
    NSLog(@"No bookmark stored");
    return;
  }
  BOOL isStale;
  NSError *error;
  NSURL *url = [NSURL URLByResolvingBookmarkData:bookmark
                                         options:NSURLBookmarkResolutionWithSecurityScope
                                   relativeToURL:nil
                             bookmarkDataIsStale:&isStale
                                           error:&error];
  if (error != nil) {
    NSLog(@"Error resolving URL from bookmark: %@", error);
    return;
  } else if (isStale) {
    if ([url startAccessingSecurityScopedResource]) {
      NSLog(@"Attempting to renew bookmark for %@", url);
      // NOTE: This is the bit that fails, a 256 error is 
      //       returned due to a deny file-read-data from sandboxd
      bookmark = [url bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope
               includingResourceValuesForKeys:nil
                                relativeToURL:nil
                                        error:&error];
      [url stopAccessingSecurityScopedResource];
      if (error != nil) {
        NSLog(@"Failed to renew bookmark: %@", error);
        return;
      }
      [userDefaults setObject:bookmark forKey:@"bookmark"];
      NSLog(@"Bookmark renewed, yay.");
    } else {
      NSLog(@"Could not start using the bookmarked url");
    }
  } else {
    NSLog(@"Bookmarked url resolved successfully!");
    [url startAccessingSecurityScopedResource];
    NSArray *contents = [NSFileManager.new contentsOfDirectoryAtPath:url.path error:&error];
    [url stopAccessingSecurityScopedResource];
    if (error != nil) {
      NSLog(@"Error reading contents of bookmarked folder: %@", error);
      return;
    }
    NSLog(@"Contents of bookmarked folder: %@", contents);
  }
}

When the bookmark is stale, the resulting resolved URL does point to the correct location, I just can't actually access the file despite the fact that [url startAccessingSecurityScopedResource] returns YES.

Perhaps I'm misinterpreting the documentation regarding stale bookmarks, but I'm hoping I'm just doing something stupid. Popping an NSOpenPanel each time a bookmarked file/folder is renamed or moved, my only other option at this point, seems ridiculous.

I should add that I have com.apple.security.files.bookmarks.app-scope, com.apple.security.files.user-selected.read-write, and com.apple.security.app-sandbox all set to true in my entitlements file.


Solution

  • After a lot of disappointing testing I've come to the following conclusions. Though logical, they're disappointing since the resulting experience for users is far from ideal and a significant pain for developers depending on how far they're willing to go to help users re-establish references to bookmarked resources.

    When I say "renew" below, I mean "generate a new bookmark to replace a stale bookmark using the URL resolved from the stale bookmark."

    1. Renewal always works as long as the bookmarked resource is moved or renamed within a directory that your app already has permission to access. So, by default, it always works inside your application's container folder.

    2. Renewal fails if a bookmarked resource is moved into a folder your application does not have permission to access. e.g. User drags a folder from your container folder to some folder outside the container folder. You will be able to resolve the URL, but not access nor renew the bookmark.

    3. Renewal fails if a bookmarked resource lives in a folder your application doesn't have access to and is then renamed. This means a user can explicitly grant your application access to a resource, then inadvertently revoke that access just by renaming it.

    4. Resolution fails if a resource is moved to another volume. Not sure if this is a limitation of bookmarks in general or just when used in a sandboxed application.

    For issues 2 & 3 you're in a decent position as the developer since resolution of the bookmarked URL does work. You can at least lead the user by telling them exactly which resources they need to grant your app access to and where they are. The experience could be improved by having them select a folder that contains (directly or indirectly) all resources that you need to renew a bookmark for. This could even be the volume, which solves the problem completely if they're willing to give your application this much access.

    For issue 4, resolution doesn't work at all. The user will have to relocate the file without any hints since you can't resolve the new location. One thing I've done in my current app that has reduced the pain of this issue is to add an extended attribute to any resource I store a bookmark for. Doing this at least lets me have the user choose a folder to search for previously associated resources.

    Frustrating limitations, but bookmarks still win over storing static paths.