1use super::FsError;
2use super::cleanup::cleanup_path;
3use std::fs;
4use std::io::ErrorKind;
5use std::io::Write;
6use std::path::Path;
7use std::process;
8
9type BoxedResult<T> = std::result::Result<T, Box<FsError>>;
10
11pub fn atomic_write(path: &Path, temp_path: &Path, contents: &[u8]) -> BoxedResult<()> {
16 if let Some(parent) = path.parent() {
17 fs::create_dir_all(parent)
18 .map_err(|err| Box::new(FsError::create_directory(parent, err)))?;
19 }
20
21 if let Err(err) = write_temp_contents(temp_path, contents) {
22 let _ = fs::remove_file(temp_path);
23 return Err(err);
24 }
25
26 if let Err(err) = finalize_temp_file(temp_path, path) {
27 let _ = fs::remove_file(temp_path);
30 return Err(err);
31 }
32
33 Ok(())
34}
35
36pub fn atomic_write_toml_temp(path: &Path, contents: &str) -> BoxedResult<()> {
41 let temp_path = path.with_extension(format!("toml.{}.tmp", process::id()));
42 atomic_write(path, &temp_path, contents.as_bytes())
43}
44
45pub fn finalize_temp_file(temp_path: &Path, final_path: &Path) -> BoxedResult<()> {
50 match fs::rename(temp_path, final_path) {
51 Ok(()) => Ok(()),
52 Err(err) if is_target_conflict_error(&err) => {
53 cleanup_path(final_path)?;
54
55 fs::rename(temp_path, final_path)
56 .map_err(|err| Box::new(FsError::finalize_file(temp_path, final_path, err)))
57 }
58 Err(err) => Err(Box::new(FsError::finalize_file(temp_path, final_path, err))),
59 }
60}
61
62fn write_temp_contents(temp_path: &Path, contents: &[u8]) -> BoxedResult<()> {
63 let mut file = fs::File::create(temp_path)
64 .map_err(|err| Box::new(FsError::create_temp_file(temp_path, err)))?;
65 file.write_all(contents)
66 .map_err(|err| Box::new(FsError::write_temp_file(temp_path, err)))?;
67 file.sync_all()
68 .map_err(|err| Box::new(FsError::sync_temp_file(temp_path, err)))?;
69
70 Ok(())
71}
72
73fn is_target_conflict_error(err: &std::io::Error) -> bool {
74 matches!(
75 err.kind(),
76 ErrorKind::AlreadyExists | ErrorKind::PermissionDenied | ErrorKind::IsADirectory
77 )
78}
79
80#[cfg(test)]
81mod tests {
82 use super::{atomic_write, finalize_temp_file};
83 use std::fs;
84 use tempfile::tempdir;
85
86 #[test]
87 fn atomic_write_produces_correct_content() {
88 let temp_dir = tempdir().expect("temp dir");
89 let path = temp_dir.path().join("config.toml");
90 let temp_path = temp_dir.path().join("config.toml.tmp");
91
92 atomic_write(&path, &temp_path, b"name=winbrew").expect("atomic write");
93
94 assert_eq!(
95 fs::read_to_string(&path).expect("read content"),
96 "name=winbrew"
97 );
98 assert!(!temp_path.exists());
99 }
100
101 #[test]
102 fn atomic_write_replaces_existing_directory() {
103 let temp_dir = tempdir().expect("temp dir");
104 let path = temp_dir.path().join("config.toml");
105 let temp_path = temp_dir.path().join("config.toml.tmp");
106
107 fs::create_dir(&path).expect("existing final dir");
108
109 atomic_write(&path, &temp_path, b"name=winbrew").expect("atomic write");
110
111 assert_eq!(
112 fs::read_to_string(&path).expect("read content"),
113 "name=winbrew"
114 );
115 assert!(!temp_path.exists());
116 }
117
118 #[test]
119 fn finalize_temp_file_replaces_existing_directory() {
120 let temp_dir = tempdir().expect("temp dir");
121 let final_path = temp_dir.path().join("config.toml");
122 let temp_path = temp_dir.path().join("config.toml.tmp");
123
124 fs::create_dir(&final_path).expect("existing final dir");
125 fs::write(&temp_path, b"name=winbrew").expect("write temp file");
126
127 finalize_temp_file(&temp_path, &final_path).expect("finalize temp file");
128
129 assert_eq!(
130 fs::read_to_string(&final_path).expect("read content"),
131 "name=winbrew"
132 );
133 assert!(!temp_path.exists());
134 }
135}