Skip to main content

pray_core/transaction/
mod.rs

1mod journal;
2mod owner;
3
4use crate::{PrayError, PrayResult};
5use journal::Journal;
6use std::{cell::RefCell, fs, path::Path};
7
8thread_local! { static ACTIVE: RefCell<Option<Journal>> = const { RefCell::new(None) }; }
9
10pub fn run<T>(root: &Path, operation: impl FnOnce() -> PrayResult<T>) -> PrayResult<T> {
11    let root = if root.is_absolute() {
12        root.to_path_buf()
13    } else {
14        std::env::current_dir()?.join(root)
15    };
16    if ACTIVE.with(|active| {
17        active
18            .borrow()
19            .as_ref()
20            .is_some_and(|journal| journal.root == root)
21    }) {
22        return operation();
23    }
24    if ACTIVE.with(|active| active.borrow().is_some()) {
25        return Err(owner::failure(
26            "a command cannot write two projects in one transaction",
27        ));
28    }
29    let directory = root.join(".pray/write-state");
30    crate::render_path_guard::ensure_safe_destination_ancestors(
31        &root,
32        Path::new(".pray/write-state/owner"),
33        "project write state",
34    )?;
35    fs::create_dir_all(&directory)?;
36    #[cfg(unix)]
37    {
38        use std::os::unix::fs::OpenOptionsExt;
39        use std::os::unix::fs::PermissionsExt;
40        let handle = fs::OpenOptions::new()
41            .read(true)
42            .custom_flags(libc::O_NOFOLLOW | libc::O_DIRECTORY)
43            .open(&directory)?;
44        handle.set_permissions(fs::Permissions::from_mode(0o700))?;
45    }
46    crate::render_path_guard::ensure_safe_destination_ancestors(
47        &root,
48        Path::new(".pray/write-state/owner"),
49        "project write state",
50    )?;
51    for path in [&directory, &root.join(".pray"), &root] {
52        owner::sync_directory(path)?;
53    }
54    let _owner = owner::acquire(&directory)?;
55    let mut journal = Journal::open(root, directory)?;
56    journal.recover()?;
57    ACTIVE.with(|active| *active.borrow_mut() = Some(journal));
58    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(operation));
59    let mut journal = ACTIVE.with(|active| active.borrow_mut().take().unwrap());
60    match result {
61        Ok(Ok(value)) => {
62            journal.commit()?;
63            Ok(value)
64        }
65        Ok(Err(error)) => match journal.recover() {
66            Ok(()) => Err(error),
67            Err(recovery) => Err(PrayError::Render(format!(
68                "{error}\nRecovery stopped: {recovery}"
69            ))),
70        },
71        Err(panic) => {
72            let _ = journal.recover();
73            std::panic::resume_unwind(panic)
74        }
75    }
76}
77
78pub fn write_file(path: &Path, bytes: impl AsRef<[u8]>) -> PrayResult<()> {
79    let bytes = bytes.as_ref();
80    if ACTIVE.with(|active| active.borrow().is_some()) {
81        let before = match crate::render_file::destination_kind(path)? {
82            crate::render_file::DestinationKind::Missing => None,
83            _ => Some(crate::render_file::read_regular_bytes(
84                path,
85                &path.display().to_string(),
86            )?),
87        };
88        replace(path, before.as_deref(), Some(bytes))?;
89    } else {
90        fs::write(path, bytes)?;
91    }
92    Ok(())
93}
94
95pub(crate) fn remove_file(path: &Path) -> PrayResult<()> {
96    if ACTIVE.with(|active| active.borrow().is_some()) {
97        let before = crate::render_file::read_regular_bytes(path, &path.display().to_string())?;
98        replace(path, Some(&before), None)?;
99    } else {
100        fs::remove_file(path)?;
101    }
102    Ok(())
103}
104
105pub(crate) fn replace(
106    path: &Path,
107    before: Option<&[u8]>,
108    after: Option<&[u8]>,
109) -> PrayResult<bool> {
110    ACTIVE.with(|active| {
111        let mut active = active.borrow_mut();
112        let Some(journal) = active.as_mut() else {
113            return Ok(false);
114        };
115        journal.replace(path, before, after)?;
116        Ok(true)
117    })
118}