nap_core/merge/
atomic_write.rs1use std::io::Write;
9use std::path::{Path, PathBuf};
10
11#[derive(Debug, thiserror::Error)]
13pub enum AtomicWriteError {
14 #[error("failed to create temp file: {0}")]
15 CreateTemp(std::io::Error),
16
17 #[error("failed to write content: {0}")]
18 Write(std::io::Error),
19
20 #[error("failed to flush: {0}")]
21 Flush(std::io::Error),
22
23 #[error("failed to fsync: {0}")]
24 Fsync(std::io::Error),
25
26 #[error("failed to rename: {0}")]
27 Rename(std::io::Error),
28}
29
30pub fn atomic_write(path: &Path, content: &[u8]) -> Result<(), AtomicWriteError> {
43 let parent = path.parent().unwrap_or_else(|| Path::new("."));
45 let file_stem = path
46 .file_stem()
47 .and_then(|s| s.to_str())
48 .unwrap_or("nap_tmp");
49 let extension = path.extension().and_then(|s| s.to_str()).unwrap_or("tmp");
50
51 let temp_path = loop {
52 let rand_suffix: u64 = rand::random();
53 let candidate = parent.join(format!(".{file_stem}_{rand_suffix:016x}.{extension}"));
54 if !candidate.exists() {
55 break candidate;
56 }
57 };
58
59 let result = try_atomic_write(path, &temp_path, content);
60
61 if result.is_err() {
63 let _ = std::fs::remove_file(&temp_path);
64 }
65
66 result
67}
68
69fn try_atomic_write(
70 target: &Path,
71 temp_path: &PathBuf,
72 content: &[u8],
73) -> Result<(), AtomicWriteError> {
74 if let Some(parent) = target.parent() {
76 std::fs::create_dir_all(parent).map_err(AtomicWriteError::CreateTemp)?;
77 }
78
79 let mut file = std::fs::File::create(temp_path).map_err(AtomicWriteError::CreateTemp)?;
81 file.write_all(content).map_err(AtomicWriteError::Write)?;
82 file.flush().map_err(AtomicWriteError::Flush)?;
83 file.sync_all().map_err(AtomicWriteError::Fsync)?;
84 drop(file); std::fs::rename(temp_path, target).map_err(AtomicWriteError::Rename)?;
88
89 if let Some(parent) = target.parent()
91 && let Ok(dir) = std::fs::File::open(parent)
92 {
93 let _ = dir.sync_all();
94 }
95
96 Ok(())
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use std::io::Read;
103
104 #[test]
105 fn test_atomic_write_and_read() {
106 let dir = tempfile::tempdir().unwrap();
107 let path = dir.path().join("test.yaml");
108
109 let content = b"key: value\nname: Luke\n";
110 atomic_write(&path, content).unwrap();
111
112 let mut file = std::fs::File::open(&path).unwrap();
113 let mut buf = Vec::new();
114 file.read_to_end(&mut buf).unwrap();
115 assert_eq!(buf, content);
116 }
117
118 #[test]
119 fn test_atomic_write_no_temp_file_left() {
120 let dir = tempfile::tempdir().unwrap();
121 let path = dir.path().join("output.yaml");
122
123 atomic_write(&path, b"hello").unwrap();
124
125 let entries: Vec<_> = std::fs::read_dir(dir.path())
127 .unwrap()
128 .filter_map(|e| e.ok())
129 .map(|e| e.file_name())
130 .collect();
131 assert_eq!(entries.len(), 1);
132 assert_eq!(entries[0], "output.yaml");
133 }
134
135 #[test]
136 fn test_atomic_write_content_is_identical() {
137 let dir = tempfile::tempdir().unwrap();
138 let path = dir.path().join("manifest.yaml");
139
140 let content = b"id: nap://test/char/luke\nname: Luke Skywalker\nversion: 1\n";
141 atomic_write(&path, content).unwrap();
142
143 let read_back = std::fs::read(&path).unwrap();
144 assert_eq!(read_back, content);
145 }
146
147 #[test]
148 fn test_atomic_write_creates_parent_dirs() {
149 let dir = tempfile::tempdir().unwrap();
150 let path = dir.path().join("nested/sub/dir/manifest.yaml");
151
152 atomic_write(&path, b"hello").unwrap();
153 assert!(path.exists());
154 }
155}