文件夹的 macOS 安全范围 URL 书签
macOS Security scoped URL bookmark for folder
我遇到了问题(在 Mojave 和 Catalina 上)"reusing" 安全范围 URL 我的应用程序中的应用程序启动之间的文件夹书签。
使用libarchive
框架的简单解压应用。用户选择要解压缩的文件,我想为其父文件夹(例如 ~/Desktop)存储 URL 书签,并在下次用户尝试在同一文件夹中解压缩文件时重新使用它。
首先,我将以下内容添加到我的应用程序的权利文件中:
<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/>
第一次访问文件(父文件夹)时:
- 用户选择要解压的文件
- 我出示
NSOpenPanel
以获得对文件夹的访问权限:
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
// ...
}
- 我获取文件夹URL的书签数据并将其存储到密钥存档:
let data = try url.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
bookmarks[url] = data
NSKeyedArchiver.archiveRootObject(bookmarks, toFile: bookmarksPath)
- 现在我开始使用 file URL 并使用
libarchive
将 .zip 文件解压到它的父文件夹:
fileURL.startAccessingSecurityScopedResource()
// Decompressing file with libarchive...
fileURL.stopAccessingSecurityScopedResource()
- 一切正常,.zip 文件已解压
重新启动应用程序时,在同一文件夹中解压文件,重新使用保存的书签数据:
- 我从密钥存档中获取书签:
let bookmarks = NSKeyedUnarchiver.unarchiveObject(withFile: bookmarksPath) as? [URL: Data]
- 我从文件父文件夹的书签中获取书签数据并解析它:
let directoryURL = fileURL.deletingLastPathComponent()
let data = bookmarks[directoryURL]!
var isStale = false
let newURL = try URL(resolvingBookmarkData: data, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
- 现在我再次开始使用 file URL 并使用
libarchive
将 .zip 文件解压到它的父文件夹:
fileURL.startAccessingSecurityScopedResource()
// Decompressing file with libarchive...
fileURL.stopAccessingSecurityScopedResource()
但是这次libarchive
returns错误说Failed to open \'/Users/martin/Desktop/Archive.zip\'
我知道我可能做错了什么或者不理解安全范围 URL 书签的概念,但找不到问题所在。有什么提示吗?
最终解决方案
Rckstr 的回答和 this Apple developer forum thread 中的回答都为我指明了正确的方向。绝对有必要在 try URL(resolvingBookmarkData: data, options: .withSecurityScope ...
返回的 URL 的同一实例上调用 startAccessingSecurityScopedResource()
您正在将安全范围的书签(对于目录)解析为 let newUrl
,但是您在文件的 URL fileURL
上调用了 startAccessingSecurityScopedResource()
。您需要为 newURL
.
调用它
newURL.startAccessingSecurityScopedResource()
// Decompressing fileURL with libarchive...
newURL.stopAccessingSecurityScopedResource()
再说两句:
- 通过NSOpenPanel获取权限时,不需要调用
startAccessingSecurityScopedResource()
和
stopAccessingSecurityScopedResource()
,因为用户明确
授予您访问此会话的权限。
- 我改用
var isStale: ObjCBool = ObjCBool(false)
。我不是 Swift 专家,所以不确定 var isStale = false
是否可以使用。
因为我不能发表评论,所以我创建了一个新的答案。
只是一个问题:NSArchiver 不会做任何魔术,也不是绝对必要的。您可以存储 URL 您想要的任何方式,例如在用户默认值中:
我喜欢这样:
private func handleURLReceivedFromOpenPanel(_ url: URL) throws -> Void {
let data = try url.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
UserDefaults.standard.set(data, forKey: UserDefaultsKeys.writableUrl)
guard url.startAccessingSecurityScopedResource() else {
fatalError("Failed starting to access security scoped resource for: \(url.path)")
}
}
func getStoredUrl() throws -> URL {
guard let data = UserDefaults.standard.data(forKey: UserDefaultsKeys.writableUrl) else {
// no url stored so return a url that can be accessed
return try FileManager.default
.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("someSubfolderOrWhatever")
}
var isStale = false
let newUrl = try URL(resolvingBookmarkData: data,
options: .withSecurityScope,
relativeTo: nil,
bookmarkDataIsStale: &isStale)
guard newUrl.startAccessingSecurityScopedResource() else {
throw Error("Could not start accessing security scoped resource: \(newUrl.path)")
}
return newUrl
}
如果您将 URL 存储在内存中,请记住使用
释放资源
oldUrl.stopAccessingSecurityScopedResource()
我遇到了问题(在 Mojave 和 Catalina 上)"reusing" 安全范围 URL 我的应用程序中的应用程序启动之间的文件夹书签。
使用libarchive
框架的简单解压应用。用户选择要解压缩的文件,我想为其父文件夹(例如 ~/Desktop)存储 URL 书签,并在下次用户尝试在同一文件夹中解压缩文件时重新使用它。
首先,我将以下内容添加到我的应用程序的权利文件中:
<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/>
第一次访问文件(父文件夹)时:
- 用户选择要解压的文件
- 我出示
NSOpenPanel
以获得对文件夹的访问权限:
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
// ...
}
- 我获取文件夹URL的书签数据并将其存储到密钥存档:
let data = try url.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
bookmarks[url] = data
NSKeyedArchiver.archiveRootObject(bookmarks, toFile: bookmarksPath)
- 现在我开始使用 file URL 并使用
libarchive
将 .zip 文件解压到它的父文件夹:
fileURL.startAccessingSecurityScopedResource()
// Decompressing file with libarchive...
fileURL.stopAccessingSecurityScopedResource()
- 一切正常,.zip 文件已解压
重新启动应用程序时,在同一文件夹中解压文件,重新使用保存的书签数据:
- 我从密钥存档中获取书签:
let bookmarks = NSKeyedUnarchiver.unarchiveObject(withFile: bookmarksPath) as? [URL: Data]
- 我从文件父文件夹的书签中获取书签数据并解析它:
let directoryURL = fileURL.deletingLastPathComponent()
let data = bookmarks[directoryURL]!
var isStale = false
let newURL = try URL(resolvingBookmarkData: data, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
- 现在我再次开始使用 file URL 并使用
libarchive
将 .zip 文件解压到它的父文件夹:
fileURL.startAccessingSecurityScopedResource()
// Decompressing file with libarchive...
fileURL.stopAccessingSecurityScopedResource()
但是这次libarchive
returns错误说Failed to open \'/Users/martin/Desktop/Archive.zip\'
我知道我可能做错了什么或者不理解安全范围 URL 书签的概念,但找不到问题所在。有什么提示吗?
最终解决方案
Rckstr 的回答和 this Apple developer forum thread 中的回答都为我指明了正确的方向。绝对有必要在 try URL(resolvingBookmarkData: data, options: .withSecurityScope ...
startAccessingSecurityScopedResource()
您正在将安全范围的书签(对于目录)解析为 let newUrl
,但是您在文件的 URL fileURL
上调用了 startAccessingSecurityScopedResource()
。您需要为 newURL
.
newURL.startAccessingSecurityScopedResource()
// Decompressing fileURL with libarchive...
newURL.stopAccessingSecurityScopedResource()
再说两句:
- 通过NSOpenPanel获取权限时,不需要调用
startAccessingSecurityScopedResource()
和stopAccessingSecurityScopedResource()
,因为用户明确 授予您访问此会话的权限。 - 我改用
var isStale: ObjCBool = ObjCBool(false)
。我不是 Swift 专家,所以不确定var isStale = false
是否可以使用。
因为我不能发表评论,所以我创建了一个新的答案。 只是一个问题:NSArchiver 不会做任何魔术,也不是绝对必要的。您可以存储 URL 您想要的任何方式,例如在用户默认值中:
我喜欢这样:
private func handleURLReceivedFromOpenPanel(_ url: URL) throws -> Void {
let data = try url.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
UserDefaults.standard.set(data, forKey: UserDefaultsKeys.writableUrl)
guard url.startAccessingSecurityScopedResource() else {
fatalError("Failed starting to access security scoped resource for: \(url.path)")
}
}
func getStoredUrl() throws -> URL {
guard let data = UserDefaults.standard.data(forKey: UserDefaultsKeys.writableUrl) else {
// no url stored so return a url that can be accessed
return try FileManager.default
.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("someSubfolderOrWhatever")
}
var isStale = false
let newUrl = try URL(resolvingBookmarkData: data,
options: .withSecurityScope,
relativeTo: nil,
bookmarkDataIsStale: &isStale)
guard newUrl.startAccessingSecurityScopedResource() else {
throw Error("Could not start accessing security scoped resource: \(newUrl.path)")
}
return newUrl
}
如果您将 URL 存储在内存中,请记住使用
释放资源oldUrl.stopAccessingSecurityScopedResource()