Skip to main content

pulith_fs/workflow/
transaction.rs

1use 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(&current)?;
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}