BackupMeta (Backup and restore Opus metadata)

BackupMeta creates recovery files that let you restore Opus-specific metadata. Currently supported metadata are labels and user comments. This metadata is stored in the NTFS filesystem and might get deleted by other software or when files are moved to drives that don't support this type of metadata. Examples are non-NTFS drives, cloud storage, and archives except WinRAR.

The recovery file contains the Opus commands Properties and SetAttr that will restore the metadata. It should be self-explanatory and can easily be mass-edited to reflect e.g. changes in the drive/folder structure.

User comments may contain line breaks. Opus can read these multi-line comments, but cannot write them. For these files, the recovery file contains ExifTool commands.

How to set up and use

:one: Save CommandBackupMeta.js.txt to

%appdata%\GPSoftware\Directory Opus\Script AddIns

Add the new command to a button, hotkey, context menu, etc. like any built-in command.

Optional: Get ExifTool and install it on your system. It's not needed to create the backup file.

:two: Select the files and folders for which you want to create a recovery file, and run BackupMeta on them. Folders will be read recursively. The dated recovery file will show up in /profile\OpusBackup. Make the necessary changes with a text editor. Depending on your system, you might need to adjust the path to exiftool.exe.

:three: To restore metadata, copy the commands from the recovery file to an Opus button and run them from there.

Things you might enjoy reading

How to use buttons and scripts from this forum
What are aliases?

The script's inner workings

JScript
function OnInit(initData) {
    initData.name = 'BackupMeta';
    initData.version = '2023-08-18';
    initData.url = 'https://resource.dopus.com/t/backupmeta-backup-and-restore-opus-metadata/45497';
    initData.desc = 'Write recovery commands for some metadata to a text file.';
    initData.default_enable = true;
    initData.min_version = '12.0';
}

function OnAddCommands(addCmdData) {
    var cmd = addCmdData.AddCommand();
    cmd.name = 'BackupMeta';
    cmd.method = 'OnBackupMeta';
    cmd.desc = 'Write recovery commands for some metadata to a text file.';
    cmd.label = 'BackupMeta';
    cmd.template = '';
    cmd.hide = false;
    cmd.icon = 'script';
}

function OnBackupMeta(scriptCmdData) {
    var cmd = scriptCmdData.func.command;
    var tab = scriptCmdData.func.sourcetab;
    var fsu = DOpus.FSUtil();

    cmd.deselect = false;

    if (tab.selected.count == 0) return;

    var progDlg = DOpus.Create().Command().progress;

    progDlg.abort = true;
    progDlg.delay = false;
    progDlg.Init(tab.lister);
    progDlg.SetTitle('BackupMeta');
    progDlg.SetStatus('Enumerating selection...');
    progDlg.SetFiles(tab.selected.count);
    progDlg.Show();

    var vec = DOpus.Create().Vector(tab.selected_files);

    for (var e = new Enumerator(tab.selected_dirs); !e.atEnd(); e.moveNext()) {
        var item = e.item();
        vec.push_back(item);
        progDlg.StepFiles(1);
        progDlg.SetName(item);
        var folderEnum = fsu.ReadDir(item, 'r');
        while (!folderEnum.complete) {
            if (progDlg.GetAbortState() == 'a') return;
            var folderItem = folderEnum.Next();
            vec.push_back(folderItem);
            if (!folderItem.is_dir) continue;
            progDlg.SetName(folderItem);
        }
        folderEnum.Close();
    }

    if (vec.empty) return;

    progDlg.Restart();
    progDlg.SetStatus('Generating commands for ' + vec.count + ' files...');
    progDlg.SetFiles(vec.count);

    var bakFileItem = fsu.GetItem(fsu.Resolve('/profile\\OpusBackup\\Recovery-' + DOpus.Create().Date().Format('D#yyyyMMdd-T#HHmmss') + '.txt'));
    cmd.RunCommand('CreateFolder NAME="' + bakFileItem.path + '"');
    var bakFile = bakFileItem.Open('wa'); // create a new file, always. If the file already exists it will be overwritten. 

    for (var e = new Enumerator(vec); !e.atEnd(); e.moveNext()) {
        if (progDlg.GetAbortState() == 'a') break;

        var item = e.item();
        progDlg.StepFiles(1);
        progDlg.SetName(item);

        if (item.metadata == 'none') continue;

        var itemLabels = item.Labels();

        if (itemLabels.count > 0) {
            var strLabels = '';

            for (var ee = new Enumerator(itemLabels); !ee.atEnd(); ee.moveNext()) {
                strLabels += ee.item() + ',';
            }

            var cmdLine = 'Properties' +
                ' FILE="' + item + '"' +
                ' SETLABEL="' + strLabels.slice(0, -1) + '"';
            bakFile.Write(cmdLine + '\r\n');
        }

        var userComment = item.metadata.other.usercomment;

        if (typeof userComment != 'undefined') {
            if (userComment.indexOf('\r') < 0 && userComment.indexOf('\n') < 0) {
                userComment = userComment.replace(/"/g, '""');
                var cmdLine = 'SetAttr' +
                    ' FILE="' + item + '"' +
                    ' META' +
                    ' "usercomment:' + userComment + '"';
            } else {
                userComment = userComment.replace(/\r/g, '\\r');
                userComment = userComment.replace(/\n/g, '\\n');
                userComment = userComment.replace(/"/g, '\\"');
                var cmdLine = 'exiftool.exe' +
                    ' -escapeC' +
                    ' -UserComment="' + userComment + '"' +
                    ' "' + item + '"';
            }
            bakFile.Write(cmdLine + '\r\n');
        }
    }

    bakFile.Close();

    cmd.RunCommand('Go' +
        ' PATH="' + bakFileItem + '"' +
        ' NEWTAB=findexisting' +
        ' OPENINDUAL');
}
4 Likes

Hello lxp,

following your humble pointer to this thread, did not recognize yet, sorry! o)

It's an interesting approach, but I think it does not fit my backup needs so well. I wrote a powershell script some time ago, which would just copy all the ADS from source to destination files. It was kind of slow and had some drawbacks I currently cannot remember anymore, but this is more of what I am looking for.

Your approach also does not take into account ADS created by other applications and restoring would take quite a while using DO commands and exiftool.exe if you have to go over a larger file base (I guess). o)

But nonetheless, it's good to see people dealing with the same problem! o) There will be a proper solution one day. I will look into this myself at some point again, using C# or something which is not as quirky as powershell. In theory, it's not that hard to copy/sync ADS, these are just files attached to files, not much different than a regular file copy.

The difficulty is detecting whether you need to update the ADS in the backup location and making sure you hit the same files and folders, which the regular backup synced beforehand. You always have filetypes / folders excluded and maybe junctions in between when doing the regular backup. If the ADS sync does not hit the very same items as the regular backup before, it's kind of pointless again. That's why I'm also looking out for a backup tool, which does ADS sync while doing the regular file sync - in one step, making sure each file will be treated.

Thanks! o)

I think you could do it easily with AutoHotkey, maybe using the multi-threading version AutoHotkey_H.

Wha! AutoHotkey? o) I think AH is definitely not the right tool for the job. You need something which gets as close to the official filesystem APIs and better use a language / tool that was done by the filesystem issuer (Microsoft).

To me AutoHotkey seems a capable swiss knife or system hack, but unfortunately it also has the worst language and syntax I have seen so far. I often tried to get some simple things done with it and it's just a try and error pita! o) In AH v3 they address some issues, but it still is very confusing if you are used to "regular" languages of some kind.

We better stop spamming lxps thread now?! o) Sorry lxp! o)