pulith_fs/workflow/
transaction.rs1use crate::{Error, Result};
2use fs2::FileExt;
3use std::fs::File;
4use std::io::{Read, Seek, SeekFrom, Write};
5use std::path::{Path, PathBuf};
6
7pub struct Transaction {
8 file: File,
9 path: PathBuf,
10}
11
12impl Transaction {
13 fn open_file(path: impl AsRef<Path>) -> Result<File> {
14 File::options()
15 .read(true)
16 .write(true)
17 .create(true)
18 .truncate(false)
19 .open(&path)
20 .map_err(|e| Error::Write {
21 path: path.as_ref().to_path_buf(),
22 source: e,
23 })
24 }
25
26 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
27 let file = Self::open_file(path.as_ref())?;
28 let path = path.as_ref().to_path_buf();
29
30 file.lock_exclusive().map_err(|e| Error::Write {
31 path: path.clone(),
32 source: e,
33 })?;
34
35 Ok(Self { file, path })
36 }
37
38 pub fn try_open(path: impl AsRef<Path>) -> Result<Self> {
39 let file = Self::open_file(path.as_ref())?;
40 let path = path.as_ref().to_path_buf();
41
42 file.try_lock_exclusive().map_err(|e| Error::Write {
43 path: path.clone(),
44 source: e,
45 })?;
46
47 Ok(Self { file, path })
48 }
49
50 pub fn open_locked(path: impl AsRef<Path>) -> Result<Self> {
51 Self::open(path)
52 }
53
54 pub fn try_open_locked(path: impl AsRef<Path>) -> Result<Self> {
55 Self::try_open(path)
56 }
57
58 pub fn path(&self) -> &Path {
59 &self.path
60 }
61
62 pub fn read(&self) -> Result<Vec<u8>> {
63 let mut file = self.file.try_clone().map_err(|e| Error::Read {
64 path: self.path.clone(),
65 source: e,
66 })?;
67 file.seek(SeekFrom::Start(0)).map_err(|e| Error::Read {
68 path: self.path.clone(),
69 source: e,
70 })?;
71
72 let mut bytes = Vec::new();
73 file.read_to_end(&mut bytes).map_err(|e| Error::Read {
74 path: self.path.clone(),
75 source: e,
76 })?;
77 Ok(bytes)
78 }
79
80 pub fn write(&self, data: &[u8]) -> Result<()> {
81 let mut file = self.file.try_clone().map_err(|e| Error::Write {
82 path: self.path.clone(),
83 source: e,
84 })?;
85 file.seek(SeekFrom::Start(0)).map_err(|e| Error::Write {
86 path: self.path.clone(),
87 source: e,
88 })?;
89 file.set_len(0).map_err(|e| Error::Write {
90 path: self.path.clone(),
91 source: e,
92 })?;
93 file.write_all(data).map_err(|e| Error::Write {
94 path: self.path.clone(),
95 source: e,
96 })?;
97 file.sync_all().map_err(|e| Error::Write {
98 path: self.path.clone(),
99 source: e,
100 })
101 }
102
103 pub fn execute<F>(&self, operation: F) -> Result<Vec<u8>>
104 where
105 F: FnOnce(&[u8]) -> Result<Vec<u8>>,
106 {
107 let current = self.read()?;
108 let next = operation(¤t)?;
109 self.write(&next)?;
110 Ok(next)
111 }
112}
113
114impl Drop for Transaction {
115 fn drop(&mut self) {
116 let _ = self.file.unlock();
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use tempfile::tempdir;
124
125 #[test]
126 fn test_transaction_lock() {
127 let dir = tempdir().unwrap();
128 let path = dir.path().join("test.bin");
129 let tx = Transaction::open(&path).unwrap();
130 tx.write(b"data").unwrap();
131 assert_eq!(tx.read().unwrap(), b"data");
132 }
133
134 #[test]
135 fn test_transaction_execute() {
136 let dir = tempdir().unwrap();
137 let path = dir.path().join("registry.json");
138 let tx = Transaction::open(&path).unwrap();
139
140 let result = tx
141 .execute(|bytes| {
142 assert!(bytes.is_empty());
143 Ok(b"version=1".to_vec())
144 })
145 .unwrap();
146
147 assert_eq!(result, b"version=1");
148 assert_eq!(tx.read().unwrap(), b"version=1");
149 drop(tx);
150 assert_eq!(std::fs::read(path).unwrap(), b"version=1");
151 }
152
153 #[test]
154 fn test_transaction_try_open_locked() {
155 let dir = tempdir().unwrap();
156 let path = dir.path().join("test.bin");
157 let tx = Transaction::open(&path).unwrap();
158 let second = Transaction::try_open(&path);
159
160 assert!(second.is_err());
161 drop(tx);
162 assert!(Transaction::try_open(&path).is_ok());
163 }
164}