OpusCustomFields : Store custom metadata for virtually any item

OpusCustomFields lets you define and set custom fields accessible in Directory Opus for virtually any file, with support for a variety of data types. It's the first read/write script add-in for Directory Opus.

BEFORE CONTINUING, please keep the following in mind:

  • This is a beta version and may contain bugs.
  • The whole point of this is for you to test it in real-world use and report any bugs and suggestions.

Features

  • Almost any item can have custom fields, including FTP, MTP, items inside archives, etc.
  • Blazing fast!
  • You can create fields of any data type supported by Opus. It also supports multivalue data types, with special support for searching them (arrays and sets).
  • Manage custom fields through an easy-to-use interface.
  • Support for exporting/importing custom fields.
  • Support for advanced actions: PULL, PUSH, FLUSH, etc.
  • Seamless data management using an SQLite database, with commands for database maintenance.
  • Support for directly searching the database, including an advanced mode using SQL SELECT syntax.

And more!

config

Requirements

  • Directory Opus 13.25.4 or higher.
  • Windows 10 1803 or higher (since the script uses Unix sockets for faster communication with the helper).

Installation

Download the installer from here

Then run the installer. This will put the helper in place and launch the standard script installer.

This script uses a Go executable as a helper, which is why it requires a special installer.


How it works

The script stores all its data in a database in its configuration folder. It communicates with the database transparently through a helper to manage the data with zero delay.

Data is stored unencrypted, so avoid storing sensitive information in it.

To make it easier to manage and locate registered files, their paths are stored using the disk's volume name instead of its drive letter, where applicable. This is particularly useful for files on removable or USB drives, which tend to change drive letters.

This script cannot track files. If a file is moved, renamed, or deleted, its data may be lost.

To work around this limitation, the script uses several methods:

  1. For files on NTFS drives, it can back up data to an Alternate Data Stream (ADS). If the file is moved or renamed, the next time you try to retrieve its data, the script can recover it from this source and automatically update the database. (Optional; must be enabled in Options.)
  2. For files on NTFS drives, it also stores each item's unique file identifier, allowing an item to be relocated after being moved, provided its identifier hasn't changed.
  3. For all files, the script can retrieve data from a sidecar file stored alongside the original file, using the same filename plus the .dopcf extension. (Optional; must be enabled in Options.)

Automatic recovery methods are only used when the file in question has no existing entry in the database.

If ADS support is enabled, the ADS is automatically updated after every change, so manual editing is not recommended.

Sidecar files are never generated automatically, but they can be generated manually using the PULL command.

For the ADS workaround to work, you must set Copy Alternate Data Streams to Copy to new file under Preferences / File Operations / Copying Files / Metadata (Copying Files).


Configuring fields

The first time you install the script, you'll be asked if you want to start configuring fields.

To configure which fields are enabled, open the configuration dialog in one of the following ways:

  • From the Script Management window, click the Configure button.
  • Run the OpusCustomFields CONFIG command.

config

Checked fields will be enabled as columns in Opus once you save your changes and close the dialog.

  • The Category control lets you choose where the column appears. By default, columns are added under Script > OpusCustomFields. You can change this to any category you prefer. This doesn't affect the column's behavior or output.
  • Blur in secure screenshot lets you specify which columns will be blurred when Secure Screenshot is active.
  • You can export or import columns from this window using the provided buttons.
  • You can also set a short description for each field (max. 255 characters).

Press the Add button to create a new field by entering a name and choosing a type.

  • The Type control sets the data type of the column's content. Allowed types are: String (text), number (int), double, star (rating), datetime, date, time, duration, size, percentage, array, and set.

For compatibility reasons, field names must be lowercase, cannot contain spaces, commas, or slashes, and cannot begin with _.

Once a field's name and type have been set, they cannot be changed.

When selecting an existing field, press the Delete button to delete it.

Deleting a field is a destructive operation. All data associated with that field will be deleted from the database, so use this feature with extreme caution.

Once enabled, fields can be accessed and used as regular Opus columns anywhere columns are available.

There are two internal columns that are always enabled: _count (Number of fields) and _last_update (Last Saved Date).


Options

You can also access the Options dialog from the Config window to configure settings such as the script log level, backup-related options, and more.

(This dialog is also accessible from the Script Management window by clicking the About button.)

options

  • Enable ADS support as a recovery resource: When this option is enabled, if a file has no entry in the database, its ADS can be used as a recovery source. When data is set, the ADS is also updated automatically. Only applies to files on NTFS drives.

  • Enable sidecar files support as a recovery resource (read only): When this option is enabled, data can also be recovered from a sidecar file stored alongside the original file, using the same filename plus the .dopcf extension. This is for read-only recovery purposes; these files will not be created automatically.


Command arguments

Argument Type Value Description
CONFIG /S Displays the field configuration dialog.
EXPORT /O/K Exports fields to a file. Also accepts an output path.
FILES /M Files to act upon. If no files are provided, the script attempts to use the selected files in the source file display. Can be used with SET, FLUSH, PULL, PUSH, and SETUI. When used with PURGEDB, it allows the selected files to be deleted from the database.
FLUSH /O/K A comma-separated list of field names whose values should be flushed from the database. If omitted, all active fields will be flushed. If ADS support is enabled, their values will be flushed from the ADS as well.
PULL /O/K Pulls all values from active fields into a .dopcf file in JSON format. You can also specify an output filename.
PUSH /K Pushes values for active fields from a file. The file must contain a JSON object in the field:value format.
SET /M Sets values for active fields. You can pass values for multiple fields, just like with SET META.
FIND /O/R Searches the database. You can pass a raw query or enter one in the FIND dialog.
IN /K Specifies a path for FIND to search within. Must be a valid filesystem-only path.
ADVANCED /S Enables special SQL syntax for the FIND command.
PURGEDB /O Performs database cleanup, if applicable.
path/wildcard If a text value is specified, it can be the full name of a specific file or a path containing * and ? wildcards. If omitted, all entries for non-existing items are removed (KEEPMISSINGDRIVES can change this behavior).
KEEPMISSINGDRIVES /S Used with PURGEDB without a specified value, this keeps entries located on currently unavailable drives, such as external disks.
VACUUM /S Compacts the database. Use together with PURGEDB.
SETUI /O/K Sets values through a user interface.
autosave Automatically saves changes whenever the file selection changes.
text Starts with all field controls set to accept text. Useful for referencing properties in non-text fields.
NOONTOP /S Prevents the dialog from opening on top. Can be used with CONFIG and SETUI.
NOEDIT /S When used with SET, disables the add/remove feature for array-based fields.

Setting values (via SET, PUSH, or SETUI)

Valid values vary depending on the data type of each field.

  • For arrays/sets:

    • You can pass a comma- or semicolon-separated string (or an actual array when using PUSH).

    • A single quote (') can be used to escape a comma if needed.

    • To use placeholders in arrays/sets, you must pass the value as a string.

    • By default, arrays and sets support adding/removing values when used with SET, by appending the + or - suffix to each value.

      OpusCustomFields SET "tags:+test;-low" will append test and remove low (if present).

      • You cannot mix values with and without suffixes. All values must have a suffix when adding/removing items; otherwise, the entire input will be treated as a raw value. For example, +test;-low;other value is invalid for add/remove operations and will instead replace the current value with the input as is.
  • For duration values, you can pass:

    • A number indicating seconds.
    • A string in the format xYxMxDxHx(m|min)xs, where x represents an integer. Letters are case-insensitive except for M and m.
  • For size values, you can pass:

    • A number indicating bytes.
    • A string in any format that Opus recognizes as a valid size, e.g. 20.4MB.
  • For date-related values, you can pass:

    • The text now.
    • A valid ISO 8601 string in the format YYYY-MM-ddTHH:mm:ss[.ms], or the relevant portion for date and time. Include Z to indicate UTC, or specify a time zone.

To remove a field's value for a file, set it to "" (empty), regardless of its data type.

You can also reference properties of the files themselves. These can be of any type (script, evaluator, etc.). To reference a property, enclose its internal name in $_..._$. For example, $_created_$ retrieves the creation date.

Avoid referencing properties known to take a long time to calculate, such as checksums for large files, as this will stall processing for that file and all subsequent files.

  • With SET, you can pass values using the following format:

    OpusCustomFields [FILES "...." "..."] SET "field1:value" "field2:value" ...

    • If FILES is omitted, the selected files in the source file display will be used.
    • Values are specified as field:value pairs. Use quotes if they contain spaces.

    OpusCustomFields SET "mydesc:Some definition" "timestamp:$_created_$" myrating

  • PUSH accepts a file containing valid JSON in the same field:value format. The same value types accepted by SET are supported.

    OpusCustomFields PUSH="c:\path to\my file.json"

  • SETUI provides an interface for setting values more dynamically. Since it retrieves each field's current value when opened, it's ideal for editing or adding to existing values.

setui

  • Use the Use item's properties checkbox to switch the controls to text input. This is useful when referencing properties in fields that don't accept text directly.
  • After each file selection change, you must save or discard your changes using the Save button or Ctrl + S. You can enable automatic saving from the Save dropdown or by using SETUI=autosave.

FIND syntax

  • SIMPLE mode is a small expression language parsed in situ and converted into parameterized SQL. The client's text never reaches SQLite.

    expr      := and ( OR and )*
    and       := primary ( AND primary )*            (AND binds tighter than OR)
    primary   := ( expr ) | NOT ( expr ) | condition
    condition := field op value
               | field IS [NOT] EMPTY
               | field [NOT] IN ( value, ... )
               | field [NOT] BETWEEN value AND value
               | field [NOT] CONTAINS|STARTSWITH|ENDSWITH "text"     (text fields)
               | field [NOT] CONTAINS value                         (array fields - see below)
    op        :==  !=  <>  <  <=  >  >=
    value     := 12  -3.5  true  false  "text"  'text'    (dates go in quotes)
    
    • Missing values: Comparisons follow SQL semantics, so a file with no value for a field never satisfies =, !=, <, >, etc. Use field IS EMPTY or field IS NOT EMPTY to test for missing values, regardless of field type. == "" and != "" are shorthand for IS EMPTY and IS NOT EMPTY, respectively, for every field type, not just text.

    • Array fields: Only CONTAINS/NOT CONTAINS (checks whether the JSON array contains a specific element, using a plain value in the same format as above, e.g. tags CONTAINS "rock" or scores CONTAINS 5) and IS [NOT] EMPTY (null or []) are supported. =, !=, <, <=, >, >=, IN, and BETWEEN are not supported for array fields.

    • Dates: Accepted formats include:

      • Full date and time, with either T or a space between the date and time, with an explicit Z/offset for UTC or no offset for local time.
      • Date only (2026-09-21) or time only (15:30:00).
      • A placeholder keyword: today, yesterday, thisweek, lastweek, thismonth, lastmonth, thisyear, lastyear.
      • A relative duration directly appended to a number: 7d, 12h, 30min, 2y, 1.5h, 45s. The comparison operator determines what "before" or "after" means. For example, taken >= 7d finds anything from the last week onward.
  • ADVANCED mode wraps the query as follows:

SELECT _file FROM entries WHERE ( <QUERY> ) [AND _file LIKE <IN>]

Here, <QUERY> is your actual query, and <IN> is the value passed through the IN argument, if used.

  • This mode does NOT try to understand SQL. SQLite parses the expression when it runs and reports its own syntax errors. The script only lexes the text to handle strings, quoted names, and comments correctly, rejecting an empty expression and a bare ; (which would either break out of the WHERE clause into a second statement or be a pointless no-op; either way, a single expression should never need one). Otherwise, any expression-level SQL is allowed, including subqueries, CASE, json1 functions, and so on.
  • FIND has its own dialog where you can enter queries and switch between modes.

find

  • When searching, you can access the internal columns _count (Number of fields) and _last_update (Last Saved Date).
  • You can access a list of your active fields using the button or F6.
  • You can also access your recent search history using the button or F4.

CLEANING THE DATABASE

purgedb

You can maintain your database using the PURGEDB argument.

When used together with a list of FILES, this deletes those files from the database, along with their ADS entries, if any.

If you pass a wildcard to PURGEDB, or omit FILES, the script checks for orphaned entries. You can use KEEPMISSINGDRIVES to decide whether to keep entries from removable drives that are not currently available.

A list of files will then be displayed, allowing you to choose which entries to keep and which to delete. After you press OK, the selected entries will be deleted from the database.

Due to limitations in the Opus scripting API, all files on MTP devices will be listed, even if they exist.

You can also include VACUUM to compact the database.


NOTES

  • TEXT fields have a maximum length of 1,000 characters. Any excess will be automatically truncated.
  • When the database reaches 1 GB in size, certain features, such as FIND, will become unavailable.
  • Fields are NOT indexed, to maintain decent performance when setting values or rearranging the database. Even so, searches should be fast enough.
  • This script is NOT DESIGNED for indexing purposes, such as indexing file properties or content.
  • Files from remote locations stored in the database may slow down some operations, such as FIND or PURGEDB.
  • ADS reading/writing and sidecar file lookup are effectively supported only for files that report a valid drive and an NTFS filesystem through the Opus scripting API.
  • Some operations cannot run simultaneously.
  • At least for now, FTP support is disabled.

Acknowledgments

  • To the Opus team, as usual.
  • To all the testers!

The Go helper uses the following third-party libraries:

  • github.com/goccy/go-json
  • modernc.org/sqlite
  • github.com/dustin/go-humanize
  • github.com/google/uuid
  • github.com/mattn/go-isatty
  • github.com/ncruces/go-strftime
  • github.com/remyoudompheng/bigfft
  • golang.org/x/sys
  • modernc.org/libc
  • modernc.org/mathutil
  • modernc.org/memory

Changelog

v0.9.7b (Oct 15, 2026):

  • Initial public release.
6 Likes

I'm an enthusiast user - be it slowly and carefully - because this is the most complex Opus extension I've seen so far. But it's very powerful. Yet one thing that must be emphasized, I think, is with regards to the following remark:

For anyone who already saved data in NTFS/ADS via this mechanism, I suppose they can lose their previously saved comments when activating this backup mechanism (I didn't test this though). Correct me if I'm wrong, but if I'm right, then a warning should probably be added there, in all caps and with a red light sign, just in case.

Ps. This potential "calamity" could perhaps be prevented in the script itself? E.g. by implementing a small, additional step on first first access of a file's ADS comment: first appending any already existing ADS comment to the sql data (separated by a pipeline char, for instance) before doing the initial backup operation, so no "old" comments get lost. (Plus, giving a warning when exceeding the limit of 1000 chars in this case).

Check out here ADS definition:

You can have more than one btw, so I'm not sure why your comments will be in danger (unless you also saved them in an ADS named _OpusCustomFields). The script will not touch other streams.

Ah. So ... I was probably confusing

Preferences / File Operations / Copying Files / Metadata

with

Preferences / File Operations / Metadata

I'm still not sure, but I will check this. I use the Opus mechanism for storing comments in metadata.other.usercomment (a.k.a. userdesc, I think) and I thought you were using the same mechanism for your backup solution.

Here's why I find this script very useful:

  • Metadata is stored externally. No changes are made to the files themselves, which is useful when you need to preserve their hash values.
  • You can keep filenames short. Instead of cramming everything into a filename, you can move additional descriptive information into custom fields. This is especially useful for files that require lengthy descriptions, given the limits on filename and path lengths and the general best practice of keeping filenames concise.
  • You can link files that share a common topic. For example, you can use an array/set field to tag files with software and then find them with a command such as OpusCustomFields FIND tags CONTAINS "software". All other field types are searchable, too.
  • Fields aren't tied to specific file types. Unlike files built-in metadata fields, custom fields can be used across different types of files.
  • You can define as many custom fields as you need. You're not limited to a single field, and you can store text or other data types.

Of course, these are just a few of the script's many features. Many thanks again for creating and sharing it.

1 Like

I agree. I'm still not sure how exactly to use it, but with a little programming (outside errante's dialog, but using his scripted commands) I could put the metadata.other.usercomment data for thousands of files at once (in flatview mode) in the sqlite database, in a "comment" field. It's an instant backup, but I can now also search blazingly fast through that content. It is not necessarily faster than searching through thousands of files, if what you are searching for sits in the filename - but if you need to search through metadata, that's a whole different story, and having that data in sqlite makes all the difference.

Unfortunately, the sql logic does not allow a simple "get" (e.g. to edit the data of 1 file in a DOpus dialog), but this can quite easily be done via a little round-trip, using a temporary text file.

I'm still in the process of considering how to proceed from here. Data in sql is far better (faster) approachable than file metadata, but only using sql would create a huge dependency on sqlite. So it seems like storing it both ways is the better option. Yet dual storage is overhead - and the worries about that data not being updated consistently in both places. But this seems like a price one can pay for greatly improving searching through metadata. I remember Bill Gates once said he wanted sql integrated in NTFS, and when he left Microsoft he regretted not having managed to do it. I can now understand why.

I'm not sure what you're talking about, but FWIW, you can copy the userdesc value, with a one liner. :slightly_smiling_face:
OpusCustomFields SET "mydesc:$_userdesc_$"

In fact you can copy almost all properties that way (including checksums but that would also depend of your Opus settings).

Also note that unless you've found a way to explicitly write to that place, the description could be stored in other properties, depending on the filetype.

I don't know, @errante - you have the programming expertise. I guess "usercomment" is one of those older handles, which I should in my own code probably replace by "userdesc" (there seems to be some confusion about this <= even @lxp found it "odd"). The shortcut you provide is nice, but it works either way.

PS. (somewhat off-topic) In my own scripts I only make an explicit distinction for pdf's: in that case I try to store data explicitly under doc.subject because I noticed (long ago) that my pdf's sometimes have no description field (DO calls it "Comment" but it is the "userdesc" handler). And in those cases I could not always create that pdf field either - but the "subject" field always seemed to be available, so I went for that one. Whether or not this is still good practice I have no idea, but it solved my dilemma.

I wished Opus shared the part of their code (or some pseudocode, or scheme) which reveals how exactly their decision tree works for this magic userdesc field, across the jungle of file types. Some more insight wouldn't hurt.

It's not overly complicated IMHO. But you can start a new thread about your questions on that topic, and hopefully you'll get the answers you need.

Staying on THIS topic, in THIS context, what matters is how you end up writing the info. If you use something like SetAttr META "comment:my comment" or SetAttr DESCRIPTION, you can use $_userdesc_$. If you use something like subject:my comment, use $_subject_$, or $_imagedesc_$ if you use imagedesc:my comment. You could easily handle this with Evaluator, too, based on the file extension, I guess, or whatever rule you use to decide where to write the data.

Assuming you don't write to more than one property for a file, you could combine them into one (since blank values will be set to ""):

OpusCustomFields SET "mydesc:$_userdesc_$$_subject_$"

1 Like