Create '.bak' Backups for Selected Files

Description / Features:

  • Command script that creates '.bak' backups for any number of selected files.
    • Can be used directly on a button or as User Command with optional arguments.
  • If a .bak already exists for a file, it will create .bak2, .bak3 and so on.
  • Works even if each selected file has a different number of .baks already
  • Allows changing .bak extension to anything
  • Also has a 'restore' mode which will use the selected backup file to replace the non-bak file.

Example Result:
image

Variable Options
backupExtension - For setting the base extension for the backup files
doRestore - If set to true (or if using command argument below), it will restore based on the selected file instead of making a backup.

Optional Command Arguments For Above Variables:

  • BACKUP_EXTENSION (String value)
  • RESTORE (Switch argument, activates if included, no value needed)

How Restore Mode Works:

  • The script will take the selected .bak file (or whatever set extension), find the corresponding file name without the .bak extension and delete it, then replace it with the selected .bak file.
  • The selected backup file must have a base extension matching backupExtension or its argument.
  • The replaced/deleted file will go into the recycle bin, assuming you have the option in the Opus preferences for "Delete to recycle bin where possible" enabled (which I believe is default anyway).

User Command Arguments template:
RESTORE/O/S,BACKUP_EXTENSION/O

Example Button Usage: The below example is what I have assigned to a button for the script. It uses restore mode while clicked when the shift key is held down. It also changes the button label based on whether the shift key is held down (The label being ".bak" by default and ".bak (Restore)" if it is held down). I have the script set to a user command called "Make_bak" but the name doesn't matter.

@label:KeyDown("shift") ? ".bak (Restore)" : ".bak"
@keydown:shift
Make_bak RESTORE BACKUP_EXTENSION=".bak"
@keydown:none
Make_bak BACKUP_EXTENSION=".bak"

How to Install (Easy Way):

  1. Download the User Command .dcf file here:
    Make_bak.dcf (14.9 KB)
  2. Open the Customize Menu > "User Commands" Tab
  3. Drag the file into the list of user commands. Or use the 'Import' button to import the file.

How to Install Manually:
Copy the script below to a new User Command via the Command Editor window. Be sure to set it as Script Function using JScript, and to set the Argument Template.


The Script:

// Button / User Command script that creates '.bak' backups for any number of selected files or folders. If a .bak already exists for an item, it will create .bak2, .bak3 and so on. Also has an argument option to restore a file.
// By ThioJoe
// Updated: 7/2/24

//    Argument Template:    
//    RESTORE/O/S,BACKUP_EXTENSION/O

//    Example usages of arguments:
//       Make_bak BACKUP_EXTENSION=".bak"
//       Make_bak RESTORE
//       Make_bak BACKUP_EXTENSION=".backup" RESTORE

function OnClick(clickData) {
    // You can change the backup extension base string to be whatever you want here. Must include period.
    // Default = '.bak'
    // >  Optional Argument Name: BACKUP_EXTENSION (string value)
    var backupExtension = '.bak';
    ////////////////////////////////////////////////////////////////////////
    
    // With this set to true (or if argument is used), the highest numbered .bak for the selected file will replace the main file
    // Note: Selected backup file to restore from must match the base backupExtension variable. (It's ok if it's numbered, for example if backupExtension is '.bak' you can still restore a '.bak4' file.
    // >  Optional Argument Name: RESTORE (Switch, no value needed)
    var doRestore = false;

    // -----------------------------------------------------------------------

    // Parse optional arguments if they're there
    if (clickData.func.args.got_arg.RESTORE) {
        doRestore = true;
        //DOpus.Output("Received RESTORE switch argument");
    }
    
    if (clickData.func.args.got_arg.BACKUP_EXTENSION) {
        //Validate argument value
        argString = String(clickData.func.args.BACKUP_EXTENSION);
        if (argString.charAt(0) == ".") {
            backupExtension = argString;
        } else {
            backupExtension = "." + argString;
            DOpus.Output("WARNING: BACKUP_EXTENSION argument did not include a period so one was added. Got argument: " + argString);
        }
        //DOpus.Output("Received BACKUP_EXTENSION argument: " + String(clickData.func.args.BACKUP_EXTENSION));
    }
    

    function createBak(item) {
        // Create item object of selected file or folder
        var selectedItem = item;
        // Get name of selected file or folder
        //var selectedItemExt = String(selectedItem.ext);
        //var selectedItemNameStem = String(selectedItem.name_stem);

        //DOpus.Output("Processing: " + selectedItem.name);
        var lastBakNum = getLastBakNum(item);
        //DOpus.Output("LastBakNum: " + lastBakNum);

        // If there is no already existing .bak or .bak# of the selected item, create them
        var commandString;
        if (lastBakNum == 0) {
            commandString = 'Copy DUPLICATE "' + selectedItem + '" AS *' + backupExtension;
            //DOpus.Output("Running command: " + commandString);
            clickData.func.command.RunCommand(commandString);
        } else {
            var newBakNum = lastBakNum + 1;
            commandString = 'Copy DUPLICATE "' + selectedItem + '" AS *' + backupExtension + newBakNum;
            //DOpus.Output("Running command: " + commandString);
            clickData.func.command.RunCommand(commandString);
        }
    }
    
    function restoreBak(item) {
        // Create a FileSystemObject
        var fso = new ActiveXObject("Scripting.FileSystemObject");
        
        // Get the full name of the selected .bak file
        var selectedBakFullName = String(item.name);
        
        // Determine the base name of the original file by removing the .bak or .bak# extension
        var originalFileName;
        var baseNameRegex = new RegExp('^(.+)' + backupExtension.replace('.', '\\.') + '(\\d*));
        //DOpus.Output("baseNameRegex:  " + baseNameRegex);
        var match = selectedBakFullName.match(baseNameRegex);
        if (match) {
            originalFileName = match[1];
        } else {
            // Show error dialogue if the selected file is not a valid .bak file
            DOpus.Dlg.Request("Error: The selected file (" + selectedBakFullName + ") does appear to match the selected backup extension: " + backupExtension, "OK");
            return;
        }
        
        // Determine the paths for the selected .bak file and the original file
        var bakFilePath = String(clickData.func.sourcetab.path) + '\\' + selectedBakFullName;
        var originalFilePath = String(clickData.func.sourcetab.path) + '\\' + originalFileName;
        if (fso.FileExists(bakFilePath)) {
            if (fso.FileExists(originalFilePath)) {
                // Delete the original file if it exists
                var commandString = 'Delete QUIET "' + originalFilePath + '"';
                //DOpus.Output("Running command: " + commandString);
                clickData.func.command.RunCommand(commandString);
            }
            
            // Rename the .bak file to the original file name
            commandString = 'Copy DUPLICATE "' + bakFilePath + '" AS "' + originalFileName + '"';
            //DOpus.Output("Running command: " + commandString);
            clickData.func.command.RunCommand(commandString);
        } else {
            DOpus.Output("Backup file does not exist: " + bakFilePath);
        }
    }
    
    function getLastBakNum (item) {
        // Create a FileSystemObject
        var lastBakNum = 0;
        var selectedItemFullName = String(item.name);
        
        var fso = new ActiveXObject("Scripting.FileSystemObject");
        // Get the parent folder of the selected item
        var parentFolder = fso.GetFolder(clickData.func.sourcetab.path);
        var files = new Enumerator(parentFolder.Files);
        var subfolders = new Enumerator(parentFolder.SubFolders);

        // Combine files and folders into a single array
        var items = [];
        while (!files.atEnd()) {
            items.push(files.item());
            files.moveNext();
        }
        while (!subfolders.atEnd()) {
            items.push(subfolders.item());
            subfolders.moveNext();
        }

        // Go through filenames in folder, if any contains itemFullName.bak, check if # at end is larger than current, if so record into lastBakNum
        for (var i = 0; i < items.length; i++) {
            var currentItem = items[i];
            var currentItemName = String(currentItem.Name);

            //DOpus.Output("Checking existing item: " + currentItemName);

            // Checks if stem of currently scanned item is same as selected item with .bak added
            var theoreticalBakName = selectedItemFullName + backupExtension;
            var theoreticalBakNameLength = theoreticalBakName.length;

            // Checking if the currently scanned item is already a .bak item of the selected item or folder
            // By checking if scanned item contains selected item name + bak, from beginning
            if (currentItemName.substr(0, theoreticalBakNameLength) == theoreticalBakName) {
                //DOpus.Output("Found backup match: " + currentItemName);

                // Checks if extension is .bak or .bak*
                if (currentItemName.length == theoreticalBakNameLength) {
                    if (lastBakNum == 0) {
                        lastBakNum = 1;
                    }
                    //DOpus.Output("Setting lastBakNum to 1");
                } else {
                    // Gets text or number after ".bak", which should be a number
                    var extEnding = currentItemName.substr(theoreticalBakNameLength);
                    // Checks if anything after .bak is not a number
                    if (!isNaN(extEnding)) {
                        // Parse the ending number into an integer in base 10
                        var extEndingNum = parseInt(extEnding, 10);
                        // Only update lastBakNum if it is the largest .bak # found so far
                        if (extEndingNum > lastBakNum) {
                            lastBakNum = extEndingNum;
                            //DOpus.Output("Updating lastBakNum to: " + lastBakNum);
                        }
                    }
                }
            }
        }
        return lastBakNum;
    }

    // Get data about selected items
    var allSelectedItems = clickData.func.sourcetab.selected;
    var enumSelectedItems = new Enumerator(allSelectedItems);

    // Runs the main function for each selected item
    enumSelectedItems.moveFirst();
    while (!enumSelectedItems.atEnd()) {
        var currentItem = enumSelectedItems.item();
        
        // Whether to restore files or create backup based on argument switch
        if (doRestore === true) {
            restoreBak(currentItem);
        } else {
            createBak(currentItem);
        }
        enumSelectedItems.moveNext();
    }
}


Changes:

  • 5/21/24: Updated to now work with folders. Also it should work with multiple panes open.
  • 7/2/24: Added restore functionality. Added optional command arguments for backup extension and restore mode.
11 Likes

The .bak file should be in the same folder as the original file is.

If you are okay with the counter being added to the filename, you could use

@nodeselect
Copy DUPLICATE AS=*.bak WHENEXISTS=rename
2 Likes

The .bak file should be in the same folder as the original file is.

It should already work like that, do you mean the files went somewhere else for you?

It's best to avoid HERE in scripts.

Instead, use TO in the Copy command line or set the destination in the command object via SetDest before running the line.

https://www.gpsoft.com.au/help/opus12/index.html#!Documents/Scripting/Command.htm

In this specific case, you can also use the DUPLICATE argument.

https://www.gpsoft.com.au/help/opus12/index.html#!Documents/Copy.htm

is this code supposed to works even with folders? after a first copy it stops (it create a 'copy of this' but not a 'copy (1) of this')

It will create one bak folder and then keep adding duplicates to that folder - probably not what you want.

To duplicate folders, you could use:

@nodeselect
Copy DUPLICATE AS=*-{date|yyyyMMdd}-{time|HHmmss}

or, if you prefer a counter:

@nodeselect
@nofilenamequoting
@dirsonly
@firstfileonly
FileType NEW=directory NEWNAME="norename:{file}"
Copy DUPLICATE AS="{$newfile}"

The second button will only work for one folder at a time!

4 Likes

great!!, the first solution is simply perfect for my needs, many thanks

I love the .bak script. But I'd like to see it modified so that .bak* files get created in or moved to a designated backup folder so that you don't have to move them after the fact.

If you want to add the date and time to the backup filename, (e.g., MyFile.xlsm_10-10-2022_06-02-53.bak), add {date|MM-dd-yyyy}_{time|HH-mm-ss}. Change this:

if (lastBakNum == 0) {
			commandString = 'Copy "' + selectedFile + '" AS *' + backupExtension + ' HERE'
			clickData.func.command.RunCommand(commandString);
		}
		else {
			newBakNum = lastBakNum + 1;
			commandString = 'Copy "' + selectedFile + '" AS *' + backupExtension + newBakNum + ' HERE';
			clickData.func.command.RunCommand(commandString);
		}

to this:

		if (lastBakNum == 0) {
			commandString = 'Copy DUPLICATE "' + selectedFile + '" AS *_{date|MM-dd-yyyy}_{time|HH-mm-ss}' + backupExtension + ' HERE';
			clickData.func.command.RunCommand(commandString);
		}
		else {
			newBakNum = lastBakNum + 1;
			commandString = 'Copy DUPLICATE "' + selectedFile + '" AS *_{date|MM-dd-yyyy}_{time|HH-mm-ss}' + backupExtension + newBakNum + ' HERE';
			clickData.func.command.RunCommand(commandString);
		}
2 Likes

I tried the script, but the backup file is created in the destination pane. Should this not be avoided by the "here" argument?
(Is that the reason to avoid "here" in scripts?)

Try what lxp wrote above:

Yes, then it works

		// If there is no already existing .bak or .bak# of the selected file, create them
		if (lastBakNum == 0) {
			clickData.func.command.SetDestTab(clickData.func.sourcetab);
			commandString = 'Copy "' + selectedFile + '" AS *' + backupExtension;
			clickData.func.command.RunCommand(commandString);
		}
		else {
			newBakNum = lastBakNum + 1;
			clickData.func.command.SetDestTab(clickData.func.sourcetab);
			commandString = 'Copy "' + selectedFile + '" AS *' + backupExtension + newBakNum;
			clickData.func.command.RunCommand(commandString);
		}

But according to this topic "Copy MOVE HERE CREATEFOLDER" doesn't create folder in source? - #9 by Leo the here argument should work, right?

What do you mean by "the backup file is created in the destination pane?" Using HERE, The backup file should be created in the directory that the file you're backing up resides. Where is your backup being created?

I use dual display mode. It was created "on the other side". When I used the script for a file on the left side the bak was created on the right side and vice versa (in the current visible tab).

Ok, I deleted the word HERE from my script, and the backup still gets created, and in the correct location, with a dual window open. This is the script without the HERE:

commandString = 'Copy DUPLICATE "' + selectedFile + '" AS *{date|MM-dd-yyyy}{time|HH-mm-ss}' + backupExtension;

So, I guess you don't even need the HERE, and your problem is something else.

I see your script doesn't have DUPLICATE in it. Try making it Copy DUPLICATE.

Yes - in the version without DUPLICATE the "here" has no function (any longer). But you have to define the sourcetab as destination tab like in my code snippet above.

Or you have to use "Copy DUPLICATE" - then no "HERE" or "setDestTab" is necessary.

But is it intended, that the "HERE" has no function any more? I assume it worked in the past.

EDIT: If the selected item is a folder nothing happens.

I've just updated the script with a new optional 'restore' functionality, as well as adding support for arguments if the script is set as a user command. I've updated the original post at the top to describe how it all works.

Also if you're using the very original version of the script, the latest version now supports backing up folders, not just files. Also it uses a better copy technique/command so it should work with multiple panes.

3 Likes

With the new script nothing happens when I click the button. Neither if i simply copy the script directly as button command or via user command + button script like shown in the first post.

Any idea?

Huh seems like the forum interpreted the dollar sign in the script's code block (in the regex pattern) as an escape character and got rid of it which broke the script.

Not sure how that happened but I changed the code block format to jscript and now it seems to be fine, so try copying it again now, it should work.