Copying files using Finder and Apple Events

I need my application to copy some files, but using Finder. Now, I know all different methods and options to programmatically copy files using various APIs, but that's not the point here. I specifically need to use Finder for the purpose, so please, let's avoid eventual suggestions mentioning other ways to copy files.

My first thought was to use the most simple approach, execute an AppleScript script using NSUserAppleScriptTask, but that turned out not to be ideal. It works fine, unless there already are files with same names at the copying destination. In such case, either the script execution ends with an error, reporting already existing files at the destination, or the existing files can be simply overridden by adding with overwrite option to duplicate command in the script.

What I need is behaviour just like when Finder is used from the UI (drag'n'drop, copy/paste…); if there are existing files with same names at the destination, Finder should offer a "resolution panel", asking the user to "stop", "replace", "don't replace", "keep both" or "merge" (the latter in case of conflicting folders). So, I came to suspect that I could achieve such bahaviour by using Apple Events directly and passing kAEAlwaysInteract | kAECanSwitchLayer options to AESendMessage(). However, I can't figure out how to construct appropriate NSAppleEventDescriptor (nor old-style Carbon AppleEvent) objects and instruct Finder to copy files.

This is where I came so far, providing srcFiles are source files (to be copied) URLs and dstFolder destination folder (to be copied into) URL:

NSRunningApplication *finder = [[NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.finder"] firstObject];
if (!finder)
{
    NSLog(@"Finder is not running.");
    return;
}

NSAppleEventDescriptor *finderDescriptor = [NSAppleEventDescriptor descriptorWithBundleIdentifier:[finder bundleIdentifier]];
NSAppleEventDescriptor *dstDescriptor = [NSAppleEventDescriptor descriptorWithString:[dstFolder path]];
NSAppleEventDescriptor *srcDescriptor = [NSAppleEventDescriptor listDescriptor];
for (NSURL *url in srcFiles)
{
    NSAppleEventDescriptor *fileDescriptor = [NSAppleEventDescriptor descriptorWithString:[url path]];
    [srcDescriptor insertDescriptor:fileDescriptor atIndex:([srcDescriptor numberOfItems] + 1)];
}

NSAppleEventDescriptor *event = [NSAppleEventDescriptor appleEventWithEventClass:kAECoreSuite
                                                                         eventID:kAEClone
                                                                targetDescriptor:finderDescriptor
                                                                        returnID:kAutoGenerateReturnID
                                                                   transactionID:kAnyTransactionID];
[event setParamDescriptor:srcDescriptor forKeyword:keyDirectObject];
[event setParamDescriptor:dstDescriptor forKeyword:keyAETarget];

NSError *error;
NSAppleEventDescriptor *result = [event sendEventWithOptions:(NSAppleEventSendAlwaysInteract | NSAppleEventSendCanSwitchLayer) timeout:10.0 error:&error];

The code above executes without any error. The final result descriptor is a NULL descriptor ([NSAppleEventDescriptor nullDescriptor]) and there's no error returned (by reference). However, nothing happens, Finder remains silent and the application doesn't make macOS/TCC prompt for a permission to "automate Finder".

I wonder if the approach above is correct and if I use correct parameters as arguments for all calling method/messages. I'm specially interested if passing keyAETarget is the right value in [event setParamDescriptor:dstDescriptor forKeyword:keyAETarget], since that one looks most suspicious to me. I'd really appreciate if anyone can help me with this.

I'd also like to point out that I tried the same approach outlined above with old-style Carbon AppleEvent API, using AECreateDesc(), AECreateAppleEvent(), AEPutParamDesc() and AESendMessage()… All API calls succeeded, returning noErr, but again, nothing happened, Finder remained silent and no macOS/TCC prompt for a permission to "automate Finder".

Any help is highly appreciated, thanks!

-- Dragan

Also, just to mention that I tried the same above code, but creating URL-based descriptors for source and destination, instead of text-based ones. So, instead of:

[NSAppleEventDescriptor descriptorWithString:[url path]]

using

[NSAppleEventDescriptor descriptorWithFileURL:url]

The outcome was exactly the same.

Any help is highly appreciated, thanks!

Have you jumped through all the hoops so that your app can actually send Apple Events?

Here is some old documentation that might be a good starting point: https://developer.apple.com/library/archive/qa/qa1888/_index.html

And here is a previous question from someone managed to get it working, after a fashion: https://developer.apple.com/forums/thread/771769

Any attempt to script the Finder is going to limit your app. It won't be acceptable for the Mac App Store.

Any new code that relies on AppleScript is risky. Apple is already transitioning to Shortcuts. And Apple has begun to be much more aggressive about deprecating and discontinuing technologies, pretty much all of which are much recent than Apple Events.

In older apps where I still did such things, I had good success with running the "osascript" command-line tool with carefully constructed parameters.

Have you jumped through all the hoops so that your app can actually send Apple Events?

Here is some old documentation that might be a good starting point: https://developer.apple.com/library/archive/qa/qa1888/_index.html

The application isn't sandboxed, so I didn't add any additional entitlements and it should not matter. Moreover, if it was sandboxed I'd expect some kind of error in the console regarding sandboxing limitations, as it usually happens in such cases. But in this case, everything is silent (but doesn't work).

NSAppleEventsUsageDescription should not matter either, but even with that added, nothing changes.

Any new code that relies on AppleScript is risky. Apple is already transitioning to Shortcuts. And Apple has begun to be much more aggressive about deprecating and discontinuing technologies, pretty much all of which are much recent than Apple Events.

I realise all that. However, I don't see why I wouldn't use it while it's still there and hasn't been deprecated yet. I can't switch to Shortcuts yet, as I need to support some older OS versions.

In older apps where I still did such things, I had good success with running the "osascript" command-line tool with carefully constructed parameters.

This approach works the same way as when executing AppleScript script with NSUserAppleScriptTask (or otherwise) and I've mentioned in my initial post why such approach isn't working for me in case files with same names already exist at the destination.

It looks like you've put a lot of thought into this question already. Not to discount any of that but it seems to me that if you're able to get NSUserAppleScriptTask working you could write a slightly more complicated script that checks for files in the destination before saving and then takes appropriate action inside of the script. The Finder defines an 'exists' verb in its dictionary that should be very helpful for that sort of thing.

The application isn't sandboxed, so I didn't add any additional entitlements and it should not matter.

Well there's still the Apple Events capability in "Signing & Capabilities". You have to provide a message to the user about why your app needs AppleEvents. I'm not sure what happens if that's missing.

I don't see why I wouldn't use it while it's still there and hasn't been deprecated yet.

Just because something hasn't been deprecated doesn't mean that it's reliable.

This approach works the same way as when executing AppleScript script with NSUserAppleScriptTask

I'm going to go ahead and disagree on that point. But I assume you're only talking about the end result for your use case.

I've mentioned in my initial post why such approach isn't working for me in case files with same names already exist at the destination.

Are you sure that's even possible? While the Finder is an unusual app, it's still a stand-alone app. Just because it exposes certain functionality via AppleScript doesn't guarantee that said functionality is going to be the same as what the user experiences when interacting directly with the app. There's a strong argument to say that it shouldn't. People who are writing scripts want their script to run in their own UI and logic flow. They generally don't want a service provider to inject its own UI into the process.

So I think your suspicions here:

I came to suspect that I could achieve such bahaviour by using Apple Events directly

are likely unfounded.

I don't understand why it's so important to use the Finder. Writing your own conflict resolution UI would be a lot easier than writing anything in AppleScript. I'm aware of Finder's "merge" functionality, but it's really not very good. It's quite difficult to trigger it in the first place. That strongly suggests that it's an artifact of the user's interaction with the Finder's file listing rather than a lower-level, scriptable operation.

@DTS Engineer,

Not to discount any of that but it seems to me that if you're able to get NSUserAppleScriptTask working you could write a slightly more complicated script that checks for files in the destination before saving and then takes appropriate action inside of the script. The Finder defines an 'exists' verb in its dictionary that should be very helpful for that sort of thing.

Right, and it sounds like a very sensible thing to do. However, as I've mentioned at the very beginning of my initial post, I need to programmatically invoke exactly the same behaviour of Finder as if a user copies files in it using standard UI-based methods. "Why is that?" discussion is out of the scope of the question. I appreciate your effort to help, I really do, but the solution you suggested isn't what I need.

@Etresoft,

Well there's still the Apple Events capability in "Signing & Capabilities". You have to provide a message to the user about why your app needs AppleEvents. I'm not sure what happens if that's missing.

I don't remember from when I've done it previously, but I think OS just offers some default message presented to the user.

I'm going to go ahead and disagree on that point. But I assume you're only talking about the end result for your use case.

I haven't tried it, but "osascript" just executes scripts, without providing any extra functionality to control them. Since the plain script exhibits the undesirable (for me) behaviour with already existing files, I assume using "osascript" would result in the same.

Are you sure that's even possible? So I think your suspicions here are likely unfounded.

I'm not sure, but for the beginning, I first need to programmatically make Finder to react. And at the moment I'm not able to, due to all the explanations I provided in the initial post.

I don't understand why it's so important to use the Finder. Writing your own conflict resolution UI would be a lot easier than writing anything in AppleScript.

Like in my previous response, discussion "why?" is out of the scope of the question. Like I've said, I know how to write code to copy files using various and all available currently functioning, stable and non-deprecated APIs. But I need what I asked for, and that's using Finder like it's being used by a regular user. I know it sounds strange, but that's how it is.

That's why I mentioned it at the very beginning to save you from spending time offering me what I don't need. Again here, I appreciate your effort to help, but the solution you suggested isn't what I need. At this moment I need to know how to use NSAppleEventDescriptor (or AESendMessage() for what it matters) to make Finder copy files. Or a straight answer that it isn't possible.

Copying files using Finder and Apple Events
 
 
Q