Skip to main content

nils_common/
fs.rs

1use std::io;
2use std::path::Path;
3
4/// Replace `to` by renaming `from` to `to`.
5///
6/// Notes:
7/// - On Unix, `rename` overwrites atomically when `from` and `to` are on the same filesystem.
8/// - On Windows, `rename` fails when `to` exists. We fall back to remove + rename, which is not
9///   atomic but matches the expected overwrite behavior for temp-file workflows.
10pub fn replace_file(from: &Path, to: &Path) -> io::Result<()> {
11    replace_file_impl(from, to)
12}
13
14/// Alias for `replace_file` (kept for readability at call sites).
15pub fn rename_overwrite(from: &Path, to: &Path) -> io::Result<()> {
16    replace_file(from, to)
17}
18
19#[cfg(unix)]
20fn replace_file_impl(from: &Path, to: &Path) -> io::Result<()> {
21    std::fs::rename(from, to)
22}
23
24#[cfg(windows)]
25fn replace_file_impl(from: &Path, to: &Path) -> io::Result<()> {
26    match std::fs::rename(from, to) {
27        Ok(()) => Ok(()),
28        Err(err) => {
29            // Be conservative: do not delete `to` unless we can confirm `from` exists.
30            if !from.exists() {
31                return Err(err);
32            }
33
34            if !to.exists() {
35                return Err(err);
36            }
37
38            match std::fs::remove_file(to) {
39                Ok(()) => {}
40                Err(remove_err) if remove_err.kind() == io::ErrorKind::NotFound => {}
41                Err(remove_err) => {
42                    return Err(io::Error::new(
43                        io::ErrorKind::Other,
44                        format!("rename failed: {err} (remove failed: {remove_err})"),
45                    ));
46                }
47            }
48
49            std::fs::rename(from, to).map_err(|err2| {
50                io::Error::new(
51                    io::ErrorKind::Other,
52                    format!("rename failed: {err} ({err2})"),
53                )
54            })
55        }
56    }
57}
58
59#[cfg(not(any(unix, windows)))]
60fn replace_file_impl(from: &Path, to: &Path) -> io::Result<()> {
61    std::fs::rename(from, to)
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use std::fs;
68    use tempfile::TempDir;
69
70    #[test]
71    fn replace_file_overwrites_existing_destination() {
72        let dir = TempDir::new().expect("tempdir");
73        let from = dir.path().join("from.tmp");
74        let to = dir.path().join("to.txt");
75
76        fs::write(&from, "new").expect("write from");
77        fs::write(&to, "old").expect("write to");
78
79        replace_file(&from, &to).expect("replace_file");
80
81        assert!(!from.exists(), "from should be moved away");
82        assert_eq!(fs::read_to_string(&to).expect("read to"), "new");
83    }
84}