opencode_ralph_loop_cli/fs_atomic/
mod.rs1use 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}