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

> Discover how OpenLogi ensures safe configuration persistence with atomic writes and automatic backups. Learn about its three-stage mechanism for preventing data corruption and conflicts.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: internals
- Published: 2026-08-31

---

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

```rust
// 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.

```rust
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):

```rust
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.

```rust
// 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:

```rust
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):

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

```

Manually trigger backup rotation (rarely needed):

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