From fb6f429169fdc36e8a1512284a88a19622f91e50 Mon Sep 17 00:00:00 2001 From: Kim Altintop Date: Fri, 24 Apr 2026 10:58:46 +0200 Subject: [PATCH 1/6] paths: fsync after writing to a file As reported in #4886, `metadata.toml` can get get lost if the server crashes at an unfortunate point in time. Do a textbook fsync to mitigate that. While not necessarily preventing the server from starting, the same is advisable for any of the files, so the logic is put in the macro- generated `write` method of typed paths. --- crates/paths/src/utils.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/paths/src/utils.rs b/crates/paths/src/utils.rs index a03a9b87d9c..b5870e26e9f 100644 --- a/crates/paths/src/utils.rs +++ b/crates/paths/src/utils.rs @@ -104,8 +104,21 @@ macro_rules! path_type { } pub fn write(&self, contents: impl AsRef<[u8]>) -> std::io::Result<()> { + use std::io::Write as _; + self.create_parent()?; - std::fs::write(self, contents) + let mut file = std::fs::File::create(self)?; + file.write_all(contents.as_ref())?; + file.sync_all()?; + + // In case the file got created, we also need to fsync the + // directory, so that the directory entry becomes durable. + if let Some(parent) = self.0.parent() + && parent != std::path::Path::new("") { + std::fs::File::open(parent)?.sync_all()?; + } + + Ok(()) } /// Opens a file at this path with the given options, ensuring its parent directory exists. From 4ce5a077bde9350af99ae0ac58c0afef62a81f76 Mon Sep 17 00:00:00 2001 From: Kim Altintop Date: Tue, 28 Apr 2026 12:56:52 +0200 Subject: [PATCH 2/6] Make path type `write` replace the file atomically --- crates/paths/Cargo.toml | 4 +--- crates/paths/src/utils.rs | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/paths/Cargo.toml b/crates/paths/Cargo.toml index c293807a35e..49dd1bbdece 100644 --- a/crates/paths/Cargo.toml +++ b/crates/paths/Cargo.toml @@ -12,6 +12,7 @@ chrono = { workspace = true, features = ["now"] } fs2.workspace = true itoa.workspace = true serde.workspace = true +tempfile.workspace = true thiserror.workspace = true [target.'cfg(windows)'.dependencies] @@ -21,8 +22,5 @@ junction.workspace = true [target.'cfg(not(windows))'.dependencies] xdg.workspace = true -[dev-dependencies] -tempfile.workspace = true - [lints] workspace = true diff --git a/crates/paths/src/utils.rs b/crates/paths/src/utils.rs index b5870e26e9f..326ba795775 100644 --- a/crates/paths/src/utils.rs +++ b/crates/paths/src/utils.rs @@ -106,17 +106,19 @@ macro_rules! path_type { pub fn write(&self, contents: impl AsRef<[u8]>) -> std::io::Result<()> { use std::io::Write as _; - self.create_parent()?; - let mut file = std::fs::File::create(self)?; - file.write_all(contents.as_ref())?; - file.sync_all()?; + let path = self.0.canonicalize()?; + let parent = path.parent().ok_or_else(|| + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("cannot replace {} without enclosing directory", path.display())) + )?; + std::fs::create_dir_all(&path)?; - // In case the file got created, we also need to fsync the - // directory, so that the directory entry becomes durable. - if let Some(parent) = self.0.parent() - && parent != std::path::Path::new("") { - std::fs::File::open(parent)?.sync_all()?; - } + let mut tmp = tempfile::NamedTempFile::new_in(parent)?; + tmp.write_all(contents.as_ref())?; + tmp.as_file().sync_all()?; + tmp.persist(&path)?; + std::fs::File::open(parent)?.sync_all()?; Ok(()) } From 9e87feac2d5bf2f29872ee198f74ad13774c7f02 Mon Sep 17 00:00:00 2001 From: Kim Altintop Date: Tue, 28 Apr 2026 14:53:13 +0200 Subject: [PATCH 3/6] fixup! Make path type `write` replace the file atomically --- crates/paths/src/utils.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/paths/src/utils.rs b/crates/paths/src/utils.rs index 326ba795775..a8e629f1cf7 100644 --- a/crates/paths/src/utils.rs +++ b/crates/paths/src/utils.rs @@ -106,13 +106,13 @@ macro_rules! path_type { pub fn write(&self, contents: impl AsRef<[u8]>) -> std::io::Result<()> { use std::io::Write as _; - let path = self.0.canonicalize()?; + let path = &self.0; let parent = path.parent().ok_or_else(|| std::io::Error::new( std::io::ErrorKind::InvalidInput, format!("cannot replace {} without enclosing directory", path.display())) )?; - std::fs::create_dir_all(&path)?; + std::fs::create_dir_all(&parent)?; let mut tmp = tempfile::NamedTempFile::new_in(parent)?; tmp.write_all(contents.as_ref())?; From 253e46bab4c9335e4106a7100b7fa360d5fd03df Mon Sep 17 00:00:00 2001 From: Kim Altintop Date: Thu, 30 Apr 2026 07:35:01 +0200 Subject: [PATCH 4/6] Macro Hygiene <3 --- crates/paths/src/lib.rs | 2 ++ crates/paths/src/utils.rs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/paths/src/lib.rs b/crates/paths/src/lib.rs index 3ae55eed685..84eb4a8bb80 100644 --- a/crates/paths/src/lib.rs +++ b/crates/paths/src/lib.rs @@ -162,6 +162,8 @@ mod utils; #[doc(hidden)] pub use serde as __serde; +#[doc(hidden)] +pub use tempfile as __tempfile; /// Implemented for path types. Use `from_path_unchecked()` to construct a strongly-typed /// path directly from a `PathBuf`. diff --git a/crates/paths/src/utils.rs b/crates/paths/src/utils.rs index a8e629f1cf7..54a2b22f29b 100644 --- a/crates/paths/src/utils.rs +++ b/crates/paths/src/utils.rs @@ -114,7 +114,7 @@ macro_rules! path_type { )?; std::fs::create_dir_all(&parent)?; - let mut tmp = tempfile::NamedTempFile::new_in(parent)?; + let mut tmp = $crate::__tempfile::NamedTempFile::new_in(parent)?; tmp.write_all(contents.as_ref())?; tmp.as_file().sync_all()?; tmp.persist(&path)?; From 7f55d664df1de84423799617ce4382a78bc8c5d2 Mon Sep 17 00:00:00 2001 From: Kim Altintop Date: Thu, 30 Apr 2026 16:45:33 +0200 Subject: [PATCH 5/6] Apparently, a directory can't be synced on windows --- crates/paths/src/utils.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/paths/src/utils.rs b/crates/paths/src/utils.rs index 54a2b22f29b..29c13c33a2e 100644 --- a/crates/paths/src/utils.rs +++ b/crates/paths/src/utils.rs @@ -118,6 +118,7 @@ macro_rules! path_type { tmp.write_all(contents.as_ref())?; tmp.as_file().sync_all()?; tmp.persist(&path)?; + #[cfg(not(target_os = "windows"))] std::fs::File::open(parent)?.sync_all()?; Ok(()) From db10ec71faaa5122732230a891e3ece9692f7b7d Mon Sep 17 00:00:00 2001 From: Kim Altintop Date: Thu, 30 Apr 2026 17:34:19 +0200 Subject: [PATCH 6/6] Comment on windows --- crates/paths/src/utils.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/paths/src/utils.rs b/crates/paths/src/utils.rs index 29c13c33a2e..e62702765cc 100644 --- a/crates/paths/src/utils.rs +++ b/crates/paths/src/utils.rs @@ -118,6 +118,7 @@ macro_rules! path_type { tmp.write_all(contents.as_ref())?; tmp.as_file().sync_all()?; tmp.persist(&path)?; + // On Windows, syncing the directory is not necessary and doesn't even work. #[cfg(not(target_os = "windows"))] std::fs::File::open(parent)?.sync_all()?;