1use std::fs;
10use std::io::Write as _;
11use std::path::Path;
12
13pub fn write(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
21 let parent = path.parent().filter(|p| !p.as_os_str().is_empty());
22 if let Some(parent) = parent {
23 fs::create_dir_all(parent)?;
24 }
25 let name = path
26 .file_name()
27 .ok_or_else(|| std::io::Error::other(format!("no file name in {}", path.display())))?;
28 let mut tmp_name = std::ffi::OsString::from(format!(".{}", std::process::id()));
29 tmp_name.push(".rk-tmp.");
30 tmp_name.push(name);
31 let tmp = path.with_file_name(tmp_name);
32 let written = fs::File::create(&tmp)
33 .and_then(|mut file| file.write_all(bytes).and_then(|()| file.sync_all()))
34 .and_then(|()| fs::rename(&tmp, path));
35 if written.is_err() {
36 let _ = fs::remove_file(&tmp);
37 }
38 written
39}
40
41#[cfg(test)]
42mod tests {
43 #![allow(clippy::expect_used)]
44
45 use super::write;
46
47 #[test]
48 fn a_write_creates_parents_lands_whole_and_leaves_no_temp() {
49 let dir = tempfile::tempdir().expect("a scratch dir exists");
50 let path = dir.path().join("deep/nested/file.txt");
51 write(&path, b"first").expect("the write lands");
52 assert_eq!(std::fs::read(&path).expect("the file reads"), b"first");
53 write(&path, b"second").expect("the overwrite lands");
54 assert_eq!(std::fs::read(&path).expect("the file reads"), b"second");
55 let leftovers: Vec<_> = std::fs::read_dir(path.parent().expect("a parent"))
56 .expect("the dir reads")
57 .map(|entry| entry.expect("an entry").file_name())
58 .filter(|name| name != "file.txt")
59 .collect();
60 assert!(
61 leftovers.is_empty(),
62 "temp files left behind: {leftovers:?}"
63 );
64 }
65
66 #[test]
67 fn a_failed_write_leaves_the_destination_alone() {
68 let dir = tempfile::tempdir().expect("a scratch dir exists");
69 let path = dir.path().join("blocked");
71 std::fs::create_dir(&path).expect("the blocking dir creates");
72 assert!(write(&path, b"bytes").is_err());
73 assert!(path.is_dir(), "the destination must be untouched");
74 }
75}