atomic_file_install/
lib.rs

1//! Atomically install a regular file or a symlink to destination,
2//! can be either noclobber (fail if destination already exists) or
3//! replacing it atomically if it exists.
4
5use std::{fs, io, path::Path};
6
7use reflink_copy::reflink_or_copy;
8use tempfile::{NamedTempFile, TempPath};
9use tracing::{debug, warn};
10
11#[cfg(unix)]
12use std::os::unix::fs::symlink as symlink_file_inner;
13
14#[cfg(windows)]
15use std::os::windows::fs::symlink_file as symlink_file_inner;
16
17fn parent(p: &Path) -> io::Result<&Path> {
18    p.parent().ok_or_else(|| {
19        io::Error::new(
20            io::ErrorKind::InvalidData,
21            format!("`{}` does not have a parent", p.display()),
22        )
23    })
24}
25
26fn copy_to_tempfile(src: &Path, dst: &Path) -> io::Result<NamedTempFile> {
27    let parent = parent(dst)?;
28    debug!("Creating named tempfile at '{}'", parent.display());
29    let tempfile = NamedTempFile::new_in(parent)?;
30
31    debug!(
32        "Copying from '{}' to '{}'",
33        src.display(),
34        tempfile.path().display()
35    );
36    fs::remove_file(tempfile.path())?;
37    // src and dst is likely to be on the same filesystem.
38    // Uses reflink if the fs support it, or fallback to
39    // `fs::copy` if it doesn't support it or it is not on the
40    // same filesystem.
41    reflink_or_copy(src, tempfile.path())?;
42
43    debug!("Retrieving permissions of '{}'", src.display());
44    let permissions = src.metadata()?.permissions();
45
46    debug!(
47        "Setting permissions of '{}' to '{permissions:#?}'",
48        tempfile.path().display()
49    );
50    tempfile.as_file().set_permissions(permissions)?;
51
52    Ok(tempfile)
53}
54
55/// Install a file, this fails if the `dst` already exists.
56///
57/// This is a blocking function, must be called in `block_in_place` mode.
58pub fn atomic_install_noclobber(src: &Path, dst: &Path) -> io::Result<()> {
59    debug!(
60        "Attempting to rename from '{}' to '{}'.",
61        src.display(),
62        dst.display()
63    );
64
65    let tempfile = copy_to_tempfile(src, dst)?;
66
67    debug!(
68        "Persisting '{}' to '{}', fail if dst already exists",
69        tempfile.path().display(),
70        dst.display()
71    );
72    tempfile.persist_noclobber(dst)?;
73
74    Ok(())
75}
76
77/// Atomically install a file, this atomically replace `dst` if it exists.
78///
79/// This is a blocking function, must be called in `block_in_place` mode.
80pub fn atomic_install(src: &Path, dst: &Path) -> io::Result<()> {
81    debug!(
82        "Attempting to atomically rename from '{}' to '{}'",
83        src.display(),
84        dst.display()
85    );
86
87    if let Err(err) = fs::rename(src, dst) {
88        warn!("Attempting at atomic rename failed: {err}, fallback to other methods.");
89
90        #[cfg(windows)]
91        {
92            match win::replace_file(src, dst) {
93                Ok(()) => {
94                    debug!("ReplaceFileW succeeded.");
95                    return Ok(());
96                }
97                Err(err) => {
98                    warn!("ReplaceFileW failed: {err}, fallback to using tempfile plus rename")
99                }
100            }
101        }
102
103        // src and dst is not on the same filesystem/mountpoint.
104        // Fallback to creating NamedTempFile on the parent dir of
105        // dst.
106
107        persist(copy_to_tempfile(src, dst)?.into_temp_path(), dst)?;
108    } else {
109        debug!("Attempting at atomically succeeded.");
110    }
111
112    Ok(())
113}
114
115/// Create a symlink at `link` to `dest`, this fails if the `link`
116/// already exists.
117///
118/// This is a blocking function, must be called in `block_in_place` mode.
119pub fn atomic_symlink_file_noclobber(dest: &Path, link: &Path) -> io::Result<()> {
120    match symlink_file_inner(dest, link) {
121        Ok(_) => Ok(()),
122
123        #[cfg(windows)]
124        // Symlinks on Windows are disabled in some editions, so creating one is unreliable.
125        // Fallback to copy if it fails.
126        Err(_) => atomic_install_noclobber(dest, link),
127
128        #[cfg(not(windows))]
129        Err(err) => Err(err),
130    }
131}
132
133/// Atomically create a symlink at `link` to `dest`, this atomically replace
134/// `link` if it already exists.
135///
136/// This is a blocking function, must be called in `block_in_place` mode.
137pub fn atomic_symlink_file(dest: &Path, link: &Path) -> io::Result<()> {
138    let parent = parent(link)?;
139
140    debug!("Creating tempPath at '{}'", parent.display());
141    let temp_path = NamedTempFile::new_in(parent)?.into_temp_path();
142    // Remove this file so that we can create a symlink
143    // with the name.
144    fs::remove_file(&temp_path)?;
145
146    debug!(
147        "Creating symlink '{}' to file '{}'",
148        temp_path.display(),
149        dest.display()
150    );
151
152    match symlink_file_inner(dest, &temp_path) {
153        Ok(_) => persist(temp_path, link),
154
155        #[cfg(windows)]
156        // Symlinks on Windows are disabled in some editions, so creating one is unreliable.
157        // Fallback to copy if it fails.
158        Err(_) => atomic_install(dest, link),
159
160        #[cfg(not(windows))]
161        Err(err) => Err(err),
162    }
163}
164
165fn persist(temp_path: TempPath, to: &Path) -> io::Result<()> {
166    debug!("Persisting '{}' to '{}'", temp_path.display(), to.display());
167    match temp_path.persist(to) {
168        Ok(()) => Ok(()),
169        #[cfg(windows)]
170        Err(tempfile::PathPersistError {
171            error,
172            path: temp_path,
173        }) => {
174            warn!(
175                "Failed to persist symlink '{}' to '{}': {error}, fallback to ReplaceFileW",
176                temp_path.display(),
177                to.display(),
178            );
179            win::replace_file(&temp_path, to).map_err(io::Error::from)
180        }
181        #[cfg(not(windows))]
182        Err(err) => Err(err.into()),
183    }
184}
185
186#[cfg(windows)]
187mod win {
188    use std::{os::windows::ffi::OsStrExt, path::Path};
189
190    use windows::{
191        core::{Error, PCWSTR},
192        Win32::Storage::FileSystem::{ReplaceFileW, REPLACE_FILE_FLAGS},
193    };
194
195    pub(super) fn replace_file(src: &Path, dst: &Path) -> Result<(), Error> {
196        let mut src: Vec<_> = src.as_os_str().encode_wide().collect();
197        let mut dst: Vec<_> = dst.as_os_str().encode_wide().collect();
198
199        // Ensure it is terminated with 0
200        src.push(0);
201        dst.push(0);
202
203        // SAFETY: We use it according its doc
204        // https://learn.microsoft.com/en-nz/windows/win32/api/winbase/nf-winbase-replacefilew
205        //
206        // NOTE that this function is available since windows XP, so we don't need to
207        // lazily load this function.
208        unsafe {
209            ReplaceFileW(
210                PCWSTR::from_raw(dst.as_ptr()), // lpreplacedfilename
211                PCWSTR::from_raw(src.as_ptr()), // lpreplacementfilename
212                PCWSTR::null(),                 // lpbackupfilename, null for no backup file
213                REPLACE_FILE_FLAGS(0),          // dwreplaceflags
214                None,                           // lpexclude, unused
215                None,                           // lpreserved, unused
216            )
217        }
218    }
219}