Enable access to Documents Directory in Xcode 13

I'm trying to export a text document in the Documents directory and running into file permission problem in a Mac app that does not use the App Sandbox. As a workaround for the issue, I tried turning on the App Sandbox and giving read/write access to the Documents directory, but when I add the App Sandbox capability, I have the following entries for file access:

  • User Selected File
  • Downloads Folder
  • Pictures Folder
  • Music Folder
  • Movies Folder

Since the file is a text file, exporting to any of the folders in the list makes no sense. If I give User Selected File read/write access, I still get file permission errors when I export. The UI in Xcode provides no way to add folders to the File Access list.

All I want to do is let someone export a file in the folder of their choice. How do I do this with the App Sandbox?

UPDATE

I'm developing a SwiftUI app. I was using SwiftUI's file exporter to export the document. I took robnotyou's suggestion to use NSSavePanel, but I still get the file permission error, with or without the App Sandbox. I get the following message in Xcode's console:

Error: Error Domain=NSCocoaErrorDomain Code=513 "You don’t have permission to save the file

If I turn on the App Sandbox, give the Downloads folder read/write permission, and export the document there, the file permissions issue goes away.

I'm running macOS 11.5.2. If I open the Privacy preferences in System Preferences and select Files and Folders, I can see that some previous apps I developed appear in the list and have a checkbox selected that grants the app access to the Documents folder. The list of apps has an Add button, but it's disabled.

I tried giving this app full disk access and adding it to the Developer Tools list of apps that can run software locally that does not meet the system's security policy. But the file permission error persists.

I'm exporting the file in a folder inside the Documents directory. The file has read/write access for my user account and my group. The Documents folder has custom access when I get info on the folder in the file.

How do I get around this file permission error?

Accepted Reply

Does this mean my creating the temporary file wrapper is forbidden in the App Sandbox?

No, just that you have to use the right API to find the correct place to put your temporary. Specifically, see the discussion of .itemReplacementDirectory in the url(for:in:appropriateFor:create:) docs.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Replies

If you use an NSSavePanel to allow the user to choose the save location, that will implicitly grant the write permission.

"When the user saves the document, macOS adds the saved file to the app's sandbox (if necessary) so that the app can write to the file."

  • Thanks. I was using a SwiftUI file exporter to export the document. Apparently the file exporter does not implicitly grant the write permission.

Add a Comment

I’m confused about how you’ve set this up. I tried the .fileExporter(…) modifier here in my office and it seems to work as expected. Specifically:

  1. Using Xcode 12.5 on macOS 11.5.2, I created a new app from the macOS > Document App template.

  2. I modified ContentView to look like this:

    struct ContentView: View {
        @Binding var document: FETestDocument
        @State var isExporting: Bool = false
    
        var body: some View {
            VStack {
                TextEditor(text: $document.text)
                Button("Export", action: { isExporting = true })
            }
                .fileExporter(isPresented: $isExporting, document: document, contentType: .exampleText) { result in
                    print(result)
                }
        }
    }
    
  3. I ran the app and clicked the Export button; it presented the export sheet.

  4. I chose a location on my desktop. It saved the file with the right content and printed:

    success(file:///Users/quinn/Exported%20Example%20Text2.exampletext)
    

What am I missing here?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

What I am trying to do is export a collection of Markdown files into an EPUB book. To create the EPUB book I start by creating a file wrapper that has everything the EPUB needs. I finish by using the ZIPFoundation framework to compress the file wrapper into a valid EPUB/Zip archive.

When I finish building the file wrapper, I call the FileWrapper class's write function to temporarily create the file so ZIPFoundation can compress and archive it.

do {
    //  mainDirectory is the file wrapper root. wrapperURL is the location to export
    // from either the file exporter panel or NSSavePanel.
    try mainDirectory.write(to: wrapperURL, options: [], originalContentsURL: nil)
} catch {
    Swift.print("Error temporarily writing the book's file wrapper to disk. Error: \(error)")
 }

When I step through this code in the debugger, it goes to the catch block and prints the following message in the console:

Error: Error Domain=NSCocoaErrorDomain Code=513 "You don’t have permission to save the file TestBook in the location (path to folder inside the Documents directory)."

This error occurs both with the SwiftUI file exporter and NSSavePanel. The only way I can get the error message to go away is to turn on the App Sandbox, give the Downloads folder read/write access, and export my books in the Downloads folder.

Thanks for the detailed explanation.

As a next step, let’s try isolating this problem from your zip handling. You wrote:

wrapperURL is the location to export from either the file exporter panel or NSSavePanel.

If you replace the code that saves the zip archive (that is, the line after the above-mentioned comment) with this:

try "Hello Cruel World!".write(to: wrappeURL, atomically: false, encoding: .utf8)

what happens?

If this works, we know this problem is specific to your zip archive handling. If it fails in the same way, we know it’s something about the way that you got wrapperURL.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

When I try the code you suggested.

try "Hello Cruel World!".write(to: wrapperURL, atomically: false, encoding: .utf8)

I get the permission error. However, I was inaccurate in my comment about wrapperURL. The wrapperURL variable removes the .epub extension from the location URL from the file exporter or save panel.

// location is the URL from the file exporter or NSSavePanel.
var wrapperURL = location
    wrapperURL.deleteLastPathComponent()
     wrapperURL.appendPathComponent(location.lastPathComponent
        .replacingOccurrences(of: ".epub", with: ""))

When I replace wrapperURL with location in your example, the file permissions error goes away.

try "Hello Cruel World!".write(to: location, atomically: false, encoding: .utf8)

If I write the wrapper using the location,

try mainDirectory.write(to: location, options: [], originalContentsURL: nil)

The file permissions error goes away, but the file wrapper is empty.

The wrapperURL variable removes the .epub extension from the location URL from the file exporter or save panel.

Yeah, that’s problematic. When the user chooses a file in a standard file panel, the system extends your sandbox to access just that file. If you move the extension that’s a different file and you don’t have access to it.

For more on dynamic sandbox extensions, see the discussion in my On File System Permissions post.

When I replace wrapperURL with location in your example, the file permissions error goes away.

OK, that confirms that your sandbox extension is working correctly.

If I write the wrapper using the location [the] file permissions error goes away, but the file wrapper is empty.

Hmmm, I’m not sure what’s going on there. Just as an experiment, what happens if you pass the .atomic option to write(to:options:originalContentsURL:)?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

If I pass the .atomic option, the file wrapper remains empty.

I took another look at my code and realized why I need the following code:

var wrapperURL = location
wrapperURL.deleteLastPathComponent()
wrapperURL.appendPathComponent(location.lastPathComponent
       .replacingOccurrences(of: ".epub", with: ""))

When someone chooses to publish the book, I get the location to publish the book from the file exporter or save panel. The URL will look similar to the following:

/path/to/MyBook.epub

When I write the file wrapper to disk, I can't write the wrapper at the location variable's URL because location is the destination for creating the book's Zip archive. I have to temporarily create the file wrapper with a different file name than MyBook.epub.

/path/to/MyBook

The MyBook file wrapper is the source for creating the Zip archive and MyBook.epub is the destination. I need both files to create the Zip archive with the ZIPFoundation framework.

You said in an earlier post:

When the user chooses a file in a standard file panel, the system extends your sandbox to access just that file. If you move the extension that’s a different file and you don’t have access to it.

Does this mean my creating the temporary file wrapper is forbidden in the App Sandbox? For now I can avoid using the App Sandbox, but these permission errors also occur with the App Sandbox turned off.

Does this mean my creating the temporary file wrapper is forbidden in the App Sandbox?

No, just that you have to use the right API to find the correct place to put your temporary. Specifically, see the discussion of .itemReplacementDirectory in the url(for:in:appropriateFor:create:) docs.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

The material in the url(for:in:appropriateFor:create:) docs provided the solution. The file permission problem was caused by the temporary file wrapper being in the same folder as the published book, The solution is to create a temporary directory for the file wrapper. Here's the code for anyone else who may have the same problem in the future.

var wrapperURL = URL(string: "/")
    do {
        wrapperURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, 
            appropriateFor: nil, create: true)
        wrapperURL?.appendPathComponent("TempBook")
     } catch {
        Swift.print("Error creating a temporary URL for the file wrapper. Error: \(error)")
    }