Security-scoped URL bookmark for folder and it's subfolders and files

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
    // ...
}


3. 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)


4. 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()


5. 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]


2. 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)

3. 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?


I also tried to start accessing folderURL instead of fileURL but it keeps returning false and not working either:

let success = folderURL.startAccessingSecurityScopedResource()

Accepted Reply

Maybe I miss something.


But why do you use fileURL and not newURL here ?

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


In fact newURL is optional, so should call

if let newURL = newURL {


I noticed we need to be careful when you call stopAccessing (if done at the wrong place, that impedes further access).

So, could you try, just for testing purpose, to comment it out ?


May have a look here as well:

https://stackoverflow.com/questions/42179567/what-does-startaccessingsecurityscopedresource-actually-do

and this one:

h ttps://danieltull.co.uk/blog/2018/09/09/wrapping-urls-security-scoped-resource-methods/

Replies

Maybe I miss something.


But why do you use fileURL and not newURL here ?

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


In fact newURL is optional, so should call

if let newURL = newURL {


I noticed we need to be careful when you call stopAccessing (if done at the wrong place, that impedes further access).

So, could you try, just for testing purpose, to comment it out ?


May have a look here as well:

https://stackoverflow.com/questions/42179567/what-does-startaccessingsecurityscopedresource-actually-do

and this one:

h ttps://danieltull.co.uk/blog/2018/09/09/wrapping-urls-security-scoped-resource-methods/

Thanks Claude!

You pointed me into right direction. According to my finding it's absolutely necessary to use the newURLinstance I created sooner by resolving the bookmark data!

Cheers,

Martin