blockworx/storage/
atomic.rs1use std::io::Write as _;
10use std::path::{Path, PathBuf};
11
12use super::Durability;
13
14pub fn write_atomically(path: &Path, contents: &[u8]) -> std::io::Result<()> {
21 write_with(path, contents, Durability::Sync)
22}
23
24pub fn write_with(path: &Path, contents: &[u8], durability: Durability) -> std::io::Result<()> {
28 let target = resolve(path);
29 let temp = temp_path(&target)?;
30 write_and_replace(&temp, &target, contents, durability).inspect_err(|_| {
31 let _ = std::fs::remove_file(&temp);
34 })
35}
36
37fn resolve(path: &Path) -> PathBuf {
41 std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
42}
43
44fn temp_path(target: &Path) -> std::io::Result<PathBuf> {
47 let name = target.file_name().ok_or_else(|| {
48 std::io::Error::new(
49 std::io::ErrorKind::InvalidInput,
50 format!("{} names no file to write", target.display()),
51 )
52 })?;
53 let mut temp = std::ffi::OsString::from(".");
54 temp.push(name);
55 temp.push(".tmp");
56 Ok(target.with_file_name(temp))
57}
58
59fn write_and_replace(
60 temp: &Path,
61 target: &Path,
62 contents: &[u8],
63 durability: Durability,
64) -> std::io::Result<()> {
65 let mut file = std::fs::File::create(temp)?;
66 file.write_all(contents)?;
67 if durability == Durability::Sync {
70 file.sync_all()?;
71 }
72 drop(file);
73 std::fs::rename(temp, target)?;
74 if durability == Durability::Sync {
75 sync_dir(parent_dir(target))?;
76 }
77 Ok(())
78}
79
80fn parent_dir(target: &Path) -> &Path {
83 match target.parent() {
84 Some(dir) if !dir.as_os_str().is_empty() => dir,
85 _ => Path::new("."),
86 }
87}
88
89#[cfg(unix)]
92fn sync_dir(dir: &Path) -> std::io::Result<()> {
93 std::fs::File::open(dir)?.sync_all()
94}
95
96#[cfg(not(unix))]
99fn sync_dir(_dir: &Path) -> std::io::Result<()> {
100 Ok(())
101}
102
103#[cfg(test)]
104pub mod tests {
105 use super::*;
106
107 pub struct TempDir(PathBuf);
111
112 impl TempDir {
113 pub fn new(name: &str) -> Self {
118 let dir = std::env::temp_dir().join(format!("blockworx-{}-{name}", std::process::id()));
119 let _ = std::fs::remove_dir_all(&dir);
120 std::fs::create_dir_all(&dir).unwrap();
121 Self(dir)
122 }
123
124 pub fn join(&self, name: &str) -> PathBuf {
125 self.0.join(name)
126 }
127
128 pub fn path(&self) -> &Path {
129 &self.0
130 }
131 }
132
133 impl Drop for TempDir {
134 fn drop(&mut self) {
135 let _ = std::fs::remove_dir_all(&self.0);
136 }
137 }
138
139 fn read(path: &Path) -> String {
140 std::fs::read_to_string(path).unwrap()
141 }
142
143 #[test]
144 fn it_creates_and_then_overwrites() {
145 let dir = TempDir::new("create-overwrite");
146 let doc = dir.join("doc.kdl");
147
148 write_atomically(&doc, b"first").unwrap();
149 assert_eq!(read(&doc), "first");
150
151 write_atomically(&doc, b"second").unwrap();
152 assert_eq!(read(&doc), "second");
153 }
154
155 #[test]
156 fn a_successful_write_leaves_no_temp_behind() {
157 let dir = TempDir::new("no-temp");
158 let doc = dir.join("doc.kdl");
159 write_atomically(&doc, b"content").unwrap();
160
161 let leftovers: Vec<_> = std::fs::read_dir(&dir.0)
162 .unwrap()
163 .map(|e| e.unwrap().file_name())
164 .filter(|name| name != "doc.kdl")
165 .collect();
166 assert!(leftovers.is_empty(), "left {leftovers:?} behind");
167 }
168
169 #[test]
174 fn a_failed_write_leaves_the_previous_contents_intact() {
175 let dir = TempDir::new("failed-write");
176 let doc = dir.join("doc.kdl");
177 write_atomically(&doc, b"the good version").unwrap();
178
179 let blocker = dir.join(".doc.kdl.tmp");
180 std::fs::create_dir(&blocker).unwrap();
181 assert_eq!(temp_path(&resolve(&doc)).unwrap(), blocker);
183
184 assert!(write_atomically(&doc, b"a doomed longer version").is_err());
185 assert_eq!(read(&doc), "the good version");
186 }
187
188 #[test]
189 fn a_path_naming_no_file_is_an_error() {
190 assert!(write_atomically(Path::new(".."), b"x").is_err());
191 }
192
193 #[cfg(unix)]
196 #[test]
197 fn writing_a_symlink_updates_its_target() {
198 let dir = TempDir::new("symlink");
199 let target = dir.join("real.kdl");
200 let link = dir.join("link.kdl");
201 write_atomically(&target, b"before").unwrap();
202 std::os::unix::fs::symlink(&target, &link).unwrap();
203
204 write_atomically(&link, b"after").unwrap();
205
206 assert!(
207 std::fs::symlink_metadata(&link).unwrap().is_symlink(),
208 "the link itself was replaced"
209 );
210 assert_eq!(read(&target), "after");
211 }
212}