File icon does not show for sandboxed app

I have defined the UTI to get an icon associated to files created by the App.


This works well when the app is not sandboxed.


But when sandboxed, it does not work anymore.

In fact, I can't even create the file with a file extenssion : data.write() returns false if I have an extension attached.

It sounds like a security problem, not a UTI problem.


What is "data.write()"? Do you mean Data's "write(to:options:)" method? If so, it should be throwing an error that you can print.


Are you creating the file inside or outside the sandbox container? If outside, how do you get permission to do so?

data.write is data.write(to: fileUrl, atomically: true)

It does not throw, so I can just test if true or false


Here is the part of code :

          // docURL is a global var stored after NSSavePanel completes
           let documentsUrl = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask)[0] as URL
            let fileName = docURL!.lastPathComponent  // data.write does not work with extension
          //            let fileName = docURL!.deletingPathExtension().lastPathComponent       // data.write works if extension deleted
            let fileUrl = documentsUrl.appendingPathComponent(fileName)
            let result = data.write(to: fileUrl, atomically: true)   // 31.7.2017   Ca marche
            hideFileExtension(fileURLStringWithExtension: fileUrl.path)


In fact, I suspect that icon does not show because the file has not the extension

Use the newer "write" method instead!


It is odd that the presence of an extension should matter, but using the throwing method will tell you why it's failing.

Thanks,


So I used : write(to:options:) instead

      let fileName = docURL!.lastPathComponent  // data.write does not work with extension
      do {

        try data.write(to: fileUrl, options: [.atomic])
        }  catch {
            Swift.print(error)
        }

I get the following error thrown : "You are not authorized to save file Theleme22.xxx in Desktop directory

Error Domain=NSCocoaErrorDomain Code=513 "Vous n’êtes pas autorisé à enregistrer le fichier « Theleme22.xxxx » dans le dossier « Desktop »." UserInfo={NSFilePath=/Users/me/Library/Containers/com.company.AppName/Data/Desktop/Theleme22.xxxx, NSUnderlyingError=0x600000056920

No error if I replace line 01 with

  let fileName = docURL!.deletingPathExtension().lastPathComponent      // data.write works if extension deleted



Note: surprisingly, auto completion indicates that write(to:options:) is deprecated, but it is not. If I option click on the func name, it calls the func information with the following Availability information :

iOS (2.0 and later), macOS (10.4 and later), tvOS (9.0 and later), watchOS (2.0 and later)


Edited Aug 4

deprecation note has disappeared in 8.3.3.

Is "fileUrl" a security-scoped URL that you previously asked the user for? If so, you can't change the last component or extension and expect it to be security scoped still.


If you want permission to write any of several files in (say) Desktop, you must get the user to choose the Desktop folder using NSOpenPanel (rather than a file to save with NSSavePanel). Or, if this is is a document-scoped security-scoped URL, there's more to it (but I don't know the details offhand, since I've never had to use one of those).

No, I'm creating the file.


What is surprising is the difference between the 2 cases.

if url is defined as :

let fileName = docURL!.deletingPathExtension().lastPathComponent   


then the file is written.


but if url is defined as with an extension :

let fileName = docURL!.lastPathComponent


I get the error, the file is not written


The url is read with NSSavePanel dialog and is written for the first time by this part of code.


How can I ask for NSOpen if the file does not exist yet ? Or should the dialog be used only to select a folder (that'a a bit strange)

What's different between the two cases is the name (filename + extension). Since Desktop is a user-visible folder, you have to ask for permission, and you only get access to the thing you ask for.


If you use NSSavePanel, you get permission to create a file at the specified URL.


If you use NSOpenPanel, and you prompt the user to choose the folder, you get permission to access or create anything in the folder.


The document-scoped security scope is intended to mitigate this a little bit. If you have permission to save your document file (via NSSavePanel), it lets you save an "associated" file with a different extension in the same place. This sounds a bit like what you're doing, except I don't think you have documents. So, asking the user for access to the folder is what you'd have to do instead.

Thank you for all your help.


You guessed, It is not a document app.


I'll try to ask the permission for, with NSOpenPanel.


I think I've understood why it works without extension.

- In the NSSavePanel, I get a fileURL.

- I complete it with the extension, so that the user has not to know about the extension, but that allows to set icon.

So the URL known from NSSavePanel is without extension.

- If I save without extension, it works.

- But using the extension creates another file, as you explained. And writing is denied


I've tested by:

- commenting out the addition of extension

- typing name with extension in NSSavePanel


It works.And file shows with its icon.


So, I'll do either :

- call NSOpenPanel, but I fear how much user friendly it will be, as I will have first to ask for NSOpenPanel and then for NSSavePanel to get the name of the file, in the same directory !!!!

- try to add programmatically the extension "during typing", but that may just be impossible. And as I create a second file with a different name, I think that will not work anyway.


Otherwise, I'll be forced to give up sandboxing.

If you're just creating a single file (with the extension), then you can set the NSSavePanel to not show the extension. That way, the user types just the filename and the URL you get back will already have the extension in it, and you'll be able to create it.


You should also set the save panel to allow only that extension. That way, if the user accidentally types a different extension, there'll be an error message preventing them from using that name.

Thanks, I changed to allowFileTypes to only one in the NSSavePanel ; now it works when I write to desktop directory as follows:

let documentsUrl = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask)[0] as URL  // .documentDirectory
let fileName = fromNSPanelURL!.lastPathComponent
let fileUrl = documentsUrl.appendingPathComponent(fileName)
do {
        try data.write(to: fileUrl, options: [.atomic])
        }  catch {
            Swift.print(error)
        }


But now, I need to create a companion file, with a different name (automatically built, not asked to user), in the same directory.

I tried to follow advices you gave me in another thread :

"1. You must use NSSavePanel to request the location from the user. The user can choose any folder (subject to normal access restrictions such as file permissions based on the login user ID).

2. The URL you get back from the NSSavePanel is a special secure URL. You can use that URL (that instance of that URL object) to form a URL for the file you want to write there (and AFAIK you can use any folder nested inside the folder that the user chose).

3. I believe (but I'm not sure) that the system caches this security information, so you should be able to use a path matching the URL, but you should be using URLs exclusively these days, so this should not be an issue."

The point is that I do not want to ask a second time NSSavePanel for the companion.


I tried to do it with NSOpenPanel, asking for directory only, but I do not understand how to connect the 2.

I wrote this, but I miss something to go on : companion file is not written, there is a flaw somewhere in my logic


    @IBAction func newDossier(_ sender: NSMenuItem) {

        let openPanel = NSOpenPanel()
        openPanel.message = "Choose a directory to save"
        openPanel.prompt = "Select"
        openPanel.canChooseFiles = false
        openPanel.canChooseDirectories = true
        openPanel.begin() {
            (result2) -> Void in
            if (result2 == NSFileHandlingPanelOKButton) {
              // Now I open the NSSavePanel
                let savePanel = NSSavePanel()
                savePanel.title = "File to save"
                savePanel.prompt = "Save"
                savePanel.allowedFileTypes = ["xxxx"]
                let fileManager = FileManager.default    
       
                savePanel.begin() { (result) -> Void in
                    if (result == NSFileHandlingPanelOKButton) {
                        let fileWithExtensionURL = savePanel.url!  //  with extension xxxx
                        if fileManager.fileExists(atPath: fileWithExtensionURL.path) {
                            let fileName = fileWithExtensionURL.deletingPathExtension().lastPathComponent
                            let folderName = fileWithExtensionURL.deletingLastPathComponent().lastPathComponent
                            self.errorExistingFile(fileName: fileName, folderName: folderName)
                        } else {
                   
                            let companionFileUrl = // Built programmatically
                          
                            let _ = saveFile()  // This works, because it is the selected file in NSSave
                            let _ = saveCompanion()    // How to make it work ? That' where I need to connect with authorization given by NSOpenPanel ?????
                            // I hideFileExtension
                        }
               
                    }      // result == NSModalResponseOK
                }      // beginWithCompletionHandler
       
            }
        }
    }



In addition, to write to a directory different from desktop, I understand I need to pass the url I got from NSOPenPanel ?

The right approach depends on what you're trying to achieve.


— If you're trying to create a "package" of files, then you should create an actual package. For that, the URL from the save panel is the name of a folder you create, and you can put whatever files you want inside it. By setting up your UTI for this folder's extension properly, this appears as a single file to the user.


— If you want the user to see both files as separate entities, then it's a bit unpleasant (in the UI sense) to simply splat the second file alongside the first. What if there's already a file of that name? What if there's a file of that name from an earlier save? Do you replace the secondary file (under the assumption you created it), or not (because it might be the user's very important information)? How do you give a "do you want to replace…" alert during the save panel, like it would for the original file? Those are all reasons for using a save panel to ask where to save any file, aside even from security considerations.


Or, you could put both files in a folder, but not as a package, so the user can see both by opening the folder.


I agree that there's no good way to use an open and a save panel in combination, as the above code show. You could use 2 save panels, one after the other, which would work fine but be annoying. So I think you actually need to rethink the UI here.

Effectively, I want the second option, so that user can manage the 2 files independantly later. User can also later create a new companion file, to save intermediate results for instance.

In fact, a user may have several companion file associalted to a single original file, which correspond to different result.


I test if the file already exists and alert user in that case ; he can then create a new companion file with a name of his choice.

This case is so rare that I really prefer, from UI point of view to try first automatically and just ask by exception.


I understand I hit some limits imposed by sandboxing.

I'll try the 2 solutions :

- double NSSavePanel: I will set nameFieldStringValue with the computed name, to ease a bit. I understand directory will be initialized automatically to the first one.

- new Folder: What should be the solution creating a folder ? Will that require to display 2 dialogs for folder creation and then file creation ? I really do not see the sequence of calls.

I would recommend you ask the user for a folder to save into, and you would then create a folder of the specified name on the user's behalf. In this case, the folder doesn't use the extension — it can be called whatever the user wants, though you can prefill a suggested name.


That way, it takes only one dialog (NSSavePanel). You are then allowed to create the folder in code, and you can create whatever files you want inside the folder. So long as you save this URL as a security scoped bookmark for later, when you need to create additional "companion" files, you will be able to just write them inside the same folder, without any file dialogs.

Do you suggest to ask user to create the folder and name the file all in the same NSSavePanel ?

Or cretae the folder and later manage to create the file (I want the main file to be given a name by user).


Last question: what is the recommended place to save bookmarks ? NSUserDefaults ?

Accepted Answer

Hmm, I hadn't thought about the need for 2 names. I guess I was assuming that the folder and the main file inside the foler would have the same name (or the same base filename, with possibly an extension on the file inside the folder).


My preference would be to save the bookmarks as data files in Application Support (in the user domain though, not the system domain). I don't see any real problem with using UserDefaults instead, though.

File icon does not show for sandboxed app
 
 
Q