MacOS app with user-selected file read-write entitlement rejected from Mac App Store because SQLite creates a temp file when writing to database
Asked Answered
R

1

7

I have a MacOS app that I want to put on the Mac App Store so it must be sandboxed. With the app the user creates multiple SQLite database files storing information they create, somewhat analogous to Excel Spreadsheet files but I am leveraging SQLite in my app to create, read, and edit the files. They store these files in the Documents folder on their mac. I added an entitlement for this: com.apple.security.files.user-selected.read-write. The app can read these database files fine, but if I try to write to them it fails.

Per the Mac Console app's log the error is:

Sandbox: MyApp Helpe(16378) deny(1) file-write-create /Users/steve/Documents/myFile1.sqlite-journal

Violation: deny(1) file-write-create /Users/steve/Documents/myFile1.sqlite-journal

So it is failing because SQLite, behind the scenes, creates a temporary file with the same name as the one accessed but changes the extension by appending "-journal" to it. So if the user opens a file named myFile1.sqlite it opens and reads fine, but if they try to write to it, SQLite will create a temporary file named myFile1.sqlite-journal as part of the process, then deletes it. But because the user did not open or save a file named myFile1.sqlite-journal, it is not in the sandbox and is denied.

I confirmed this is the problem by creating an empty file in Finder named myFile1.sqlite-journal and opened it from my app (thus adding it as a user selected file to the sandbox), and was then able to write to myFile1.sqlite.

This is a known issue and there seems to be a solution per the docs by using "Related Items": https://developer.apple.com/library/archive/documentation/Security/Conceptual/AppSandboxDesignGuide/AppSandboxInDepth/AppSandboxInDepth.html.

Below is the relevant text, but this is my first mac app and these instructions are clear as mud. Been fooling around with them for days and still have no idea what I'm supposed to do. The below mentions extensions but sqlite does not have a set extension. You just use whatever extension you want (let's say .sqlite). Can someone explain what properties I need to add to the info.plist.

RELATED ITEMS:

The related items feature of App Sandbox lets your app access files that have the same name as a user-chosen file, but a different extension. This feature consists of two parts: a list of related extensions in the application’s Info.plist file and code to tell the sandbox what you’re doing.

There are two common scenarios where this makes sense:

Scenario 1: (Not Relevant to my issue)

Scenario 2: Your app needs to be able to open or save multiple related files with the same name and different extensions (for example, to automatically open a subtitle file with the same name as a movie file, or to allow for a SQLite journal file).

To gain access to that secondary file, create a class that conforms to the NSFilePresenter protocol. This object should provide the main file’s URL as its primaryPresentedItemURL property, and should provide the secondary file’s URL as its presentedItemURL property.

After the user opens the main file, your file presenter object should call the addFilePresenter: class method on the NSFileCoordinator class to register itself.

Note: In the case of a SQLite journal file, beginning in 10.8.2, journal files, write-ahead logging files, and shared memory files are automatically added to the related items list if you open a SQLite database, so this step is unnecessary.

In both scenarios, you must make a small change to the application’s Info.plist file. Your app should already declare a Document Types (CFBundleDocumentTypes) array that declares the file types your app can open.

For each file type dictionary in that array, if that file type should be treated as a potentially related type for open and save purposes, add the key NSIsRelatedItemType with a boolean value of YES.

Rus answered 10/1, 2020 at 23:24 Comment(15)
If I don't have the entitlement I can't even read the sqlite db.Rus
The db is in the user's documents folder, not inside the app. That's cuz they can create multiple db files, similar to say multiple excel spreadsheets or word documents.Rus
Que!? Why do you want to put them there?Cruet
Part of the app's use is using db files created by other people. Excel spreadsheets or word docs is a good analogy. MS could store them within Excel/Word but that would really suck.Rus
I have nothing more to say, but you seriously need to consider the location where you keep those files.Cruet
Okay, storing them in the app is a possibility but would really hurt user experience. Image having to import and export spreadsheets into/out of Excel to share them. Or Photoshop files, etc.Rus
It sounds as if you lived in a case without knowing how others keep their files.Cruet
Not sure what you mean? The programs I use daily - Excel, Word, Photoshop, Sublime Text, and more all keep their files in the documents folder. Even XCode.Rus
Those files are for your personal use as a end user. That's not where your application files should go. Anyway, I'm done.Cruet
Okay, I'll amend my question to make it clearer. These are files for the end user.Rus
Did you solve this?Salep
@Salep After banging my head on this issue for far too long, including going to the Apple Developer support forum, it appears there is no way to do this. So I gave up and used a different solution. Not as good in my opinion but it works and is not too bad. I store the sqlite files in the app data itself. This is allowed by MacOS. So each Sqlite file is created inside the app. To share it the user has to export it and another user import it (I use the Downloads folder to import/export to/from). Then the user can create, update and delete any file they want as long as it's within the app.Rus
Oh, sorry to hear that. Are your documents still backed up on iCloud? I assume they aren’t (can’t) be shared via iCloud with other devices owned by the same user..Salep
Combining CoreData (or SQLite directly) and iCloud into a document based app is just a mess unfortunately.Salep
Yeah I think you are right that it is not stored on iCloud.Rus
G
3

Use a Document Package

Your app can register a document file type that is presented as a regular file in Finder, but actually a directory. When the user selects the new location to save the file, the sandbox gives your app permission to write multiple files (including sqlite's temporary files) to the location selected by the user (and any sub-directories you want to create starting with that URL).

So if the user selects ~/Desktop/Untitled.appDocExtension to save their file, your app can write now write the following files: ~/Desktop/Untitled.appDocExtension/myFile1.sqlite and also ~/Desktop/Untitled.appDocExtension/myFile1.sqlite-journal.

GarageBand (.band) files would be an example of this type of document. You can tell a Finder file icon represents a document package if you right-click a document in Finder and you can select "Show Package Contents". (Basically like an App Bundle, of course.)

How to set this up:

  1. Add the user-selected-file read-write entitlement to your app: com.apple.security.files.user-selected.read-write
  2. Add a new Document Type to your app's Info.plist, including a unique filename extension and also make sure to add the LSTypeIsPackage key:
<key>CFBundleDocumentTypes</key>
    <array>
        <dict>
            <key>CFBundleTypeExtensions</key>
            <array>
                <string>appDocExtension</string>
            </array>
            <key>CFBundleTypeName</key>
            <string>MyDocTypeName</string>
            <key>CFBundleTypeRole</key>
            <string>Editor</string>
            <key>LSHandlerRank</key>
            <string>Default</string>
            <key>LSItemContentTypes</key>
            <array>
                <string>com.mydomain.typeidentifier</string>
            </array>
            <key>LSTypeIsPackage</key>
            <true/>
        </dict>
    </array>
  1. Add an Exported Type Declaration to your app's Info.plist (credit here).
<key>UTExportedTypeDeclarations</key>
    <array>
        <dict>
            <key>UTTypeConformsTo</key>
            <array>
                <string>com.apple.package</string>
                <string>public.composite-content</string>
            </array>
            <key>UTTypeDescription</key>
            <string>My Doc type description</string>
            <key>UTTypeIcons</key>
            <dict/>
            <key>UTTypeIdentifier</key>
            <string>com.mydomain.typeidentifier</string>
            <key>UTTypeTagSpecification</key>
            <dict>
                <key>public.filename-extension</key>
                <array>
                    <string>appDocExtension</string>
                </array>
            </dict>
        </dict>
    </array>

To save the document at run-time:

Present an NSSavePanel to the user:

        let savePanel = NSSavePanel()
        savePanel.canCreateDirectories = true
        savePanel.allowedFileTypes = ["appDocExtension"]
        savePanel.begin { response in
            guard response == .OK else {return}
            guard let url = savePanel.url else {return}
            DispatchQueue.main.async {[weak self] in
                // this directory will look like a single file in Finder
                try! FileManager.default.createDirectory(at: url, withIntermediateDirectories: false)
                // create a location under this directory to save your sqlite db:
                let dbURL = url.appendingPathComponent("myFile1.sqlite")
                // ... and now you can open and write to the db at dbURL
            }
        }

If the user wants to eventually get direct access to the raw sqlite file, they can:

  • Use the "Show Package Contents" command in Finder.
  • or Navigate to the file in Terminal.
  • or use an Export command that you provide in your app to copy the .sqlite file to a new location.

You can store whatever you want in these packages, so if you later want to add metadata or sidecar files, like images or other media, you can package them all together.

Gupta answered 12/1, 2023 at 4:6 Comment(1)
.xcodeproj and .xcworkspace would also be familiar examples of Document Packages.Gupta

© 2022 - 2024 — McMap. All rights reserved.