Search code examples
macoscocoasandboxnsopenpanel

NSOpenPanel under Sandbox - Access All Files inside User Selected folder


My sandboxed macOS app imports image files selected by the user via an NSOpenPanel modal window, as is customary.

At first, I configured the panel to canChooseDirectories = false, and set the allowedFileTypes property to NSImage.imageTypes. So far so good.

Using the app, I realized that the images I want to import are more often than not all grouped inside a folder with nothing more in it. It would be great if I could have the user just select the containing folder and import the images within "wholesale", so I adopted this code:

let panel = NSOpenPanel()

panel.allowsMultipleSelection = true
panel.canChooseDirectories = true
panel.canCreateDirectories = false
panel.canChooseFiles = true
panel.allowedFileTypes = NSImage.imageTypes

panel.begin { [unowned self] (result) in
    guard result == .OK else {
        return // User cancelled
     }

     // Read all selected images:

     let urls: [URL] = {
        if let directory = panel.directoryURL {
            // .........................................
            // [A] One directory selected:

            do {
                let urls = try FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil, options: [])
                return urls
            } catch {
                // (I ALWAYS END UP HERE)

                print(error.localizedDescription)
                return [] 
            }
        } else {
            // .........................................
            // [B] One or more files selected:

            return panel.urls
        }
     }()

 // (next: read individual urls...)

...but the try statement always fails, the catch block is executed and the error thrown is:

"The file “MyImageFolder” couldn’t be opened because you don’t have permission to view it."

Is there a way around this for sandboxed apps? Anything that I am forgetting, that will allow me to read the contents of a user-selected folder?


Addendum: Apple's documentation states that:

When a user of your app specifies they want to use a file or a folder, the system adds the associated path to your app’s sandbox. Say, for example, a user drags the ~/Documents folder onto your app’s Dock tile (or onto your app’s Finder icon, or into an open window of your app), thereby indicating they want to use that folder. In response, the system makes the ~/Documents folder, its contents, and its subfolders available to your app.

(emphasis mine)


Solution

  • I accepted @vadian's quick answer a bit prematurely, but it seems that I can access the individual files inside the user-selected folder from NSOpenPanel.

    After reading this answer (that I somehow missed at first in my searches), I found out that the code below works:

    // Warning! This code does not deal with the user selecting 
    // multiple folders!
    
    let urls: [URL] = {
    
        if inputURLs.count == 1, inputURLs[0].hasDirectoryPath {
            // Folder; Read its contents:
            do {
                let urls = try FileManager.default.contentsOfDirectory(at: inputURLs[0], includingPropertiesForKeys: nil, options: [])
                    return urls
    
            } catch {
                // (todo: Handle Errors)
                return []
            }
        } else {
            // One or more images; Read them directly:
            return inputURLs
        }
    }()
    

    An additional mistake I seemed to be making is to use NSURL's isFileURL property to distinguish between a folder selected and a single file: it returns true for folder too!

    So after I switched from using panel.directoryURL to using panel.urls[0] (when isFileURL is true), my app was trying to read a single image from the directory URL. No sandbox violation, but no image read either.

    According to the docs, that property returns true "if the receiver uses the file scheme" (whatever that means). I guess folders too "use the file scheme". I switched to using hasDirectoryPath instead, as suggested in this other answer.