Skip to main content

agent_migrate_core/transaction/
engine.rs

1use std::path::Path;
2
3use crate::transaction::conflict::resolve_plan;
4use crate::transaction::filesystem::FileSystem;
5use crate::transaction::snapshot_store::SnapshotStore;
6use crate::transaction::{
7    SnapshotId, TransactionError, TransactionOptions, TransactionResult, WritePlan,
8};
9
10#[derive(Debug, Clone, Copy)]
11pub struct TransactionEngine {
12    private_mode: u32,
13}
14
15impl Default for TransactionEngine {
16    fn default() -> Self {
17        Self {
18            private_mode: 0o600,
19        }
20    }
21}
22
23impl TransactionEngine {
24    /// Applies a write plan atomically enough to restore all touched files on failure.
25    ///
26    /// # Errors
27    ///
28    /// Returns an error for conflicts, locks, unsafe paths, I/O failure, or failed doctor.
29    pub fn apply<F>(
30        self,
31        plan: &WritePlan,
32        options: &TransactionOptions,
33        doctor: F,
34    ) -> Result<TransactionResult, TransactionError>
35    where
36        F: FnOnce(&Path) -> Result<(), String>,
37    {
38        let filesystem = FileSystem::new(self.private_mode);
39        let resolved = resolve_plan(plan, options.root(), options.conflict_policy())?;
40        preflight(&resolved, options)?;
41        let snapshot_id = SnapshotId::new();
42        if options.is_dry_run() {
43            return Ok(TransactionResult {
44                snapshot_id,
45                dry_run: true,
46                writes: resolved.len(),
47            });
48        }
49        FileSystem::prepare_roots(options)?;
50        let _lock = FileSystem::acquire_lock(options)?;
51        let snapshots = SnapshotStore::new(filesystem, options);
52        snapshots.create(snapshot_id, &resolved)?;
53        for write in resolved.writes() {
54            let target = options.root().join(write.path.as_path());
55            if let Err(error) =
56                filesystem.atomic_write(options.root(), &target, &write.content, write.mode)
57            {
58                return match snapshots.restore(snapshot_id) {
59                    Ok(()) => Err(error),
60                    Err(rollback) => Err(TransactionError::RollbackFailed {
61                        message: format!("{error}; {rollback}"),
62                    }),
63                };
64            }
65        }
66        if let Err(message) = doctor(options.root()) {
67            return match snapshots.restore(snapshot_id) {
68                Ok(()) => Err(TransactionError::DoctorFailed { message }),
69                Err(rollback) => Err(TransactionError::RollbackFailed {
70                    message: format!("{message}; {rollback}"),
71                }),
72            };
73        }
74        Ok(TransactionResult {
75            snapshot_id,
76            dry_run: false,
77            writes: resolved.len(),
78        })
79    }
80
81    /// Restores every file recorded by a transaction snapshot.
82    ///
83    /// # Errors
84    ///
85    /// Returns an error when the snapshot is absent, invalid, locked, or cannot be restored.
86    pub fn rollback(
87        self,
88        snapshot_id: SnapshotId,
89        options: &TransactionOptions,
90    ) -> Result<(), TransactionError> {
91        let filesystem = FileSystem::new(self.private_mode);
92        FileSystem::prepare_roots(options)?;
93        let _lock = FileSystem::acquire_lock(options)?;
94        SnapshotStore::new(filesystem, options).restore(snapshot_id)
95    }
96}
97
98fn preflight(plan: &WritePlan, options: &TransactionOptions) -> Result<(), TransactionError> {
99    for write in plan.writes() {
100        FileSystem::ensure_path_safe(options.root(), write.path.as_path())?;
101    }
102    Ok(())
103}