winbrew_core\fs/
write.rs

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
11/// Writes `contents` to `path` through `temp_path` and publishes the result atomically.
12///
13/// The temp file is synced before rename, so callers either see the
14/// old file or the fully-written new file. The temp file is removed on failure.
15pub 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        // atomic_write owns the temp path, so it cleans up here even though
28        // finalize_temp_file leaves cleanup to direct callers on failure.
29        let _ = fs::remove_file(temp_path);
30        return Err(err);
31    }
32
33    Ok(())
34}
35
36/// Writes `contents` to a PID-scoped TOML temp file and atomically publishes it.
37///
38/// This is useful when the caller wants a predictable temporary name per
39/// process and does not need to manage the temp file path directly.
40pub 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
45/// Replaces `final_path` with `temp_path`, removing any existing target first.
46///
47/// If this helper is called directly and the rename fails, the caller remains
48/// responsible for cleaning up `temp_path`.
49pub 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}