'Fetch' command: Find and copy related files

Overview

Install this script to get a new command: Fetch

The Fetch command is given a list of folders to search. For each selected file, it will look in the specified folders for any files with the same name, ignoring their file extensions. If a matching file is found, it will be copied so it is next to the selected file.

For example, if cat.jpg is selected, and cat.nef exists somewhere in the other folders, then that NEF file will be copied so it is next to the JPG.

The idea for this came from this thread: Find corresponding .nef (camera raw file) file for jpg

You can also specify a pattern which the found files have to match. For example, you may only want to fetch *.nef files and avoid copying any JPG, BMP, XMP, etc. files at the same time.

Note that, if the specified pattern matches its extension, the matching file may have the same name and extension as the one you had selected, in which case the selected file will be overwritten (after prompting you, unless Opus is configured not to). So you can use this to replace edited files with 'original' versions, as well as to retrieve companion files. (Of course, if you want to avoid that, change the pattern so it won't match the types of files you are selecting.)

By default, files will be deselected if a matching file is found successfully, and left selected if no matching file could be found.

Video example

(The video is silent.)

The video shows an example, using the following command for the Fetch! button on the top toolbar:

Fetch PATTERN="*.(nef|raw)" FROM "F:\Temp\Demo Raw 1" "T:\Temp\Demo Raw 2" 

The left window shows the two folders which the command is configured to search, using Flat View (Grouped) so you can see everything below the two starting points. The left window does not need to be open for the command to work; it's just there to help you see where the files are coming from.

The right window is where the button is being used. When a file is selected on the right and the Fetch! button is clicked, the folders are searched and matching NEF or RAW files are copied next to the selected file.

At the end, two files are left selected as there are no matching files for them.

History

v1.0 (20-Sep-2017):

  • Initial version.

Download

(See How to use Buttons and Scripts from this Forum for more detailed instructions on how to add script add-ins and .dcf buttons to your setup.)

First, you need to install the script add-in:

  • Download: Fetch.js.txt (5.5 KB)
  • Select Settings > Preferences.
  • Go to Toolbars / Scripts in the tree on the left of the Preferences window.
  • Drag the .js.txt file to the list of scripts.

Once the script is installed, a new Fetch command will be available to you. You will need to create a button to use it.

If you like, you can use this button as a starting point:

  • Download: Fetch!.dcf (335 Bytes)
  • Select Settings > Customize Toolbars.
  • Drag the .dcf file on to your toolbar, to where you want it.
  • Click OK in the Customize dialog.

That button runs the same command as in the example video. You will need to edit it to change the paths it searches, and maybe the file extensions it looks for, or other details.

Fetch Command Arguments

The Fetch command takes the following arguments:

FIRSTONLY (Optional, /S: switch)

  • By default, if multiple files match one of the selected files, they will all be copied over (as long as they also match the PATTERN argument; see below). If two matched files have the same name, that can result in one file overwriting the other (after you are prompted, if Opus is configured to do so).

    If you add FIRSTONLY to the command-line, only the first file which is found will be copied. (If you know there will only be one file, this can speed things up as the command can stop looking sooner.)

FROM (Required, /K/M: keyword, multiple-value)

  • You must use the FROM argument to specify the folder(s) to be searched. You can specify one or more folders.

MOVE (Optional, /S: switch)

  • The files will be moved, instead of being copied, if MOVE is included on the command-line.

NODESELECT (Optional, /S: switch)

  • Normally, a selected file will be deselected if at least one matching file is found. Files that don't have matches will be left selected.

    If you add NODESELECT to the command-line, all of the selected files will always be left selected.

NORECURSE (Optional, /S: switch)

  • Normally, the folders passed to the FROM argument will be searched recursively. In other words, the script will look in the folders, and in their sub-folders, and their sub-folders, and so on.

    If you only want the script to look for files directly below each folder, without entering any sub-folders, then you should add NORECURSE to the command-line.

PATTERN (Optional, /K: keyword)

  • Use this to specify a wildcard which restricts the files that will be copied.

    For example, PATTERN="*.bmp" will mean only files with the bmp extension will be considered.

    PATTERN="*.(bmp|gif)" will mean only files with the bmp or gif extensions will be considered.

    If the PATTERN argument is not used, all files will be considered, the same as if you had specified PATTERN=*

    For a file to be copied, it must also have the same base name as one of the selected files. In other words, the part of the name that comes before the extension must be the same as one of the selected files. For example, if you use *.bmp as the pattern and cat.jpg is selected, it means cat.bmp will be copied over if it is found, but cat.png and dog.bmp would not be copied.

More Example Command-Lines

Search in and below C:\My Backups for .BAK files, without deselecting anything, and copy the results:

Fetch PATTERN="*.bak" NODESELECT FROM "C:\My Backups"

Search in, but not below, C:\Folder 1, C:\Folder 2, and C:\Folder 3 for .NEF and .RAW files, and move the results:

Fetch PATTERN="*.(nef|raw)" NORECURSE MOVE FROM "C:\My Folder 1" "C:\My Folder 2" "C:\My Folder 3"

Script Code

If you just want to use this, see above for the downloads.

The script code is reproduced here to help people browsing the forum for scripting techniques.

// Fetch
// (c) 2017 Leo Davidson

// This is a script for Directory Opus.
// See http://www.gpsoft.com.au/DScripts/redirect.asp?page=scripts for development information.

// Called by Directory Opus to initialize the script
function OnInit(initData)
{
	initData.name = "Fetch";
	initData.version = "1.0";
	initData.copyright = "(c) 2017 Leo Davidson";
	initData.url = "https://resource.dopus.com/t/fetch-command-find-and-copy-related-files/26918";
	initData.desc = "A 'Fetch' command which locates and copies matching files.";
	initData.default_enable = true;
	initData.min_version = "12.6";

	var cmd = initData.AddCommand();
	cmd.name = "Fetch";
	cmd.method = "OnFetch";
	cmd.desc = "Find and copy matching files.";
	cmd.label = "Fetch";
	cmd.template = "PATTERN/K,MOVE/S,FIRSTONLY/S,NODESELECT/S,NORECURSE/S,FROM/K/M";
	cmd.hide = false;
	cmd.icon = "dupepane";
}

// Implement the Fetch command
function OnFetch(scriptCmdData)
{
	var COMMAND_FAILED = true;
	var COMMAND_OK = false;

	var args = scriptCmdData.func.args;
	var searchPattern = args.pattern;
	var moveFiles = args.move;
	var firstMatchOnly = args.firstonly;
	var deselectMatches = !args.nodeselect;
	var recursive = !args.norecurse;
	var vecPathsToSearch = args.from;
	
	if (!vecPathsToSearch || vecPathsToSearch.empty)
	{
		// Tell caller we failed/aborted, due to invalid arguments.
		return COMMAND_FAILED;
	}
	
	var cmd = scriptCmdData.func.command;

	// Prevent automatic deselection.
	// We will explicitly deselect the files that succeed and leave the rest selected.
	cmd.deselect = false;

	var cmdToDeselect = null;
	if (deselectMatches)
	{
		cmdToDeselect = DOpus.Create.Command();
		cmdToDeselect.SetSourceTab(cmd.sourceTab);
	}

	var searchWild = null;
	if (searchPattern)
	{
		searchWild = DOpus.FSUtil.NewWild(searchPattern);
	}

	// Make a list of things we want to find matches for.
	var mapNamesToFind = DOpus.Create.Map();
	makeNameMap(scriptCmdData.func.sourcetab.selected_files, mapNamesToFind);

	// Search the specified folders for any matches.
	var mapDestToVecSources = DOpus.Create.Map();
	for (var eSearch = new Enumerator(vecPathsToSearch); !eSearch.atEnd(); eSearch.moveNext())
	{
		var searchPath = eSearch.item();
		searchFolder(searchPath, searchWild, mapNamesToFind, mapDestToVecSources, cmdToDeselect, firstMatchOnly, recursive)
	}

	// If we found nothing, consider it a failure.
	if (mapDestToVecSources.empty)
	{
		return COMMAND_FAILED;
	}

	// Copy everything we want to copy.
	for (var eDest = new Enumerator(mapDestToVecSources); !eDest.atEnd(); eDest.moveNext())
	{
		var destPath = eDest.item();
		var vecFilesToCopy = mapDestToVecSources(destPath);
		if (!copyFiles(cmd, destPath, vecFilesToCopy, moveFiles))
		{
			// Tell caller we failed/aborted, because a copy command failed or was aborted.
			return COMMAND_FAILED;
		}
	}

	// Anything to deselect? Do it.
	if (cmdToDeselect && cmdToDeselect.files && cmdToDeselect.files.count > 0)
	{
		cmdToDeselect.RunCommand("Select DESELECT FROMSCRIPT");
	}
	
	return COMMAND_OK;
}

function makeNameMap(sel_files, mapNamesToFind)
{
	for (var eSel = new Enumerator(sel_files); !eSel.atEnd(); eSel.moveNext())
	{
		var fileItem = eSel.item();
		var nameNoExtLower = fileItem.name_stem_m.toLowerCase();
		var parentPath = fileItem.path;

		if (!mapNamesToFind.exists(nameNoExtLower))
		{
			mapNamesToFind(nameNoExtLower) = DOpus.Create.Vector();
		}
		// Record that we are looking for things like nameNoExtLower.
		mapNamesToFind(nameNoExtLower).push_back(fileItem);
	}
}

function searchFolder(searchPath, searchWild, mapNamesToFind, mapDestToVecSources, cmdToDeselect, firstMatchOnly, recursive)
{
	if (mapNamesToFind.empty)
	{
		return; // Avoid listing the folder if there is nothing left to find.
	}

	var searchEnum = DOpus.FSUtil.ReadDir(searchPath, recursive);
	while (!searchEnum.complete)
	{
		var searchItem = searchEnum.Next;
		if (!searchItem.is_dir)
		{
			if (searchWild && !searchWild.Match(searchItem.name)) { continue; }

			var nameNoExtLower = searchItem.name_stem_m.toLowerCase();
			if (!mapNamesToFind.exists(nameNoExtLower)) { continue; }

			for (var eSelFile = new Enumerator(mapNamesToFind(nameNoExtLower)); !eSelFile.atEnd(); eSelFile.moveNext())
			{
				var selFile = eSelFile.item();
				var destPath = DOpus.FSUtil.NewPath(selFile.realpath);
				destPath.Parent();
				if (!mapDestToVecSources.exists(destPath))
				{
					mapDestToVecSources(destPath) = DOpus.Create.Vector();
				}

				// Record that we want to copy searchItem to destPath.
				mapDestToVecSources(destPath).push_back(searchItem);

				if (cmdToDeselect)
				{
					// Record that we found at least one file for the selected item.
					// Note: This will add the same file to cmdToDeselect multiple times if there are
					//   multiple matches for it (implies firstMatchOnly is false). That's OK as
					//   it is a list of files to deselect. Deselecting the same file twice is harmless.
					cmdToDeselect.AddFile(selFile);
				}
			}

			if (firstMatchOnly)
			{
				// Stop looking for nameNoExtLower if we only want the first match.
				mapNamesToFind.erase(nameNoExtLower);
				if (mapNamesToFind.empty)
				{
					return;
				}
			}
		}
	}
}

function copyFiles(cmd, destPath, vecFilesToCopy, moveFiles)
{
	cmd.ClearFiles();
	cmd.AddFiles(vecFilesToCopy);
	cmd.RunCommand("Copy " + (moveFiles ? "MOVE " : "") + "QUEUE=none AUTOSELECT=no FLATVIEWCOPY=single TO=\"" + destPath + "\"");
	return (cmd.Results.result != 0);
}
4 Likes