Skip to main content

pulith_fs/primitives/
rw.rs

1use crate::permissions::PermissionMode;
2use crate::{Error, Result};
3use std::fs;
4use std::path::Path;
5
6#[derive(Clone, Copy, Debug, Default)]
7pub struct Options {
8    pub permissions: Option<PermissionMode>,
9    pub sync: bool,
10}
11
12impl Options {
13    pub fn new() -> Self {
14        Self::default()
15    }
16    pub fn permissions(mut self, mode: PermissionMode) -> Self {
17        self.permissions = Some(mode);
18        self
19    }
20    pub fn sync(mut self, sync: bool) -> Self {
21        self.sync = sync;
22        self
23    }
24}
25
26pub fn atomic_write(path: impl AsRef<Path>, content: &[u8], options: Options) -> Result<()> {
27    let path = path.as_ref();
28    let parent = path.parent().ok_or_else(|| Error::Write {
29        path: path.to_path_buf(),
30        source: std::io::Error::other("no parent directory"),
31    })?;
32
33    let mut tmp_path = parent.to_path_buf();
34    tmp_path.push(format!(".tmp.{}.pulith", uuid::Uuid::new_v4()));
35
36    fs::write(&tmp_path, content).map_err(|e| Error::Write {
37        path: tmp_path.clone(),
38        source: e,
39    })?;
40
41    if let Some(mode) = options.permissions {
42        mode.apply_to_path(&tmp_path)?;
43    }
44
45    if options.sync {
46        let file = fs::File::open(&tmp_path).map_err(|e| Error::Write {
47            path: tmp_path.clone(),
48            source: e,
49        })?;
50        file.sync_all().map_err(|e| Error::Write {
51            path: tmp_path.clone(),
52            source: e,
53        })?;
54    }
55
56    fs::rename(&tmp_path, path).map_err(|e| {
57        let _ = fs::remove_file(&tmp_path);
58        Error::Write {
59            path: path.to_path_buf(),
60            source: e,
61        }
62    })?;
63
64    Ok(())
65}
66
67pub fn atomic_read(path: impl AsRef<Path>) -> Result<Vec<u8>> {
68    let path = path.as_ref();
69    std::fs::read(path).map_err(|e| Error::Read {
70        path: path.to_path_buf(),
71        source: e,
72    })
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use tempfile::tempdir;
79
80    #[test]
81    fn test_atomic_write() {
82        let dir = tempdir().unwrap();
83        let path = dir.path().join("test.txt");
84        atomic_write(&path, b"hello world", Options::new()).unwrap();
85        assert_eq!(fs::read(&path).unwrap(), b"hello world");
86    }
87
88    #[test]
89    fn test_atomic_write_with_custom_permissions() {
90        let dir = tempdir().unwrap();
91        let path = dir.path().join("test.txt");
92        atomic_write(
93            &path,
94            b"data",
95            Options::new().permissions(PermissionMode::Custom(0o755)),
96        )
97        .unwrap();
98        #[cfg(unix)]
99        {
100            use std::os::unix::fs::PermissionsExt;
101            let metadata = fs::metadata(&path).unwrap();
102            assert_eq!(metadata.permissions().mode() & 0o777, 0o755);
103        }
104    }
105
106    #[test]
107    fn test_atomic_write_with_readonly() {
108        let dir = tempdir().unwrap();
109        let path = dir.path().join("test.txt");
110        atomic_write(
111            &path,
112            b"data",
113            Options::new().permissions(PermissionMode::ReadOnly),
114        )
115        .unwrap();
116        let metadata = fs::metadata(&path).unwrap();
117        #[cfg(unix)]
118        {
119            use std::os::unix::fs::PermissionsExt;
120            assert_eq!(metadata.permissions().mode() & 0o777, 0o444);
121        }
122        #[cfg(windows)]
123        assert!(metadata.permissions().readonly());
124    }
125
126    #[test]
127    fn test_atomic_write_with_inherit() {
128        let dir = tempdir().unwrap();
129        let path = dir.path().join("test.txt");
130        atomic_write(
131            &path,
132            b"data",
133            Options::new().permissions(PermissionMode::Inherit),
134        )
135        .unwrap();
136        assert!(path.exists());
137    }
138}