nexus_memory_core/
fsutil.rs1use std::io::{ErrorKind, Write};
4use std::path::{Path, PathBuf};
5
6pub fn atomic_write(path: &Path, content: &str) -> std::io::Result<()> {
10 let tmp_path = path.with_extension(format!(
11 "tmp.{}-{}",
12 std::process::id(),
13 uuid::Uuid::new_v4()
14 ));
15 {
16 let mut f = std::fs::File::create(&tmp_path)?;
17 f.write_all(content.as_bytes())?;
18 f.sync_all()?;
19 }
20
21 let result = match std::fs::rename(&tmp_path, path) {
22 Ok(()) => Ok(()),
23 Err(err) if err.kind() == ErrorKind::AlreadyExists => {
24 match std::fs::symlink_metadata(path) {
28 Ok(metadata) if metadata.file_type().is_dir() => Err(err),
29 Ok(_) => {
30 let backup_path = backup_path(path);
31 if let Ok(metadata) = std::fs::symlink_metadata(&backup_path) {
32 if metadata.file_type().is_dir() {
33 return Err(err);
34 }
35 std::fs::remove_file(&backup_path)?;
36 }
37
38 match std::fs::rename(path, &backup_path) {
39 Ok(()) => match std::fs::rename(&tmp_path, path) {
40 Ok(()) => {
41 let _ = std::fs::remove_file(&backup_path);
42 Ok(())
43 }
44 Err(rename_err) => match std::fs::rename(&backup_path, path) {
45 Ok(()) => Err(rename_err),
46 Err(restore_err) => Err(std::io::Error::new(
47 restore_err.kind(),
48 format!(
49 "atomic_write failed: {}; backup restore failed: {}",
50 rename_err, restore_err
51 ),
52 )),
53 },
54 },
55 Err(backup_err) => Err(backup_err),
56 }
57 }
58 Err(_) => std::fs::rename(&tmp_path, path),
59 }
60 }
61 Err(err) => Err(err),
62 };
63
64 if result.is_err() {
65 let _ = std::fs::remove_file(&tmp_path);
66 }
67
68 result
69}
70
71fn backup_path(path: &Path) -> PathBuf {
72 path.with_extension(format!(
73 "bak.{}-{}",
74 std::process::id(),
75 uuid::Uuid::new_v4()
76 ))
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn atomic_write_replaces_existing_file() {
85 let dir = tempfile::tempdir().unwrap();
86 let path = dir.path().join("content.md");
87
88 std::fs::write(&path, "old").unwrap();
89 atomic_write(&path, "new").unwrap();
90
91 assert_eq!(std::fs::read_to_string(&path).unwrap(), "new");
92 }
93}