Batch move to Trash with permissions


(Phil Stokes) #1

I just want to check I haven’t missed any other possibilities.

Situation: moving multiple files that require authorisation to the Trash
Problem: I want to do it with only one authorisation request, not one for every file in the batch, which is tedious when there’s a large number.

OK, options that I know of

  1. Tell Finder to delete
  2. Tell System Events to move to trash
  3. Call NSFileManager’s removeItemAtPath
  4. do shell script with administrator privileges

All of these have issues.
1 requires the authorisation for every file.
2 & 3 appear to fail if authorisation is required, and don’t show an auth request dialog.
4 works, but the question is what command to do. rm -rf is dangerous, and deletes directly rather than sending to Trash. mv I guess could be used to put things in the .Trashes folders, but I’m not sure of the consequences of “going behind the OS’s back”, and it just seems kind of hacky to me.

Any other options?


(Shane Stanley) #2

There’s trashItemAtURL:::, although if removeItemAtPath: is problematic, I’d expect it to be the same.


(Phil Stokes) #3

Thanks, Shane; I’m pretty sure I’ve tried that in the past, but I’d forgotten about it. I suppose all the Cocoa APIs will fail as apps should really be doing it via XPC and some kind of PrivilegedHelper tool.

For scripting purposes, I guess the shell is the only route.


(Jonas Whale) #4

Maybe you already know this tool but in case…
trash

:wink:


(Phil Stokes) #5

Interesting, thanks. He seems to have struggled with the same issue and come up with this solution. I don’t know if anyone wants to comment on it…

static OSStatus askFinderToMoveFilesToTrash(NSArray *filePaths, BOOL bringFinderToFront)
{
    // Here we manually send Finder the Apple Event that tells it
    // to trash the specified files all at once. This is roughly
    // equivalent to the following AppleScript:
    //
    //   tell application "Finder" to delete every item of
    //     {(POSIX file "/path/one"), (POSIX file "/path/two")}
    //
    // First of all, this doesn't seem to be possible with the
    // Scripting Bridge (the -delete method is only available
    // for individual items there, and we don't want to loop
    // through items, calling that method for each one because
    // then Finder would prompt for authentication separately
    // for each one).
    //
    // The second approach I took was to construct an AppleScript
    // string that looked like the example above, but this
    // seemed a bit volatile. 'has' suggested in a comment on
    // my blog that I could do something like this instead,
    // and I thought it was a good idea. Seems to work just
    // fine and this is noticeably faster this way than generating
    // and executing some AppleScript was. I also don't have
    // to worry about input sanitization anymore.
    //

    // generate list descriptor containting the file URLs
    NSAppleEventDescriptor *urlListDescr = [NSAppleEventDescriptor listDescriptor];
    NSInteger i = 1;
    for (NSString *filePath in filePaths)
    {
        NSURL *url = [NSURL fileURLWithPath:getAbsolutePath(filePath)];
        NSAppleEventDescriptor *descr = [NSAppleEventDescriptor
            descriptorWithDescriptorType:'furl'
            data:[[url absoluteString] dataUsingEncoding:NSUTF8StringEncoding]
            ];
        [urlListDescr insertDescriptor:descr atIndex:i++];
    }

    // generate the 'top-level' "delete" descriptor
    pid_t finderPID = getFinderPID();
    NSAppleEventDescriptor *targetDesc = [NSAppleEventDescriptor
        descriptorWithDescriptorType:typeKernelProcessID
        bytes:&finderPID
        length:sizeof(finderPID)
        ];
    NSAppleEventDescriptor *descriptor = [NSAppleEventDescriptor
        appleEventWithEventClass:'core'
        eventID:'delo'
        targetDescriptor:targetDesc
        returnID:kAutoGenerateReturnID
        transactionID:kAnyTransactionID
        ];

    // add the list of file URLs as argument
    [descriptor setDescriptor:urlListDescr forKeyword:'----'];

    if (bringFinderToFront)
        [getFinderApp() activate];

    // send the Apple Event synchronously
    AppleEvent replyEvent;
    OSStatus sendErr = AESendMessage([descriptor aeDesc], &replyEvent, kAEWaitReply, kAEDefaultTimeout);
    if (sendErr != noErr)
        return sendErr;

    // check reply in order to determine return value
    AEDesc replyAEDesc;
    OSStatus getReplyErr = AEGetParamDesc(&replyEvent, keyDirectObject, typeWildCard, &replyAEDesc);
    if (getReplyErr != noErr)
        return getReplyErr;

    NSAppleEventDescriptor *replyDesc = [[[NSAppleEventDescriptor alloc] initWithAEDescNoCopy:&replyAEDesc] autorelease];
    if ([replyDesc numberOfItems] == 0
        || (1 < filePaths.count && ([replyDesc descriptorType] != typeAEList || [replyDesc numberOfItems] != (NSInteger)filePaths.count)))
        return kHGNotAllFilesTrashedError;

    return noErr;
}