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

> OpenLogi automates TOML configuration file migrations with version detection, data transformations, and atomic backups. Ensure seamless upgrades and prevent conflicts.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: how-to-guide
- Published: 2026-09-11

---

**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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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:

```rust
// 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`](https://github.com/AprilNEA/OpenLogi/blob/main/src/config.rs) handles v5 upgrades by restructuring device identifiers:

```rust
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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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.