How to access files in macOS FileProvider Extension programmatically?

I have a FileProvider app consisting of MainApp and FileProviderExtension. MainApp has a popover which should have features like:

  1. Button which opens FileProvider folder
  2. List of files being uploaded/download and when user clicks on a file, it should either be opened or shown in Finder in containing folder.

How to achieve these in Sandboxed macOS app? I know it's possible because Microsoft's OneDrive app is distributed via AppStore and can perform these features.

So far I've been able to create an alias to a FileProvider folder in user's home folder by utilizing NSOpenPanel.

I tried opening FileProvider files from MainApp with something like:

let url = try! await NSFileProviderManager(for: myDomain)?.getUserVisibleURL(for: fileIdentifier)
NSWorkspace.shared.open(url!)

but getUserVisibleURL returns nil.

Does anyone know how to achieve described functionality?

Accepted Reply

I was chaising my own tail for a while. Anyways, here's a solution that worked for me:

Here is an example on how to open a FileProvider folder:

  1. Retrieve FileProvider root folder URL:
let fileProviderFolderURL = try! await NSFileProviderManager(for: SREG.context.domain)?.getUserVisibleURL(for: .rootContainer)
  1. Use NSOpenPanel to ask user where to store the symbolic link to FileProvider root folder. Then store bookmark data of user-selected location and create symbolic link to FileProvider root within that location.
let savePanel = NSOpenPanel()
savePanel.canChooseDirectories = true
savePanel.title = "Choose this location"
savePanel.prompt = "Choose"

let result = await savePanel.begin()

if result == NSApplication.ModalResponse.OK {
    let panelURL = savePanel.url
    let mountBookmarkData = try! panelURL.bookmarkData(options: .withSecurityScope)
    UserDefaults.standard.set(mountBookmarkData, forKey: "aliasLocationBookmarkData")

    let fileManager = FileManager.default
    var aliasURL = panelURL.appendingPathComponent("MyRoot")

    UserDefaults.standard.set(aliasURL.path, forKey: Constants.UserDefaults.aliasPath)

    do {
        try fileManager.createSymbolicLink(at: aliasURL, withDestinationURL: fileProviderFolderURL)
    } catch {
        // handle error
    }

}
  1. Later, when you want to open the Root folder in Finder, fetch bookmark data from UserDefaults and create URL from it. It is the location where symbolic link is stored. Start secure access on it and then open the symbolic link URL:
let aliasLocationBookmarkData = UserDefaults.standard.data(forKey: "aliasBookmarkData")!
var isStale = false
let aliasLocationURL = try URL(resolvingBookmarkData: aliasLocationBookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)

let securityScopeResult = aliasLocationURL.startAccessingSecurityScopedResource()
if securityScopeResult {
    let aliasPathKey = Constants.UserDefaults.aliasPath
    let aliasPath = UserDefaults.standard.string(forKey: aliasPathKey)
    let aliasURL = URL(fileURLWithPath: aliasPath)
    
    NSWorkspace.shared.open(aliasURL)    

    aliasLocationURL.stopAccessingSecurityScopedResource()
}

That's pretty much it. The key is to store bookmark data of the location user picked in NSOpenPanel dialog, create symbolic link in it and then you'll be able to access that resource even after app relaunch.

Replies

You should be able to access files in the user visible location with the getUserVisibleURL API.

Are you sure NSFileProviderManager(for: myDomain)? is non-nil?

Where do you get fileIdentifier argument? That should be an identifier previously passed to the system on a completion handler for createItem, modifyItem, or on a directory/working set enumerator. Don't pass the identifier from the itemTemplate argument of a createItem call - that is a temporary identifier that you are meant to replace with your own on the createItem completion handler.

The most trivial test would be, passing .rootContainer as the fileIdentifier. That should give you the root of the domain (~/Library/CloudStorage/your-domain-here).

  • Indeed, passing a .rootContainer as fileIdentifier does return a URL like:

    let url = try! await NSFileProviderManager(for: myDomain)?.getUserVisibleURL(for: .rootContainer)

    but then:

    NSWorkspace.shared.open(url!)

    shows an error dialog: The application “MyApp” does not have permission to open “MyApp-FileProviderExtension-1.”

Add a Comment

I have tried playing with bookmark files like:

let url = try! await NSFileProviderManager(for: SREG.context.domain)?.getUserVisibleURL(for: .rootContainer)
let bookmarkData = try! url!.bookmarkData(options: [.suitableForBookmarkFile],
                                          includingResourceValuesForKeys: nil, relativeTo: nil)

let savePanel = NSOpenPanel()
savePanel.canChooseDirectories = true

let result = await savePanel.begin()
if result == NSApplication.ModalResponse.OK {
    var mountUrl = panelURL.appendingPathComponent("MyApp")
    do {
        try URL.writeBookmarkData(bookmarkData, to: mountUrl)
        let bdata = try mountUrl?.bookmarkData(options: .withSecurityScope)
        UserDefaults.standard.set(bdata!.base64EncodedString(), forKey: "b64")
    } catch {
        // print error
    }
}

and then later on next app run:

bookmarkURL = URL(fileURLWithPath: "/Users/me/MyApp")
let b64 = UserDefaults.standard.string(forKey: "b64")!
let bdata = Data(base64Encoded: b64)
let mountURL = try URL(resolvingBookmarkData: bdata!, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
let res = mountURL?.startAccessingSecurityScopedResource()

if res {
    NSWorkspace.shared.open(mountURL!)
    mountURL?.stopAccessingSecurityScopedResource()
}

but I get the same error that

"The application “MyApp” does not have permission to open “MyApp-FileProviderExtension-1".”

It does however work on the same app run where bookmark is created and then accessed at later point, but I think it's because NSOpenPanel has been shown in the same app run session.

What I'm looking for is a way for a MainApp to open files from FileProviderExtension on user's demand (like user clicking on some button to open root folder or a specific file from list of Uploaded files...)

I'm kind of stuck here. I changed code so here is the new approach.

  1. I create bookmark in Application Sandboxed Document folder:
let url = try! await NSFileProviderManager(for: SREG.context.domain)?.getUserVisibleURL(for: .rootContainer)
let bookmarkData = try! url!.bookmarkData(options: [.suitableForBookmarkFile], includingResourceValuesForKeys: nil, relativeTo: nil)
let dirURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let bookmarkURL = dirURL.appendingPathComponent("RootFolderLink")
try URL.writeBookmarkData(bookmarkData, to: bookmarkURL)
  1. Then at later point when I try to programmatically open the bookmark:
let dirURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let bookmarkURL = dirURL.appendingPathComponent("RootFolderLink")
let bookmarkData = try bookmarkURL.bookmarkData(options: .withSecurityScope)
let mountURL = try URL(resolvingBookmarkData: bdata, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)

let res = mountURL?.startAccessingSecurityScopedResource() ?? false
if res {
    NSWorkspace.shared.open(mountURL!)
    mountURL?.stopAccessingSecurityScopedResource()
}

The Finder does indeed open the FileProvider root folder. However, after quitting the app and trying to open the folder again, I get error message:

Important note is that I still have the part with NSOpenPanel where user chooses location where to store bookmark (ie in his Home folder). And only in session where this dialog is shown, opening bookmark will work programmatically.

How was app able to open the bookmark link in the same session when it was created, but after app relaunch it refuses to open the link? What can I do to fix this issue? Could it have something to do with signing? This is Xcode debug run.

I was chaising my own tail for a while. Anyways, here's a solution that worked for me:

Here is an example on how to open a FileProvider folder:

  1. Retrieve FileProvider root folder URL:
let fileProviderFolderURL = try! await NSFileProviderManager(for: SREG.context.domain)?.getUserVisibleURL(for: .rootContainer)
  1. Use NSOpenPanel to ask user where to store the symbolic link to FileProvider root folder. Then store bookmark data of user-selected location and create symbolic link to FileProvider root within that location.
let savePanel = NSOpenPanel()
savePanel.canChooseDirectories = true
savePanel.title = "Choose this location"
savePanel.prompt = "Choose"

let result = await savePanel.begin()

if result == NSApplication.ModalResponse.OK {
    let panelURL = savePanel.url
    let mountBookmarkData = try! panelURL.bookmarkData(options: .withSecurityScope)
    UserDefaults.standard.set(mountBookmarkData, forKey: "aliasLocationBookmarkData")

    let fileManager = FileManager.default
    var aliasURL = panelURL.appendingPathComponent("MyRoot")

    UserDefaults.standard.set(aliasURL.path, forKey: Constants.UserDefaults.aliasPath)

    do {
        try fileManager.createSymbolicLink(at: aliasURL, withDestinationURL: fileProviderFolderURL)
    } catch {
        // handle error
    }

}
  1. Later, when you want to open the Root folder in Finder, fetch bookmark data from UserDefaults and create URL from it. It is the location where symbolic link is stored. Start secure access on it and then open the symbolic link URL:
let aliasLocationBookmarkData = UserDefaults.standard.data(forKey: "aliasBookmarkData")!
var isStale = false
let aliasLocationURL = try URL(resolvingBookmarkData: aliasLocationBookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)

let securityScopeResult = aliasLocationURL.startAccessingSecurityScopedResource()
if securityScopeResult {
    let aliasPathKey = Constants.UserDefaults.aliasPath
    let aliasPath = UserDefaults.standard.string(forKey: aliasPathKey)
    let aliasURL = URL(fileURLWithPath: aliasPath)
    
    NSWorkspace.shared.open(aliasURL)    

    aliasLocationURL.stopAccessingSecurityScopedResource()
}

That's pretty much it. The key is to store bookmark data of the location user picked in NSOpenPanel dialog, create symbolic link in it and then you'll be able to access that resource even after app relaunch.

Hm, but suggested solution only works if user selected his home folder as location for alias creation. If user in NSOpenPanel selects any subfolder in user's home, then

NSWorkspace.shared.open(aliasURL)

will fail with error message:

The application “MyApp” does not have permission to open “MyAppFileProvider.”

Is there any solution to that?

The solution is quite simple:

Task{
    guard let fileProviderFolderURL = try! await NSFileProviderManager(for: domain)?.getUserVisibleURL(for: .rootContainer) else{
        print("No CloudStorage URL found")
        return
    }

    fileProviderFolderURL.startAccessingSecurityScopedResource()
    let openResult = NSWorkspace.shared.open(fileProviderFolderURL)
    if !openResult {
        print("There was an error opening FileProvider Folder")
    }
    fileProviderFolderURL.stopAccessingSecurityScopedResource()
}