How OpenLogi Handles Configuration File Migrations: Version-Aware TOML Upgrades

OpenLogi automatically migrates legacy TOML configuration files to the current schema by detecting version headers, preserving original source text, applying sequential data transformations, and creating atomic backups while preventing write conflicts.

OpenLogi is an open-source input device management system written in Rust that persists user settings to a local config.toml file. As the codebase evolves, the configuration schema undergoes breaking changes—currently at schema version 7—necessitating a robust migration pipeline that upgrades older files without data loss or corruption risks.

Detecting Schema Versions During Load

The migration process initiates in crates/openlogi-core/src/config/file.rs within the ConfigFile::load_from_path method. This function reads the raw TOML file and deserializes a minimal ConfigHeader struct containing only the schema_version field to determine if the file predates the current SCHEMA_VERSION constant.

If the loaded version is older than the current release, the original file text is immediately stored in the ConfigFile::migrated_from field. This preservation step occurs before any structural transformations, ensuring the pre-migration state remains available for backup creation even if parsing fails.

Executing Sequential Migrations

After header detection and initial deserialization, load_from_path invokes three specific migration methods on the Config struct defined in crates/openlogi-core/src/config.rs. These version-gated transformations upgrade data in-place:

  • migrate_owner_locked_gestures: Lifts the legacy "owner-locked" gesture model introduced in schema v4, converting old ownership semantics to the current gesture binding system.
  • migrate_transport_scoped_keys: Rewrites device keys containing transport prefixes (e.g., "direct:") for the v5 schema, stripping prefixes and relocating values to DeviceConfig::links while preserving logical device identifiers.
  • migrate_thumbwheel_native_direction: Normalizes thumb-wheel scroll direction values for the v7 schema, ensuring consistent input handling across different hardware generations.

Each method mutates the configuration instance sequentially, allowing cumulative upgrades from very old versions to the current schema in a single load cycle.

Atomic Backup and Conflict Prevention

Before writing changes to disk, ConfigFile::save implements a safety protocol that protects user data. First, it generates a backup path via migration_backup_path, writing the exact pre-migration text to config.toml.v<old>.bak where <old> represents the original schema version. This backup persists permanently, providing a recovery path even if subsequent save operations fail.

The save routine implements conflict detection by comparing the current on-disk file content against the originally loaded source (current != self.source). If external modifications are detected, the function aborts with ConfigError::Conflict, preventing accidental overwrites of concurrent changes. Otherwise, the system uses AtomicWriteFile to perform atomic writes, ensuring that partial updates cannot leave the configuration file in a corrupted intermediate state.

Practical Migration Examples

When loading a legacy configuration, the automatic migration workflow appears transparent to calling code:

// Loads and migrates configuration in one operation
let (config, mut file) = ConfigFile::load_from_path(&path)?;

// `config` now conforms to SCHEMA_VERSION 7
// If migration occurred, `file` holds the original text for backup

file.save(&config)?; // Atomically persists with backup creation

The individual migration methods contain logic specific to their schema transitions. For example, the transport-scoped key migration in src/config.rs handles v5 upgrades by restructuring device identifiers:

impl Config {
    fn migrate_transport_scoped_keys(&mut self) {
        // Transforms v4/v5 transport-prefixed keys like "direct:device123"
        // into modern DeviceConfig links without prefixes
        for device in &mut self.devices {
            // Rewrite logic strips prefixes and updates links field
            // while maintaining device UUID continuity
        }
    }
}

Testing Migration Correctness

The core test suite in crates/openlogi-core/src/config/tests.rs validates the entire migration pipeline under various edge cases. Notable test scenarios verify that:

  • Backup files are created exactly once during the first save after migration
  • Transport-scoped keys undergo correct prefix stripping and field relocation
  • Backup files contain the exact pre-migration text, not intermediate transformation states
  • Conflict detection triggers appropriately when the underlying file changes during runtime

These tests ensure that users upgrading across multiple versions—say from v4 directly to v7—experience reliable data preservation without manual intervention.

Summary

  • Version Detection: ConfigFile::load_from_path parses ConfigHeader to identify outdated schemas before full deserialization
  • Data Preservation: Original file contents are stored in migrated_from immediately upon detecting an old version
  • Sequential Upgrades: Three specific methods—migrate_owner_locked_gestures, migrate_transport_scoped_keys, and migrate_thumbwheel_native_direction—handle v4, v5, and v7 transitions respectively
  • Atomic Safety: Pre-migration backups are written to config.toml.v<old>.bak before the first save, and AtomicWriteFile prevents partial writes
  • Conflict Prevention: Write operations verify the source file remains unchanged since loading, aborting with ConfigError::Conflict if external modifications are detected

Frequently Asked Questions

What happens if a configuration file is too old to migrate?

OpenLogi supports migrations from any previous schema version up to the current SCHEMA_VERSION 7. The sequential migration methods in src/config.rs apply transformations cumulatively. For example, a v4 file undergoes migrate_owner_locked_gestures, migrate_transport_scoped_keys, and migrate_thumbwheel_native_direction in order during a single load operation, ensuring all intermediate schema changes are applied correctly.

Where are migration backups stored and how long do they persist?

Backups are created in the same directory as the original configuration file using the naming convention config.toml.v<old>.bak, where <old> represents the pre-migration schema version. According to the implementation in src/config/file.rs, these backups are written once during the first save after migration and persist indefinitely as a recovery mechanism, even if subsequent saves or migrations fail.

How does OpenLogi prevent configuration corruption during saves?

The save routine in ConfigFile::save implements two protective mechanisms. First, it verifies the on-disk file has not been modified externally since loading, returning ConfigError::Conflict if changes are detected. Second, it uses AtomicWriteFile to write both the migration backup and the updated configuration atomically, ensuring that system crashes or power failures cannot result in partially written or truncated TOML files.

Can I disable automatic configuration migrations?

No, the migration system is mandatory and transparent. When ConfigFile::load_from_path detects a schema version lower than the current SCHEMA_VERSION, it automatically applies the necessary transformations to ensure the in-memory Config struct matches the expected layout. However, the original file remains untouched until the first explicit save call, at which point the atomic backup is created before any changes are written to disk.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →