agent-migrate-core 0.6.1

Core models and transactional filesystem engine for agent configuration migration
Documentation
use std::path::Path;

use crate::transaction::conflict::resolve_plan;
use crate::transaction::filesystem::FileSystem;
use crate::transaction::snapshot_store::SnapshotStore;
use crate::transaction::{
    SnapshotId, TransactionError, TransactionOptions, TransactionResult, WritePlan,
};

#[derive(Debug, Clone, Copy)]
pub struct TransactionEngine {
    private_mode: u32,
}

impl Default for TransactionEngine {
    fn default() -> Self {
        Self {
            private_mode: 0o600,
        }
    }
}

impl TransactionEngine {
    /// Applies a write plan atomically enough to restore all touched files on failure.
    ///
    /// # Errors
    ///
    /// Returns an error for conflicts, locks, unsafe paths, I/O failure, or failed doctor.
    pub fn apply<F>(
        self,
        plan: &WritePlan,
        options: &TransactionOptions,
        doctor: F,
    ) -> Result<TransactionResult, TransactionError>
    where
        F: FnOnce(&Path) -> Result<(), String>,
    {
        let filesystem = FileSystem::new(self.private_mode);
        let resolved = resolve_plan(plan, options.root(), options.conflict_policy())?;
        preflight(&resolved, options)?;
        let snapshot_id = SnapshotId::new();
        if options.is_dry_run() {
            return Ok(TransactionResult {
                snapshot_id,
                dry_run: true,
                writes: resolved.len(),
            });
        }
        FileSystem::prepare_roots(options)?;
        let _lock = FileSystem::acquire_lock(options)?;
        let snapshots = SnapshotStore::new(filesystem, options);
        snapshots.create(snapshot_id, &resolved)?;
        for write in resolved.writes() {
            let target = options.root().join(write.path.as_path());
            if let Err(error) =
                filesystem.atomic_write(options.root(), &target, &write.content, write.mode)
            {
                return match snapshots.restore(snapshot_id) {
                    Ok(()) => Err(error),
                    Err(rollback) => Err(TransactionError::RollbackFailed {
                        message: format!("{error}; {rollback}"),
                    }),
                };
            }
        }
        if let Err(message) = doctor(options.root()) {
            return match snapshots.restore(snapshot_id) {
                Ok(()) => Err(TransactionError::DoctorFailed { message }),
                Err(rollback) => Err(TransactionError::RollbackFailed {
                    message: format!("{message}; {rollback}"),
                }),
            };
        }
        Ok(TransactionResult {
            snapshot_id,
            dry_run: false,
            writes: resolved.len(),
        })
    }

    /// Restores every file recorded by a transaction snapshot.
    ///
    /// # Errors
    ///
    /// Returns an error when the snapshot is absent, invalid, locked, or cannot be restored.
    pub fn rollback(
        self,
        snapshot_id: SnapshotId,
        options: &TransactionOptions,
    ) -> Result<(), TransactionError> {
        let filesystem = FileSystem::new(self.private_mode);
        FileSystem::prepare_roots(options)?;
        let _lock = FileSystem::acquire_lock(options)?;
        SnapshotStore::new(filesystem, options).restore(snapshot_id)
    }
}

fn preflight(plan: &WritePlan, options: &TransactionOptions) -> Result<(), TransactionError> {
    for write in plan.writes() {
        FileSystem::ensure_path_safe(options.root(), write.path.as_path())?;
    }
    Ok(())
}