swiftmacosappstore-sandboxsecurity-scoped-bookmarks

macOS Security scoped URL bookmark for folder


I'm experiencing problems (on Mojave and Catalina) with "reusing" security scope URL bookmark for a folder between app launches in my app.

It's simple decompressing application using libarchive framework. User selects file to decompress, I want to store URL bookmark for it's parent folder (e.g. ~/Desktop), and reuse it next time user tries to decompress file in the same folder.

First, I added following to my app's entitlements file:

<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.bookmarks.app-scope</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>

When accessing file (parent folder respectively) for the first time:

  1. User selects file to decompress
  2. I present NSOpenPanel to obtain access to the file folder:
let directoryURL = fileURL.deletingLastPathComponent()

let openPanel = NSOpenPanel()
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = true
openPanel.canCreateDirectories = false
openPanel.canChooseFiles = false
openPanel.prompt = "Grant Access"
openPanel.directoryURL = directoryURL

openPanel.begin { [weak self] result in
    guard let self = self else { return }
    // WARNING: It's absolutely necessary to access NSOpenPanel.url property to get access
    guard result == .OK, let url = openPanel.url else {
        // HANDLE ERROR HERE ...
        return
    }

    // We got URL and need to store bookmark's data
    // ...
}
  1. I obtain bookmark data of folder URL and store it to keyed archive:
let data = try url.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
bookmarks[url] = data
NSKeyedArchiver.archiveRootObject(bookmarks, toFile: bookmarksPath)
  1. Now I start using file URL and use libarchive to decompress .zip file to it's parent folder:
fileURL.startAccessingSecurityScopedResource()
// Decompressing file with libarchive...
fileURL.stopAccessingSecurityScopedResource()
  1. Everything is working as expected, .zip file gets decompressed

When relaunching app, decompressing file in the same folder, reusing saved bookmark data:

  1. I get bookmarks from keyed archive:
let bookmarks = NSKeyedUnarchiver.unarchiveObject(withFile: bookmarksPath) as? [URL: Data]
  1. I get bookmark data from bookmarks for the file's parent folder and resolve it:
let directoryURL = fileURL.deletingLastPathComponent()
let data = bookmarks[directoryURL]!
var isStale = false
let newURL = try URL(resolvingBookmarkData: data, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
  1. Now again I start using file URL and use libarchive to decompress .zip file to it's parent folder:
fileURL.startAccessingSecurityScopedResource()
// Decompressing file with libarchive...
fileURL.stopAccessingSecurityScopedResource()

But this time libarchive returns error saying Failed to open \'/Users/martin/Desktop/Archive.zip\'

I know I might be doing something terribly wrong or not understanding concept of security scoped URL bookmarks but can't find where's the problem. Any hints?

FINAL SOLUTION Both Rckstr's answer and answer in this Apple developer forum thread pointed me into the right direction. It's absolutely necessary to call startAccessingSecurityScopedResource() on THE SAME INSTANCE of URL returned by try URL(resolvingBookmarkData: data, options: .withSecurityScope ...


Solution

  • You're resolving the security-scoped bookmark (for the directory) to let newUrl, but you call startAccessingSecurityScopedResource() on the file's URL fileURL. You need to call it for newURL.

    newURL.startAccessingSecurityScopedResource()
    // Decompressing fileURL with libarchive...
    newURL.stopAccessingSecurityScopedResource()
    

    Two more remarks:

    1. When obtaining access through NSOpenPanel, you don't need to call startAccessingSecurityScopedResource() and stopAccessingSecurityScopedResource(), because the user explicitly granted you access for this session.
    2. I use var isStale: ObjCBool = ObjCBool(false) instead. I'm no Swift expert, so not sure if var isStale = false is ok to use.