文件夹的 macOS 安全范围 URL 书签

Mar*_*lch 5 macos appstore-sandbox security-scoped-bookmarks swift

我在应用程序启动之间为文件夹“重用”安全范围 URL 书签时遇到问题(在 Mojave 和 Catalina 上)。

这是使用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/>
Run Code Online (Sandbox Code Playgroud)

第一次访问文件(分别为父文件夹)时:

  1. 用户选择要解压的文件
  2. 我提出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
    // ...
}
Run Code Online (Sandbox Code Playgroud)
  1. 我获取文件夹URL 的书签数据并将其存储到密钥存档:
let data = try url.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
bookmarks[url] = data
NSKeyedArchiver.archiveRootObject(bookmarks, toFile: bookmarksPath)
Run Code Online (Sandbox Code Playgroud)
  1. 现在我开始使用文件URL 并用于libarchive将 .zip 文件解压缩到它的父文件夹:
fileURL.startAccessingSecurityScopedResource()
// Decompressing file with libarchive...
fileURL.stopAccessingSecurityScopedResource()
Run Code Online (Sandbox Code Playgroud)
  1. 一切都按预期工作,.zip 文件被解压缩

重新启动应用程序时,解压缩同一文件夹中的文件,重复使用保存的书签数据:

  1. 我从键控档案中获取书签:
let bookmarks = NSKeyedUnarchiver.unarchiveObject(withFile: bookmarksPath) as? [URL: Data]
Run Code Online (Sandbox Code Playgroud)
  1. 我从文件的父文件夹的书签中获取书签数据并解决它:
let directoryURL = fileURL.deletingLastPathComponent()
let data = bookmarks[directoryURL]!
var isStale = false
let newURL = try URL(resolvingBookmarkData: data, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
Run Code Online (Sandbox Code Playgroud)
  1. 现在我再次开始使用文件URL 并用于libarchive将 .zip 文件解压缩到它的父文件夹:
fileURL.startAccessingSecurityScopedResource()
// Decompressing file with libarchive...
fileURL.stopAccessingSecurityScopedResource()
Run Code Online (Sandbox Code Playgroud)

但是这次libarchive返回错误说Failed to open \'/Users/martin/Desktop/Archive.zip\'

我知道我可能在做一些非常错误的事情或不理解安全范围 URL 书签的概念,但找不到问题出在哪里。任何提示?

最终解决方案 Rckstr 的回答和Apple 开发者论坛帖子中的回答都为我指明了正确的方向。绝对有必要调用startAccessingSecurityScopedResource()由返回的 URL 的相同实例try URL(resolvingBookmarkData: data, options: .withSecurityScope ...

小智 7

您将安全范围的书签(对于目录)解析为let newUrl,但您调用startAccessingSecurityScopedResource()文件的 URL fileURL。您需要调用它newURL

newURL.startAccessingSecurityScopedResource()
// Decompressing fileURL with libarchive...
newURL.stopAccessingSecurityScopedResource()
Run Code Online (Sandbox Code Playgroud)

还有两点备注:

  1. 当通过 NSOpenPanel 获取访问权限时,您不需要调用 startAccessingSecurityScopedResource()stopAccessingSecurityScopedResource(),因为用户明确授予您对此会话的访问权限。
  2. 我用var isStale: ObjCBool = ObjCBool(false)的是。我不是 Swift 专家,所以不确定是否var isStale = false可以使用。


pot*_*tmo 5

由于我无法发表评论,因此我创建了一个新答案。只是一个问题: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
}
Run Code Online (Sandbox Code Playgroud)

如果您将 URL 存储在内存中,请记住使用以下命令释放资源

oldUrl.stopAccessingSecurityScopedResource()
Run Code Online (Sandbox Code Playgroud)