laminar_core/
durable_fs.rs1use std::io;
9use std::path::Path;
10
11pub fn ensure_durable_directory(path: &Path) -> io::Result<()> {
24 if path.as_os_str().is_empty() {
25 return Err(io::Error::new(
26 io::ErrorKind::InvalidInput,
27 "durable directory path is empty",
28 ));
29 }
30 let path = if path.is_absolute() {
31 path.to_path_buf()
32 } else {
33 std::env::current_dir()?.join(path)
34 };
35
36 match std::fs::symlink_metadata(&path) {
37 Ok(metadata) if metadata.file_type().is_dir() => {
38 #[cfg(unix)]
39 return establish_existing_directory(&path);
40 #[cfg(not(unix))]
41 {
42 establish_existing_directory(&path);
43 return Ok(());
44 }
45 }
46 Ok(_) => {
47 return Err(io::Error::new(
48 io::ErrorKind::AlreadyExists,
49 format!("{} exists and is not a directory", path.display()),
50 ));
51 }
52 Err(error) if error.kind() == io::ErrorKind::NotFound => {}
53 Err(error) => return Err(error),
54 }
55
56 let mut ancestor = path.as_path();
57 loop {
58 ancestor = ancestor.parent().ok_or_else(|| {
59 io::Error::new(
60 io::ErrorKind::NotFound,
61 "durable directory has no existing ancestor",
62 )
63 })?;
64 match std::fs::symlink_metadata(ancestor) {
65 Ok(metadata) if metadata.file_type().is_dir() => break,
66 Ok(_) => {
67 return Err(io::Error::new(
68 io::ErrorKind::AlreadyExists,
69 format!("{} exists and is not a directory", ancestor.display()),
70 ));
71 }
72 Err(error) if error.kind() == io::ErrorKind::NotFound => {}
73 Err(error) => return Err(error),
74 }
75 }
76
77 let relative = path.strip_prefix(ancestor).map_err(|_| {
78 io::Error::new(
79 io::ErrorKind::InvalidInput,
80 "durable directory is outside its existing ancestor",
81 )
82 })?;
83 let mut current = ancestor.to_path_buf();
84 for component in relative.components() {
85 let std::path::Component::Normal(component) = component else {
86 return Err(io::Error::new(
87 io::ErrorKind::InvalidInput,
88 "durable directory contains a non-normal path component",
89 ));
90 };
91 let destination = current.join(component);
92 publish_directory_component(¤t, &destination)?;
93 current = destination;
94 }
95 Ok(())
96}
97
98#[cfg(unix)]
99fn establish_existing_directory(path: &Path) -> io::Result<()> {
100 sync_directory(path)?;
101 if let Some(parent) = path.parent() {
102 sync_directory(parent)?;
103 }
104 Ok(())
105}
106
107#[cfg(not(unix))]
108fn establish_existing_directory(_path: &Path) {}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub enum DurableRenameMode {
113 NoReplace,
115 Replace,
117}
118
119pub fn durable_rename(
128 source: &Path,
129 destination: &Path,
130 mode: DurableRenameMode,
131) -> io::Result<()> {
132 let source_parent = source.parent().unwrap_or_else(|| Path::new("."));
133 let destination_parent = destination.parent().unwrap_or_else(|| Path::new("."));
134 if source_parent != destination_parent {
135 return Err(io::Error::new(
136 io::ErrorKind::InvalidInput,
137 "durable rename requires source and destination in the same directory",
138 ));
139 }
140
141 durable_rename_platform(source, destination, destination_parent, mode)
142}
143
144#[cfg(unix)]
145fn publish_directory_component(parent: &Path, destination: &Path) -> io::Result<()> {
146 match std::fs::create_dir(destination) {
147 Ok(()) => {}
148 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
149 let metadata = std::fs::symlink_metadata(destination)?;
150 if !metadata.file_type().is_dir() {
151 return Err(error);
152 }
153 }
154 Err(error) => return Err(error),
155 }
156 sync_directory(destination)?;
157 sync_directory(parent)
158}
159
160#[cfg(windows)]
161fn publish_directory_component(parent: &Path, destination: &Path) -> io::Result<()> {
162 let temporary = loop {
163 let candidate = parent.join(format!(
164 ".laminardb-directory#{}",
165 uuid::Uuid::new_v4().as_u128()
166 ));
167 match std::fs::create_dir(&candidate) {
168 Ok(()) => break candidate,
169 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
170 Err(error) => return Err(error),
171 }
172 };
173 let cleanup = TemporaryDirectory(temporary.clone());
174 match durable_rename(&temporary, destination, DurableRenameMode::NoReplace) {
175 Ok(()) => {}
176 Err(error) => match std::fs::symlink_metadata(destination) {
177 Ok(metadata) if metadata.file_type().is_dir() => {}
178 _ => {
179 return Err(error);
180 }
181 },
182 }
183 drop(cleanup);
184 Ok(())
185}
186
187#[cfg(windows)]
188struct TemporaryDirectory(std::path::PathBuf);
189
190#[cfg(windows)]
191impl Drop for TemporaryDirectory {
192 fn drop(&mut self) {
193 let _ = std::fs::remove_dir(&self.0);
194 }
195}
196
197#[cfg(not(any(unix, windows)))]
198fn publish_directory_component(_parent: &Path, _destination: &Path) -> io::Result<()> {
199 Err(io::Error::new(
200 io::ErrorKind::Unsupported,
201 "no proven crash-durable directory publication primitive for this platform",
202 ))
203}
204
205#[cfg(unix)]
206fn durable_rename_platform(
207 source: &Path,
208 destination: &Path,
209 parent: &Path,
210 mode: DurableRenameMode,
211) -> io::Result<()> {
212 match mode {
213 DurableRenameMode::Replace => {
214 std::fs::rename(source, destination)?;
215 sync_directory(parent)
216 }
217 DurableRenameMode::NoReplace => {
218 std::fs::hard_link(source, destination)?;
223 sync_directory(parent)?;
224 std::fs::remove_file(source)?;
225 sync_directory(parent)
226 }
227 }
228}
229
230#[cfg(unix)]
231pub(crate) fn sync_directory(parent: &Path) -> io::Result<()> {
232 std::fs::File::open(parent)?.sync_all()
233}
234
235#[cfg(windows)]
236fn durable_rename_platform(
237 source: &Path,
238 destination: &Path,
239 _parent: &Path,
240 mode: DurableRenameMode,
241) -> io::Result<()> {
242 use std::os::windows::ffi::OsStrExt;
243 use windows_sys::Win32::Storage::FileSystem::{
244 MoveFileExW, MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH,
245 };
246
247 let source_wide: Vec<u16> = source.as_os_str().encode_wide().chain(Some(0)).collect();
248 let destination_wide: Vec<u16> = destination
249 .as_os_str()
250 .encode_wide()
251 .chain(Some(0))
252 .collect();
253 let mut flags = MOVEFILE_WRITE_THROUGH;
254 if mode == DurableRenameMode::Replace {
255 flags |= MOVEFILE_REPLACE_EXISTING;
256 }
257
258 let moved = unsafe { MoveFileExW(source_wide.as_ptr(), destination_wide.as_ptr(), flags) };
260 if moved == 0 {
261 Err(io::Error::last_os_error())
262 } else {
263 Ok(())
264 }
265}
266
267#[cfg(not(any(unix, windows)))]
268fn durable_rename_platform(
269 _source: &Path,
270 _destination: &Path,
271 _parent: &Path,
272 _mode: DurableRenameMode,
273) -> io::Result<()> {
274 Err(io::Error::new(
275 io::ErrorKind::Unsupported,
276 "no proven crash-durable rename primitive for this platform",
277 ))
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
285 fn no_replace_publishes_and_refuses_existing_destination() {
286 let directory = tempfile::tempdir().unwrap();
287 let source = directory.path().join("source.tmp");
288 let destination = directory.path().join("final");
289 std::fs::write(&source, b"first").unwrap();
290 durable_rename(&source, &destination, DurableRenameMode::NoReplace).unwrap();
291 assert_eq!(std::fs::read(&destination).unwrap(), b"first");
292 assert!(!source.exists());
293
294 std::fs::write(&source, b"second").unwrap();
295 assert!(durable_rename(&source, &destination, DurableRenameMode::NoReplace).is_err());
296 assert_eq!(std::fs::read(&destination).unwrap(), b"first");
297 assert!(source.exists());
298 }
299
300 #[test]
301 fn replace_publishes_new_bytes() {
302 let directory = tempfile::tempdir().unwrap();
303 let source = directory.path().join("source.tmp");
304 let destination = directory.path().join("final");
305 std::fs::write(&destination, b"old").unwrap();
306 std::fs::write(&source, b"new").unwrap();
307 durable_rename(&source, &destination, DurableRenameMode::Replace).unwrap();
308 assert_eq!(std::fs::read(&destination).unwrap(), b"new");
309 assert!(!source.exists());
310 }
311
312 #[test]
313 fn creates_nested_durable_directories_and_is_idempotent() {
314 let directory = tempfile::tempdir().unwrap();
315 let nested = directory.path().join("one").join("two").join("three");
316 ensure_durable_directory(&nested).unwrap();
317 assert!(nested.is_dir());
318 ensure_durable_directory(&nested).unwrap();
319 }
320
321 #[test]
322 fn rejects_a_file_in_the_directory_path() {
323 let directory = tempfile::tempdir().unwrap();
324 let file = directory.path().join("file");
325 std::fs::write(&file, b"not a directory").unwrap();
326 let error = ensure_durable_directory(&file.join("child")).unwrap_err();
327 assert!(
328 matches!(
329 error.kind(),
330 io::ErrorKind::AlreadyExists | io::ErrorKind::NotADirectory
331 ),
332 "unexpected error for a file in the directory path: {error}"
333 );
334 }
335
336 #[cfg(windows)]
337 #[test]
338 fn directory_publication_accepts_a_competing_real_directory() {
339 let directory = tempfile::tempdir().unwrap();
340 let destination = directory.path().join("winner");
341 std::fs::create_dir(&destination).unwrap();
342
343 publish_directory_component(directory.path(), &destination).unwrap();
344
345 assert!(destination.is_dir());
346 assert_eq!(std::fs::read_dir(directory.path()).unwrap().count(), 1);
347 }
348}