【问题标题】:macOS store sandbox app uses NSOpenPanel to select download file folder, but can not access the folder againmacOS 商店沙盒应用使用 NSOpenPanel 选择下载文件夹,但无法再次访问该文件夹
【发布时间】:2025-12-12 09:00:01
【问题描述】:

我的应用是从网站下载文件。

我为 macOS 商店启用了项目的沙盒。

应用程序将触发 NSOpenPanel 要求用户选择下载文件(所有文件列表存储在 sqlite 文件中)保存到的文件夹。 例如:

/home/mymac/myfolder

一切正常。如果我关闭应用程序并重新打开它,我希望它可以继续下载文件(在 sqlite 文件中)。

但它报告错误: 设置安全信息:不允许操作

系统好像不允许应用访问文件夹

/home/mymac/myfolder

再次。

如果我使用 NSOpenPanel 选择系统下载文件夹

/home/mymac/Downloads

关闭应用程序并重新打开应用程序,一切正常。 看来系统只允许应用访问文件夹

/home/mymac/Downloads

再次。

欢迎评论

【问题讨论】:

    标签: macos cocoa sandbox


    【解决方案1】:

    您需要获取 URL 的书签并将其永久存储。当您的应用程序打开时,从存储的书签中检索 URL。

    文档中描述了这样做的方法:Locating Files Using Bookmarks

    你只需要两种方法:

    - (NSData*)bookmarkForURL:(NSURL*)url
    - (NSURL*)urlForBookmark:(NSData*)bookmark
    

    如果您不希望有很多书签,您可以将书签存储在 .plist 文件中,甚至可以存储在 UserDefaults 中。

    【讨论】:

    • 谢谢,我的问题不是url,是下载文件夹无法重新打开或访问
    • 是的,我明白了。无论您从 NSOpenPanel 获得什么,都需要将其存储为书签,以便在下次启动时访问。它可能是下载文件夹,无论如何。
    • 另一种方法是将您需要的文件存储在应用程序的目录中。您始终可以访问它。例如:let downloads = FileManager.default.urls(for: .downloadsDirectory, in: .localDomainMask).first
    • 应用程序的目录不能存储用户文件,苹果拒绝了我的应用程序这样做
    • 我使用 NSString 来存储文件夹,我还尝试使用 NSURL NSData 来存储信息,没有任何变化,重新启动应用程序并访问同一文件夹时出现同样的错误 'cannot open file at line 42249 os_unix.c: 42249:(0)-未定义的错误:0'
    【解决方案2】:

    您可以为此使用安全书签。我附上了我正在使用的课程:

    import Foundation
    import Cocoa
    
    public class SecureFolders
    {
        public static var window: NSWindow?
    
        private static var folders = [URL : Data]()
        private static var path: String?
    
        public static func initialize(_ path: String)
        {
            self.path = path
        }
    
        public static func load()
        {
            guard let path = self.path else { return }
    
            if !FileManager.default.fileExists(atPath: path)
            {
                return
            }
    
            if let rawData = NSData(contentsOfFile: path)
            {
                let data = Data(referencing: rawData)
    
                if let folders = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? [URL : Data]
                {
                    for folder in folders
                    {
                        self.restore(folder)
                    }
                }
            }
        }
    
        public static func remove(_ url: URL)
        {
            folders.removeValue(forKey: url)
        }
    
        public static func store(url: URL)
        {
            guard let path = self.path else { return }
    
            do
            {
                let data = try NSKeyedArchiver.archivedData(withRootObject: self.folders, requiringSecureCoding: false)
                self.folders[url] = data
    
                if let url = URL(string: path)
                {
                    try? data.write(to: url)
                }
            }
            catch
            {
                Swift.print("Error storing bookmarks")
            }
        }
    
        public static func restore(_ folder: (key: URL, value: Data))
        {
            let restoredUrl: URL?
            var isStale = false
    
            do
            {
                restoredUrl = try URL.init(resolvingBookmarkData: folder.value, options: NSURL.BookmarkResolutionOptions.withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
            }
            catch
            {
                Swift.print("Error restoring bookmarks")
                restoredUrl = nil
            }
    
            if let url = restoredUrl
            {
                if isStale
                {
                    Swift.print ("URL is stale")
                }
                else
                {
                    if !url.startAccessingSecurityScopedResource()
                    {
                        Swift.print ("Couldn't access: \(url.path)")
                    }
    
                    self.folders[url] = folder.value
                }
            }
        }
    
        public static func allow(folder: String, prompt: String, callback: @escaping (URL?) -> ())
        {
            let openPanel = NSOpenPanel()
            openPanel.directoryURL = URL(string: folder)
            openPanel.allowsMultipleSelection = false
            openPanel.canChooseDirectories = true
            openPanel.canCreateDirectories = false
            openPanel.canChooseFiles = false
            openPanel.prompt = prompt
    
            openPanel.beginSheetModal(for: self.window!)
            {
                result in
    
                if result == NSApplication.ModalResponse.OK
                {
                    let url = openPanel.url
                    self.store(url: url!)
    
                    callback(url)
                }
                else
                {
                    callback(nil)
                }
            }
        }
    
        public static func isStored(_ directory: Directory) -> Bool
        {
            return isStored(path: IO.getDirectory(directory))
        }
    
        public static func remove(_ directory: Directory)
        {
            let path = IO.getDirectory(directory)
            self.remove(path)
        }
    
        public static func remove(_ path: String)
        {
            let url = URL(fileURLWithPath: path)
            self.remove(url)
        }
    
        public static func isStored(path: String) -> Bool
        {
            let absolutePath = URL(fileURLWithPath: path).path
    
            for url in self.folders
            {
                if url.key.path == absolutePath
                {
                    return true
                }
            }
    
            return false
        }
    
        public static func areStored(_ directories: [Directory]) -> Bool
        {
            for dir in directories
            {
                if isStored(dir) == false
                {
                    return false
                }
            }
    
            return true
        }
    
        public static func areStored(_ paths: [String]) -> Bool
        {
            for path in paths
            {
                if isStored(path: path) == false
                {
                    return false
                }
            }
    
            return true
        }
    }
    

    用法:

    fileprivate func initialize() // Put a call to this in func applicationDidFinishLaunching(_ aNotification: Notification)
    {
        let directories = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
        let path = directories[0].appending("/SecureBookmarks.dict")
    
        SecureFolders.initialize(path)
        SecureFolders.load()
    }
    

    要将文件夹添加到安全书签:

    fileprivate func allow(_ path: String)
    {
        SecureFolders.allow(folder: path, prompt: "Open")
        {
            result in
            // Update controls or whatever
        }
    }
    

    不要忘记设置要显示NSOpenPanel 所需的窗口实例。您可以在您的 NSViewController 之一的 viewDidAppear 中设置实例:

    override func viewDidAppear()
    {
        super.viewDidAppear()
        SecureFolders.window = NSApplication.shared.mainWindow
    }
    

    【讨论】:

    • 谢谢,我的问题不是url,是下载文件夹无法重新打开或访问