Skip to main content

nexus_memory_core/
fsutil.rs

1//! Filesystem utilities shared across crates.
2
3use std::io::{ErrorKind, Write};
4use std::path::{Path, PathBuf};
5
6/// Write a file atomically: write to a temp file, sync, then rename.
7/// Prevents partial writes on crash. Uses PID-scoped tmp to avoid
8/// collision when concurrent processes write the same target.
9pub 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            // Some mounted filesystems do not replace an existing destination
25            // during rename. Fall back to a remove-and-replace flow with a
26            // per-write backup so we never touch a user-owned sibling file.
27            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}