pulith_fs/primitives/
rw.rs1use 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}