Skip to main content

opencode_ralph_loop_cli/fs_atomic/
mod.rs

1use std::fs::{self, File};
2use std::io::Write;
3use std::path::Path;
4
5use crate::error::CliError;
6
7pub fn write_atomic(dest: &Path, content: &[u8]) -> Result<(), CliError> {
8    let parent = dest.parent().ok_or_else(|| {
9        CliError::io(
10            dest.to_string_lossy().into_owned(),
11            std::io::Error::new(
12                std::io::ErrorKind::InvalidInput,
13                "path has no parent directory",
14            ),
15        )
16    })?;
17
18    fs::create_dir_all(parent)
19        .map_err(|e| CliError::io(parent.to_string_lossy().into_owned(), e))?;
20
21    let tmp_path = dest.with_extension("tmp");
22
23    let write_result = (|| -> std::io::Result<()> {
24        let mut file = File::create(&tmp_path)?;
25        file.write_all(content)?;
26        file.flush()?;
27        file.sync_all()?;
28        Ok(())
29    })();
30
31    if let Err(e) = write_result {
32        let _ = fs::remove_file(&tmp_path);
33        return Err(CliError::io(tmp_path.to_string_lossy().into_owned(), e));
34    }
35
36    fs::rename(&tmp_path, dest)
37        .map_err(|e| CliError::io(dest.to_string_lossy().into_owned(), e))?;
38
39    Ok(())
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45    use tempfile::TempDir;
46
47    #[test]
48    fn atomic_write_creates_file() {
49        let dir = TempDir::new().unwrap();
50        let dest = dir.path().join("file.txt");
51        write_atomic(&dest, b"test content").unwrap();
52        let read_back = std::fs::read(&dest).unwrap();
53        assert_eq!(read_back, b"test content");
54    }
55
56    #[test]
57    fn atomic_write_creates_directories() {
58        let dir = TempDir::new().unwrap();
59        let dest = dir.path().join("subdir").join("other").join("file.txt");
60        write_atomic(&dest, b"test").unwrap();
61        assert!(dest.exists());
62    }
63
64    #[test]
65    fn atomic_write_overwrites_existing() {
66        let dir = TempDir::new().unwrap();
67        let dest = dir.path().join("file.txt");
68        write_atomic(&dest, b"first version").unwrap();
69        write_atomic(&dest, b"second version").unwrap();
70        let read_back = std::fs::read(&dest).unwrap();
71        assert_eq!(read_back, b"second version");
72    }
73}