Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Application Sandbox: renaming a file doesn't work

I have a basic Cocoa app that let's the user manage a list of files. Files are added via drag & drop and I persist the access permissions in a secure bookmark across application restarts.

So far so good. The app is allowed to read and write to the user's files, but renaming fails with a permission error claiming that my app is not allowed to access the parent folder.

Code:

[[NSFileManager defaultManager] moveItemAtPath:currentPath 
                                        toPath:newPath error:&error]

Error:

Error Domain=NSCocoaErrorDomain Code=513 "“Some image.jpg” couldn’t 
be moved because you don’t have permission to access “some folder”

I swear that this used to work just yesterday, nothing changed... (see update below)

Anyway. I would assume that if a user allows access to a file via an Open dialog or drag & drop that a sandboxed application should be allowed to rename the file.

UPDATE:

I finally managed to track this down to a change in my code:

I can reproduce the error when I call NSFileCoordinator.item(at: newSrcUrl, willMoveTo: newDstUrl) before performing the actual move. If I skip this step, everything works again without any errors in the console.

The full code:

let coordinator = NSFileCoordinator()

coordinator.coordinate(writingItemAt: srcURL, options: .forMerging,
                       writingItemAt: dstURL, options: .forMerging) { newSrcUrl, newDstUrl in

    // Calling this first results in "could not get a sandbox extension" error in console:
    //coordinator.item(at: newSrcUrl, willMoveTo: newDstUrl)

    try moveItem(at: newSrcUrl, to: newDstUrl)

    coordinator.item(at: newSrcUrl, didMoveTo: newDstUrl)
}

item(at oldURL: URL, willMoveTo newURL: URL) seems to be intended when changing the document's filename extension (e.g. .rtf to .rtfd), which is not the case for my app. Only the file name changes, not the file extension. I figured it would be best practice to call the will/didMove methods, but looks like there are side effects.

To recap, all my app does is to ask the user to open a folder (via NSOpenPanel so my app gets permission to write to that folder), then my app creates a file in that folder and later on renames it.

like image 242
Mark Avatar asked Dec 19 '12 10:12

Mark


2 Answers

The problem you are facing is not about persisting anything, it's about how sandbox works :

The rule is…

To rename a file (in fact perform a move operation),
you need to have writing access to the parent directory of said file.

Our problem is…

If you drag files, sandbox will only extend access to these files, not their parent dir, hence this error saying it needs permission

Now, try dragging the folder containing these files, and you'll find that everything just works :)

So what do we do ?

A simple solution would be to ask the user to pick via NSOpenPanel a "working" directory, so that sandbox has access without alienating the user every time he wishes to rename
But we're now bothering the user for crap he shouldn't even know about in the first place !
So to me that's bad design/ux

Now i've read the docs about sandbox and i noticed NSFileCoordinator had a method called itemAtURL:willMoveToURL:

Wich led me to this little snippet (that i rewrote here because it was missing the willMove function)
what we want to do here is ask for a sandbox extension for the sake of renaming:

NSURL *sourceURL = document.fileURL;
NSURL *destinationURL = [[sourceURL URLByDeletingLastPathComponent] URLByAppendingPathComponent:fileName isDirectory:NO];

NSError *writeError = nil;
__block NSError *moveError = nil;
__block BOOL success = NO;

NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];

[coordinator coordinateWritingItemAtURL:sourceURL
                                options:NSFileCoordinatorWritingForMoving
                       writingItemAtURL:destinationURL
                                options:NSFileCoordinatorWritingForReplacing
                                  error:&writeError
                             byAccessor:^(NSURL *newURL1, NSURL *newURL2)
{
    NSFileManager *fileManager = [NSFileManager new];

    [coordinator itemAtURL:sourceURL willMoveToURL:destinationURL];

    success = [fileManager moveItemAtURL:newURL1 toURL:newURL2 error:&moveError];

    if (success)
    {
        [coordinator itemAtURL:newURL1 didMoveToURL:newURL2];
    }
}];

Sadly, it seems this method is only targeted at changing the file extension rather than renaming, hence this error in the log :

NSFileSandboxingRequestRelatedItemExtension: an error was received from pboxd instead of a token. Domain: NSPOSIXErrorDomain, code: 1

Yay, apple, yay

like image 77
Nightbirdsevolve Avatar answered Nov 11 '22 13:11

Nightbirdsevolve


The following works for me as long as I add myext as a related item document type in my Info.plist. This is noted in Apple's Documentation.
Here's an excerpt:

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.

Code modified from BenRhayader's answer:

   NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
    NSURL *sourceURL = chosenFile;
    NSURL *destinationURL = [chosenFile URLByAppendingPathExtension: @"myext"];
    
    [coordinator coordinateWritingItemAtURL:sourceURL
                                    options:NSFileCoordinatorWritingForMoving
                           writingItemAtURL:destinationURL
                                    options:NSFileCoordinatorWritingForReplacing
                                      error:NULL
                                 byAccessor:^(NSURL *newURL1, NSURL *newURL2)
    {
        NSFileManager *fileManager = [NSFileManager new];
        
        [coordinator itemAtURL:sourceURL willMoveToURL:destinationURL];
        NSError *moveError;
        BOOL success = [fileManager moveItemAtURL:newURL1 toURL:newURL2 error:&moveError];
        
        if (success)
        {
            [coordinator itemAtURL:newURL1 didMoveToURL:newURL2];
        }
    }];
like image 37
Jess the Mess Avatar answered Nov 11 '22 14:11

Jess the Mess