laminar_core/durable_fs/
mod.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;