pray_core/transaction/
mod.rs1mod 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}