I've made a Finder extension
to add a menu to Finder's Context menu for any file. I'd like to access this file when the user selects this custom menu, obviously this file they select could be anywhere in the file system and outside the allowed sandbox areas.
func accessFile(url: URL, userID: String, completion: @escaping ([String:Any]?, Error?) -> Void){
var bookmarks = NSKeyedUnarchiver.unarchiveObject(withFile: bookmarksPath) as? [URL: Data]
print("Testing if we have access to file")
// 1. Test if I have access to a file
let directoryURL = url.deletingLastPathComponent()
let data = bookmarks?[directoryURL]
if data == nil{
print("have not asked for access yet or directory is not saved")
// 2. If I do not, open a open dialog, and get permission
let openPanel = NSOpenPanel()
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = true
openPanel.canCreateDirectories = false
openPanel.canChooseFiles = false
openPanel.prompt = "Grant Access"
openPanel.directoryURL = directoryURL
openPanel.begin { result in
guard result == .OK, let url = openPanel.url else {return}
// 3. obtain bookmark data of folder URL and store it to keyed archive
do{
let data = try url.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
}catch{
print(error)
}
bookmarks?[url] = data
NSKeyedArchiver.archiveRootObject(bookmarks, toFile: bookmarksPath)
// 4. start using the fileURL via:
url.startAccessingSecurityScopedResource()
// < do whatever to file >
url.stopAccessingSecurityScopedResource()
}
}else{
// We have accessed this directory before, get data from bookmarks
print("we have access already")
let directoryURL = url.deletingLastPathComponent()
guard let data = bookmarks?[directoryURL]! else { return }
var isStale = false
let newURL = try? URL(resolvingBookmarkData: data, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
// 3. Now again I start using file URL and upload:
newURL?.startAccessingSecurityScopedResource()
// < do whatever to file >
newURL?.stopAccessingSecurityScopedResource()
}
}
Currently it always asks for permission, so the bookmark is not getting saved
I'm not 100% sure if this is the source of your problem, but I don't see where you are using the
isStale
value. If it it comes backtrue
fromURL(resolvingBookmarkData:...)
, you have to remake/resave the bookmark. So in yourelse
block you need some code like this:data
will, of course, need to bevar
instead oflet
now.Also remember that
stopAccessingSecurityScopedResource()
has to be called on main thread, so if you're not sureaccessFile
is being called on the main thread, you might want to do that explicitly:You'd want to do that in both places you call it.
I like to write an extension on
URL
to make it a little nicer:So then I can write:
Whether you use the extension or not, explicitly calling
stopAccessingSecurityScopedResource()
onDispatchQueue.main
does mean that access won't be stopped until the next main run loop iteration. That's normally not a problem, but if you start and stop the access for the sameURL
multiple times in a single run loop iteration, it might not work, because it will callstartAccessingSecurityScopedResource()
multiple time withoutstopAccessingSecurityScopedResource()
in between, and the on the next iteration it would callstopAccessingSecurityScopedResource()
multiple times as the queued tasks are executed. I have no idea ifURL
maintains a security access count that would allow that to be safe, or just a flag, in which case it wouldn't be.