Finder File Copy

I often need to copy files which are selected in a Finder window to one of a small number of frequently-used destination folders. This is easily done with a mouse but I prefer to use a keyboard, so I wrote the script contained below. The operation of this script requires little explanation except to note that the script copies files only.

-- Revised 2021.07.01

use framework "AppKit"
use framework "Foundation"
use scripting additions

on main()
	set preferencesFolder to POSIX path of (path to preferences)
	set settingPlist to preferencesFolder & "FinderCopySetting.plist"
	set dataPlist to preferencesFolder & "FinderCopyData.plist"
	
	try
		set dialogDefault to settingOne of readPlist(settingPlist) as text
	on error
		set dialogDefault to "Add a Destination"
	end try
	
	try
		set destinationFolders to readPlist(dataPlist)
		set destinationNames to (destinationFolders's allKeys()'s sortedArrayUsingSelector:"caseInsensitiveCompare:") as list
	on error
		set destinationFolders to (current application's NSMutableDictionary's new())
		set destinationNames to {}
	end try
	
	set dialogMenu to destinationNames & {"--", "Add a Destination", "Delete a Destination"}
	set dialogResult to (choose from list dialogMenu with title "Finder Copy" default items dialogDefault)
	if dialogResult = false then error number -128
	set dialogResult to item 1 of dialogResult
	set destinationFolder to (destinationFolders's valueForKey:dialogResult)
	
	if dialogResult = "Add a Destination" then
		set {destinationFolders, currentName} to addDestination(destinationFolders, destinationNames)
		writePlist(dataPlist, destinationFolders)
		set dialogResult to currentName
	else if dialogResult = "Delete a Destination" then
		set destinationFolders to deleteDestination(destinationFolders, destinationNames)
		writePlist(dataPlist, destinationFolders)
	else if dialogResult begins with "-" then
		error number -128
	else
		copyOne(destinationFolder)
	end if
	
	set theSetting to current application's NSDictionary's dictionaryWithDictionary:{settingOne:dialogResult}
	writePlist(settingPlist, theSetting)
end main

on addDestination(destinationFolders, destinationNames)
	try
		tell application "Finder"
			set currentName to name of Finder window 1
			set currentPath to target of Finder window 1 as alias
		end tell
		set currentPath to POSIX path of currentPath
	on error
		errorDialog("Add a Destination", "A Finder window was not found or the target of the front Finder window cannot be a destination")
	end try
	
	set dialogPath to (current application's NSString's stringWithString:currentPath)'s pathComponents()
	if (count dialogPath) < 7 then
		set dialogPath to text 1 thru -2 of currentPath
	else
		set dialogPath to current application's NSString's stringWithFormat_("/%@/%@/.../%@/%@", item 2 of dialogPath, item 3 of dialogPath, item -3 of dialogPath, item -2 of dialogPath) as text
	end if
	
	set currentName to text returned of (display dialog "Destination name for " & quote & dialogPath & quote default answer currentName buttons {"Cancel", "OK"} default button 2 cancel button 1 with title "Finder Copy")
	if currentName = "" then error number -128
	if currentName is in destinationNames then errorDialog("Add a Destination", "A destination named " & quote & currentName & quote & " already exists")
	
	destinationFolders's setObject:currentPath forKey:currentName
	return {destinationFolders, currentName}
end addDestination

on deleteDestination(destinationFolders, destinationNames)
	if destinationNames = {} then errorDialog("Delete a Destination", "There are no destinations to delete")
	
	set deleteItems to (choose from list destinationNames with title "Finder Destination" with prompt "Select destinations to delete:" default items {item 1 of destinationNames} with multiple selections allowed)
	if deleteItems = false then error number -128
	
	set deleteItems to current application's NSArray's arrayWithArray:deleteItems
	(destinationFolders's removeObjectsForKeys:deleteItems)
	return destinationFolders
end deleteDestination

on copyOne(thePath)
	tell application "Finder" to set selectedFiles to selection as alias list
	if selectedFiles = {} then errorDialog("Finder Copy", "File not selected or selected item cannot be copied")
	set fileManager to current application's NSFileManager's defaultManager()
	
	repeat with aFile in selectedFiles
		copyTwo(POSIX path of aFile, thePath, fileManager)
	end repeat
end copyOne

on copyTwo(aFile, thePath, fileManager)
	set aFileURL to (current application's |NSURL|'s fileURLWithPath:aFile)
	set aFileString to (current application's NSString's stringWithString:aFile)
	set aFileName to aFileString's lastPathComponent()
	set aNewFile to (thePath's stringByAppendingPathComponent:aFileName)
	if (aFileString's isEqual:aNewFile) then errorDialog("File Copy", "The source and destination folders are the same")
	
	set {theResult, isDirectory} to (aFileURL's getResourceValue:(reference) forKey:(current application's NSURLIsDirectoryKey) |error|:(missing value))
	set {theResult, isPackage} to (aFileURL's getResourceValue:(reference) forKey:(current application's NSURLIsPackageKey) |error|:(missing value))
	if isDirectory as boolean = true and isPackage as boolean = false then
		display alert "Finder Copy" message quote & aFileName & quote & " is a folder" buttons {"Cancel", "Skip"} default button 2 cancel button 1 as critical
		return
	end if
	
	if (fileManager's fileExistsAtPath:aNewFile) then
		display alert "Finder Copy" message "The destination already contains " & quote & aFileName & quote buttons {"Cancel", "Skip", "Overwrite"} default button 2 cancel button 1 as critical
		if button returned of result = "Skip" then return
		set aNewFileURL to current application's |NSURL|'s fileURLWithPath:aNewFile
		(fileManager's trashItemAtURL:aNewFileURL resultingItemURL:(missing value) |error|:(missing value))
	end if
	
	set theResult to (fileManager's copyItemAtPath:aFileString toPath:aNewFile |error|:(missing value))
	if (theResult as boolean) is false then errorDialog("File Copy", "An error occurred while copying " & quote & aFileName & quote)
end copyTwo

on readPlist(thePath)
	return (current application's NSMutableDictionary's dictionaryWithContentsOfFile:thePath)
end readPlist

on writePlist(thePath, theDictionary)
	theDictionary's writeToFile:thePath atomically:true
end writePlist

on errorDialog(textOne, textTwo)
	display alert textOne message textTwo buttons {"OK"} default button 1 as critical
	error number -128
end errorDialog

main()

Hi peavine
I use your other script Bookmark Finder windows too

wondering if the script be modified with Another option “keep both files” if it already exists in target folder, renaming it (cud be appending 1 or similar)

And if possible to move & merge folders if they exist at Destination

Cheers

One208. Thanks for looking at my script and for the suggestions.

Replacing the skip option with a keep-both option makes sense and, in retrospect, would be my preference. I’ll work on that and post a new script here when done.

The following script is similar to the one in post 1, differing only in that the option to skip an existing file has been replaced with an option to keep both files. When this option is selected, the newly-created file contains a counter, the format of which is set by the following code. This is easily changed to whatever the user prefers.

set fileCounter to " - copy  " & (i as text)

I have tested both of my scripts at some length. but please have a reliable backup of all files during your testing of either of these scripts.

-- Revised 2021.07.03

use framework "AppKit"
use framework "Foundation"
use scripting additions

on main()
	set preferencesFolder to POSIX path of (path to preferences)
	set settingPlist to preferencesFolder & "FinderCopySetting.plist"
	set dataPlist to preferencesFolder & "FinderCopyData.plist"
	
	try
		set dialogDefault to settingOne of readPlist(settingPlist) as text
	on error
		set dialogDefault to "Add a Destination"
	end try
	
	try
		set destinationFolders to readPlist(dataPlist)
		set destinationNames to (destinationFolders's allKeys()'s sortedArrayUsingSelector:"caseInsensitiveCompare:") as list
	on error
		set destinationFolders to (current application's NSMutableDictionary's new())
		set destinationNames to {}
	end try
	
	set dialogMenu to destinationNames & {"--", "Add a Destination", "Delete a Destination"}
	set dialogResult to (choose from list dialogMenu with title "Finder Copy" default items dialogDefault)
	if dialogResult = false then error number -128
	set dialogResult to item 1 of dialogResult
	set destinationFolder to (destinationFolders's valueForKey:dialogResult)
	
	if dialogResult = "Add a Destination" then
		set {destinationFolders, currentName} to addDestination(destinationFolders, destinationNames)
		writePlist(dataPlist, destinationFolders)
		set dialogResult to currentName
	else if dialogResult = "Delete a Destination" then
		set destinationFolders to deleteDestination(destinationFolders, destinationNames)
		writePlist(dataPlist, destinationFolders)
	else if dialogResult begins with "-" then
		error number -128
	else
		copyOne(destinationFolder)
	end if
	
	set theSetting to current application's NSDictionary's dictionaryWithDictionary:{settingOne:dialogResult}
	writePlist(settingPlist, theSetting)
end main

on addDestination(destinationFolders, destinationNames)
	try
		tell application "Finder"
			set currentName to name of Finder window 1
			set currentPath to target of Finder window 1 as alias
		end tell
		set currentPath to POSIX path of currentPath
	on error
		errorDialog("Add a Destination", "A Finder window was not found or the target of the front Finder window cannot be a destination")
	end try
	
	set dialogPath to (current application's NSString's stringWithString:currentPath)'s pathComponents()
	if (count dialogPath) < 7 then
		set dialogPath to text 1 thru -2 of currentPath
	else
		set dialogPath to current application's NSString's stringWithFormat_("/%@/%@/.../%@/%@", item 2 of dialogPath, item 3 of dialogPath, item -3 of dialogPath, item -2 of dialogPath) as text
	end if
	
	set currentName to text returned of (display dialog "Destination name for " & quote & dialogPath & quote default answer currentName buttons {"Cancel", "OK"} default button 2 cancel button 1 with title "Finder Copy")
	if currentName = "" then error number -128
	if currentName is in destinationNames then errorDialog("Add a Destination", "A destination named " & quote & currentName & quote & " already exists")
	
	destinationFolders's setObject:currentPath forKey:currentName
	return {destinationFolders, currentName}
end addDestination

on deleteDestination(destinationFolders, destinationNames)
	if destinationNames = {} then errorDialog("Delete a Destination", "There are no destinations to delete")
	
	set deleteItems to (choose from list destinationNames with title "Finder Destination" with prompt "Select destinations to delete:" default items {item 1 of destinationNames} with multiple selections allowed)
	if deleteItems = false then error number -128
	
	set deleteItems to current application's NSArray's arrayWithArray:deleteItems
	(destinationFolders's removeObjectsForKeys:deleteItems)
	return destinationFolders
end deleteDestination

on copyOne(thePath)
	tell application "Finder" to set selectedFiles to selection as alias list
	if selectedFiles = {} then errorDialog("Finder Copy", "File not selected or selected item cannot be copied")
	set fileManager to current application's NSFileManager's defaultManager()
	
	repeat with aFile in selectedFiles
		copyTwo(POSIX path of aFile, thePath, fileManager)
	end repeat
end copyOne

on copyTwo(aFile, thePath, fileManager)
	set aFileURL to (current application's |NSURL|'s fileURLWithPath:aFile)
	set aFileString to (current application's NSString's stringWithString:aFile)
	set aFileName to aFileString's lastPathComponent()
	set aNewFile to (thePath's stringByAppendingPathComponent:aFileName)
	
	if (aFileString's isEqual:aNewFile) then errorDialog("File Copy", "The source and destination folders are the same")
	
	set {theResult, isDirectory} to (aFileURL's getResourceValue:(reference) forKey:(current application's NSURLIsDirectoryKey) |error|:(missing value))
	set {theResult, isPackage} to (aFileURL's getResourceValue:(reference) forKey:(current application's NSURLIsPackageKey) |error|:(missing value))
	if isDirectory as boolean = true and isPackage as boolean = false then
		display alert "Finder Copy" message quote & aFileName & quote & " is a folder" buttons {"Cancel", "Skip"} cancel button 1 default button 2 as warning
		return
	end if
	
	if (fileManager's fileExistsAtPath:aNewFile) then
		display alert "Finder Copy" message "The destination already contains " & quote & aFileName & quote buttons {"Cancel", "Replace", "Keep Both"} cancel button 1 default button 3 as warning
		if button returned of result = "Keep Both" then
			set aNewFile to makeNewFileName(aNewFile, fileManager)
		else
			fileManager's trashItemAtURL:(current application's |NSURL|'s fileURLWithPath:aNewFile) resultingItemURL:(missing value) |error|:(missing value)
		end if
	end if
	
	set theResult to (fileManager's copyItemAtPath:aFileString toPath:aNewFile |error|:(missing value))
	if (theResult as boolean) is false then errorDialog("File Copy", "An error occurred while copying " & quote & aFileName & quote & ". Please vertify that the destination folder exists.")
end copyTwo

on makeNewFileName(theFile, fileManager)
	set fileBase to theFile's stringByDeletingPathExtension()
	set fileExtension to theFile's pathExtension()
	repeat with i from 1 to 100
		set fileCounter to " - copy " & (i as text)
		set newFile to ((fileBase's stringByAppendingString:fileCounter)'s stringByAppendingPathExtension:fileExtension)
		if not (fileManager's fileExistsAtPath:newFile) then return newFile
	end repeat
end makeNewFileName

on readPlist(thePath)
	return (current application's NSMutableDictionary's dictionaryWithContentsOfFile:thePath)
end readPlist

on writePlist(thePath, theDictionary)
	theDictionary's writeToFile:thePath atomically:true
end writePlist

on errorDialog(textOne, textTwo)
	display alert textOne message textTwo buttons {"OK"} default button 1 as critical
	error number -128
end errorDialog

main()

A few final comments:

  • I have tested my scripts with macOS Catalina only.

  • When a file exists in the destination folder, the user is presented with the option to overwrite that file. This is not actually what happens, as the existing file is instead moved to the trash, which is done to reduce the possibility of data loss if something goes awry.

  • When the script moves a file to the trash, as explained above, and if the user needs to restore that file, the “put back” option is often missing or does not work as expected. The trashed file can instead be moved with a mouse to whatever folder the user wants.

  • My scripts can be made to move rather than copy selected files by changing copyItemAtPath:aFileString to moveItemAtPath:aFileString. In very preliminary testing this appears to work as expected.

This script is functionally the same as that in post 4 above, but I’ve optimized it a bit.

use framework "AppKit"
use framework "Foundation"
use scripting additions

on main()
	try
		set dialogDefault to readPlist("dialogDefaultKey") as text
		set destinationFolders to readPlist("destinationFoldersKey")'s mutableCopy()
		set destinationNames to ((destinationFolders's allKeys())'s sortedArrayUsingSelector:"localizedStandardCompare:")
	on error
		set dialogDefault to "Add a Destination"
		set destinationFolders to (current application's NSMutableDictionary's new())
		set destinationNames to current application's NSArray's arrayWithArray:{}
	end try
	
	set dialogMenu to destinationNames's arrayByAddingObjectsFromArray:{"--", "Add a Destination", "Delete a Destination"}
	set dialogResult to (choose from list (dialogMenu as list) with title "File Copy" default items dialogDefault)
	if dialogResult is false then error number -128
	set dialogResult to item 1 of dialogResult
	
	if dialogResult is "Add a Destination" then
		set {destinationFolders, dialogResult} to addDestination(destinationFolders, destinationNames)
		writePlist("destinationFoldersKey", destinationFolders)
	else if dialogResult is "Delete a Destination" then
		set destinationFolders to deleteDestination(destinationFolders, destinationNames)
		writePlist("destinationFoldersKey", destinationFolders)
	else if dialogResult is "--" then
		error number -128
	else
		set destinationFolder to (destinationFolders's valueForKey:dialogResult)
		copyOne(destinationFolder, dialogResult)
	end if
	
	writePlist("dialogDefaultKey", dialogResult)
end main

on addDestination(destinationFolders, destinationNames)
	try
		tell application "Finder"
			set theFolder to selection
			if theFolder is {} then set theFolder to {target of front Finder window}
			if class of item 1 of theFolder is not folder then error
			set theFolder to item 1 of theFolder as alias
			set folderName to name of theFolder
		end tell
	on error
		writePlist("dialogDefaultKey", "Add a Destination")
		errorAlert("A folder in a Finder window must be selected")
	end try
	set theFolder to POSIX path of theFolder
	set dialogList to theFolder
	set p to (current application's NSString's stringWithString:theFolder)'s componentsSeparatedByString:"/"
	set theCount to p's |count|()
	if theCount is greater than 6 then set dialogList to current application's NSString's stringWithFormat_("/%@/%@/.../%@/%@/", p's objectAtIndex:1, p's objectAtIndex:2, p's objectAtIndex:(theCount - 3), p's objectAtIndex:(theCount - 2))
	set folderName to text returned of (display dialog "Enter name for " & quote & (dialogList as text) & quote default answer folderName buttons {"Cancel", "OK"} default button 2 cancel button 1 with title "File Copy")
	if folderName is "" then error number -128
	if (destinationNames's containsObject:folderName) is true then
		writePlist("dialogDefaultKey", "Add a Destination")
		errorAlert("A destination named " & quote & folderName & quote & " already exists")
	end if
	destinationFolders's setObject:theFolder forKey:folderName
	return {destinationFolders, folderName}
end addDestination

on deleteDestination(destinationFolders, destinationNames as list)
	if (count destinationNames) is 0 then errorAlert("There are no destinations to delete")
	set deleteItems to (choose from list destinationNames with title "File Destination" with prompt "Select destinations to delete:" default items (item 1 of destinationNames) with multiple selections allowed)
	if deleteItems is false then error number -128
	set deleteItems to current application's NSArray's arrayWithArray:deleteItems
	(destinationFolders's removeObjectsForKeys:deleteItems)
	return destinationFolders
end deleteDestination

on copyOne(theDestination, destinationName)
	tell application "Finder" to set selectedFiles to selection as alias list
	if selectedFiles is {} then errorAlert("File not selected or selected item cannot be copied")
	set selectedFiles to current application's NSArray's arrayWithArray:selectedFiles
	set targetfolder to current application's |NSURL|'s fileURLWithPath:theDestination
	set fileManager to current application's NSFileManager's defaultManager()
	set filesCopied to 0
	repeat with aFile in selectedFiles
		set copyResult to copyTwo(aFile, targetfolder, fileManager)
		if copyResult is true then set filesCopied to (filesCopied + 1)
	end repeat
	display notification "Destination: " & destinationName & linefeed & "Files copied: " & filesCopied with title "File Copy"
end copyOne

on copyTwo(sourceFile, targetfolder, fileManager)
	set sourceFileName to sourceFile's lastPathComponent()
	set targetFile to (targetfolder's URLByAppendingPathComponent:sourceFileName isDirectory:false)
	if (sourceFile's isEqual:targetFile) then errorAlert("The source and destination folders are the same")
	set {theResult, aDirectory} to (sourceFile's getResourceValue:(reference) forKey:(current application's NSURLIsDirectoryKey) |error|:(missing value))
	if aDirectory as boolean is true then
		set {theResult, aPackage} to (sourceFile's getResourceValue:(reference) forKey:(current application's NSURLIsPackageKey) |error|:(missing value))
		if aPackage as boolean is false then
			display alert "An error has occurred" message "The selected item " & quote & sourceFileName & quote & " is a folder and cannot be copied" buttons {"Cancel", "Skip"} cancel button 1 default button 2 as warning
			return false
		end if
	end if
	set fileExists to targetFile's checkResourceIsReachableAndReturnError:(missing value)
	if fileExists is true then
		display alert "An error has occurred" message "The destination already contains a file with the name " & quote & sourceFileName & quote buttons {"Cancel", "Replace", "Keep Both"} cancel button 1 default button 3 as warning
		if button returned of result is "Keep Both" then
			set targetFile to makeNewFile(targetFile)
		else
			fileManager's trashItemAtURL:targetFile resultingItemURL:(missing value) |error|:(missing value)
		end if
	end if
	set theResult to (fileManager's copyItemAtURL:sourceFile toURL:targetFile |error|:(missing value))
	if theResult is false then errorAlert("The file " & quote & sourceFileName & quote & " could not be copied. Please verify that the destination folder exists.")
	return true
end copyTwo

on makeNewFile(theFile)
	set fileFolder to theFile's URLByDeletingLastPathComponent
	set fileName to (theFile's URLByDeletingPathExtension())'s lastPathComponent()
	set fileExtension to theFile's pathExtension()
	repeat with i from 1 to 100
		set newFileName to (fileName's stringByAppendingString:(" (copy " & i & ")"))
		set newFile to (fileFolder's URLByAppendingPathComponent:newFileName isDirectory:false)
		set newFile to (newFile's URLByAppendingPathExtension:fileExtension)
		set fileExists to (newFile's checkResourceIsReachableAndReturnError:(missing value))
		if fileExists is false then return newFile
	end repeat
end makeNewFile

on readPlist(theKey)
	set theDefaults to current application's NSUserDefaults's alloc()'s initWithSuiteName:"com.peavine.FinderFileCopy"
	return theDefaults's objectForKey:theKey
end readPlist

on writePlist(theKey, theValue)
	set theDefaults to current application's NSUserDefaults's alloc()'s initWithSuiteName:"com.peavine.FinderFileCopy"
	theDefaults's setObject:theValue forKey:theKey
end writePlist

on errorAlert(dialogMessage)
	display alert "An error has occurred" message dialogMessage as critical
	error number -128
end errorAlert

main()