How OpenLogi Handles Atomic Writes and File Backups for Safe Configuration Persistence

OpenLogi prevents data corruption and concurrent edit conflicts through a three-stage mechanism: conflict detection via content hashing, atomic file writes using temporary files and rename operations, and automatic backup rotation keeping five historical generations.

OpenLogi's configuration system persists user preferences to a TOML file (config.toml). Because this file may be edited by external tools or concurrently accessed, the application implements crash-safe, atomic persistence that prevents half-written files and detects stale data. The implementation lives primarily in crates/openlogi-core/src/config/file.rs and combines conflict detection, atomic writes, and versioned backups.

Conflict Detection Before Writing

Before any save operation, OpenLogi verifies that the on-disk file has not changed since it was loaded. This prevents a GUI snapshot from overwriting modifications made by an external editor.

When ConfigFile::load_from_path executes, it stores the raw file contents in a source field (lines 24‑30). The save() method then re-reads the entire file and compares it against this stored revision:

// Simplified conflict check from save() implementation
let current_content = std::fs::read_to_string(&self.path)?;
if current_content != self.source {
    return Err(ConfigError::Conflict);
}

If the content differs, ConfigError::Conflict is returned and the write aborts. This check appears at lines 65‑68 in the source file.

Atomic Write Implementation with write_atomic()

All configuration writes route through write_atomic() (lines 27‑42), which uses the atomic_write_file crate to guarantee crash safety:

  1. Write data to a temporary file with restrictive permissions (0o600 on Unix)
  2. Invoke file.commit() to atomically rename the temporary file into place

This rename-only approach ensures the target file either contains the complete new contents or remains untouched—never a partially written state.

use atomic_write_file::AtomicWriteFile;
use std::io::Write;

pub fn write_atomic<P: AsRef<Path>, C: AsRef<[u8]>>(
    path: P,
    contents: C,
) -> Result<()> {
    let mut file = AtomicWriteFile::new(path)?;
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        file.as_file_mut().set_permissions(
            std::fs::Permissions::from_mode(0o600)
        )?;
    }
    file.write_all(contents.as_ref())?;
    file.commit()?;
    Ok(())
}

The 0o600 permission bit prevents other users from reading potentially sensitive configuration values during the write window.

Backup Rotation Strategy

OpenLogi maintains up to five generations of backup copies using a cascading rotation scheme.

First-Time Backup Per Session

The backup_config_once() function ensures exactly one backup is created per file, per process run. It tracks completed backups in a global HashSet static called BACKED_UP_CONFIGS (lines 19‑22):

static BACKED_UP_CONFIGS: Mutex<HashSet<PathBuf>> = 
    Mutex::new(HashSet::new());

If the path has not been backed up this session and the file exists, backup_existing_config() is invoked (lines 70‑84).

Generational Backup Rotation

The backup_existing_config() function implements classic backup rotation (lines 87‑95):

Action Description
Shift generation 4 → 5 Remove oldest if present
Shift generation 3 → 4 Rename file
Shift generation 2 → 3 Rename file
Shift generation 1 → 2 Rename file
Write current → generation 1 Copy current config as newest backup

Backup filenames follow the pattern config.toml.backup.1 through config.toml.backup.5, where .1 is always the most recent.

// Backup rotation logic (simplified)
const CONFIG_BACKUP_GENERATIONS: usize = 5;

for i in (1..CONFIG_BACKUP_GENERATIONS).rev() {
    let src = path.with_extension(format!("backup.{}", i));
    let dst = path.with_extension(format!("backup.{}", i + 1));
    if src.exists() {
        std::fs::rename(&src, &dst)?;
    }
}
// Current file becomes backup.1
let backup_path = path.with_extension("backup.1");
std::fs::copy(path, backup_path)?;

Migration-Aware Emergency Backup

When OpenLogi loads a configuration requiring schema migration, it preserves the exact pre-migration file before any write occurs. The migration_backup_path helper (lines 103‑112) generates a distinctive path like config.toml.backup.migration, ensuring users can recover their original settings if the migration introduces problems.

Complete Configuration Save Workflow

The full persistence pipeline executes in this order:

  1. Conflict check – Re-read and hash-compare current file against source
  2. First backup – backup_config_once() triggers if this session hasn't backed up
  3. Rotate backups – Shift older generations, write current to .backup.1
  4. Migration backup – Save pre-migration copy if schema upgrade occurred
  5. Atomic commit – Write to temp file, rename into place, return success

Code Usage Examples

Load, modify, and save with conflict detection:

use openlogi_core::config::{Config, ConfigFile};

// Load stores source revision for safe saves
let (mut config, mut cfg_file) = ConfigFile::load_or_default()?;

// Modify configuration
config.display.theme = "dark".to_string();

// Atomic save with automatic conflict detection
cfg_file.save(&config)?; // Returns ConfigError::Conflict if file changed externally

Perform a one-off atomic write without tracking (useful for CLI tools):

Config::load_or_default()?.save_atomic()?; // Internally calls ConfigFile::save

Manually trigger backup rotation (rarely needed):

use openlogi_core::config::file::backup_existing_config;
use openlogi_core::paths;

backup_existing_config(&paths::config_path()?)?;

Summary

OpenLogi's configuration persistence provides three layers of protection against data loss:

  • Conflict detection via content comparison prevents overwriting concurrent edits
  • Atomic writes through temporary files and rename operations eliminate corruption risk from crashes
  • Generational backups preserve five historical versions plus dedicated migration snapshots

These mechanisms are implemented in crates/openlogi-core/src/config/file.rs and operate automatically on every save, requiring no user intervention.

Frequently Asked Questions

What happens if two OpenLogi instances try to save simultaneously?

The first instance to call save() succeeds. The second instance fails with ConfigError::Conflict because the on-disk content no longer matches its stored source revision. The application can then prompt the user to reload or merge changes.

Why does OpenLogi use five backup generations instead of unlimited storage?

Five generations balance recovery utility against disk footprint. Most configuration mistakes are caught immediately, and older backups rarely provide additional value. The fixed limit prevents unbounded growth in long-running installations.

Can I disable automatic backups or change the backup location?

The current implementation in crates/openlogi-core/src/config/file.rs hardcodes CONFIG_BACKUP_GENERATIONS = 5 and places backups adjacent to the main configuration file. To customize this behavior, you would need to modify the source and rebuild.

How does the migration backup differ from regular rotation backups?

Regular rotation backups (.backup.1 through .backup.5) capture the state before each save operation during normal use. Migration backups (.backup.migration) specifically preserve the pre-migration file exactly once, before any schema-upgraded data is written, providing a clean rollback point independent of the rotation cycle.

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 →