atomicwrites/
lib.rs

1// INSERT_README_VIA_MAKE
2#[cfg(unix)]
3extern crate rustix;
4extern crate tempfile;
5
6use std::convert::AsRef;
7use std::error::Error as ErrorTrait;
8use std::fmt;
9use std::fs;
10use std::io;
11use std::path;
12
13pub use OverwriteBehavior::{AllowOverwrite, DisallowOverwrite};
14
15/// Whether to allow overwriting if the target file exists.
16#[derive(Clone, Copy)]
17pub enum OverwriteBehavior {
18    /// Overwrite files silently.
19    AllowOverwrite,
20
21    /// Don't overwrite files. `AtomicFile.write` will raise errors for such conditions only after
22    /// you've already written your data.
23    DisallowOverwrite,
24}
25
26/// Represents an error raised by `AtomicFile.write`.
27#[derive(Debug)]
28pub enum Error<E> {
29    /// The error originated in the library itself, while it was either creating a temporary file
30    /// or moving the file into place.
31    Internal(io::Error),
32    /// The error originated in the user-supplied callback.
33    User(E),
34}
35
36/// If your callback returns a `std::io::Error`, you can unwrap this type to `std::io::Error`.
37impl From<Error<io::Error>> for io::Error {
38    fn from(e: Error<io::Error>) -> Self {
39        match e {
40            Error::Internal(x) => x,
41            Error::User(x) => x,
42        }
43    }
44}
45
46impl<E: fmt::Display> fmt::Display for Error<E> {
47    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
48        match *self {
49            Error::Internal(ref e) => e.fmt(f),
50            Error::User(ref e) => e.fmt(f),
51        }
52    }
53}
54
55impl<E: ErrorTrait> ErrorTrait for Error<E> {
56    fn cause(&self) -> Option<&dyn ErrorTrait> {
57        match *self {
58            Error::Internal(ref e) => Some(e),
59            Error::User(ref e) => Some(e),
60        }
61    }
62}
63
64fn safe_parent(p: &path::Path) -> Option<&path::Path> {
65    match p.parent() {
66        None => None,
67        Some(x) if x.as_os_str().is_empty() => Some(path::Path::new(".")),
68        x => x,
69    }
70}
71
72/// Create a file and write to it atomically, in a callback.
73pub struct AtomicFile {
74    /// Path to the final file that is atomically written.
75    path: path::PathBuf,
76    overwrite: OverwriteBehavior,
77    /// Directory to which to write the temporary subdirectories.
78    tmpdir: path::PathBuf,
79}
80
81impl AtomicFile {
82    /// Helper for writing to the file at `path` atomically, in write-only mode.
83    ///
84    /// If `OverwriteBehaviour::DisallowOverwrite` is given,
85    /// an `Error::Internal` containing an `std::io::ErrorKind::AlreadyExists`
86    /// will be returned from `self.write(...)` if the file exists.
87    ///
88    /// The temporary file is written to a temporary subdirectory in `.`, to ensure
89    /// it’s on the same filesystem (so that the move is atomic).
90    pub fn new<P>(path: P, overwrite: OverwriteBehavior) -> Self
91    where
92        P: AsRef<path::Path>,
93    {
94        let p = path.as_ref();
95        AtomicFile::new_with_tmpdir(
96            p,
97            overwrite,
98            safe_parent(p).unwrap_or_else(|| path::Path::new(".")),
99        )
100    }
101
102    /// Like `AtomicFile::new`, but the temporary file is written to a temporary subdirectory in `tmpdir`.
103    ///
104    /// TODO: does `tmpdir` have to exist?
105    pub fn new_with_tmpdir<P, Q>(path: P, overwrite: OverwriteBehavior, tmpdir: Q) -> Self
106    where
107        P: AsRef<path::Path>,
108        Q: AsRef<path::Path>,
109    {
110        AtomicFile {
111            path: path.as_ref().to_path_buf(),
112            overwrite,
113            tmpdir: tmpdir.as_ref().to_path_buf(),
114        }
115    }
116
117    /// Move the file to `self.path()`. Only call once! Not exposed!
118    fn commit(&self, tmppath: &path::Path) -> io::Result<()> {
119        match self.overwrite {
120            AllowOverwrite => replace_atomic(tmppath, self.path()),
121            DisallowOverwrite => move_atomic(tmppath, self.path()),
122        }
123    }
124
125    /// Get the target filepath.
126    pub fn path(&self) -> &path::Path {
127        &self.path
128    }
129
130    /// Open a temporary file, call `f` on it (which is supposed to write to it), then move the
131    /// file atomically to `self.path`.
132    ///
133    /// The temporary file is written to a randomized temporary subdirectory with prefix `.atomicwrite`.
134    pub fn write<T, E, F>(&self, f: F) -> Result<T, Error<E>>
135    where
136        F: FnOnce(&mut fs::File) -> Result<T, E>,
137    {
138        let mut options = fs::OpenOptions::new();
139        // These are the same options as `File::create`.
140        options.write(true).create(true).truncate(true);
141        self.write_with_options(f, options)
142    }
143
144    /// Open a temporary file with custom [`OpenOptions`], call `f` on it (which is supposed to
145    /// write to it), then move the file atomically to `self.path`.
146    ///
147    /// The temporary file is written to a randomized temporary subdirectory with prefix
148    /// `.atomicwrite`.
149    ///
150    /// [`OpenOptions`]: fs::OpenOptions
151    pub fn write_with_options<T, E, F>(&self, f: F, options: fs::OpenOptions) -> Result<T, Error<E>>
152    where
153        F: FnOnce(&mut fs::File) -> Result<T, E>,
154    {
155        let tmpdir = tempfile::Builder::new()
156            .prefix(".atomicwrite")
157            .tempdir_in(&self.tmpdir)
158            .map_err(Error::Internal)?;
159
160        let tmppath = tmpdir.path().join("tmpfile.tmp");
161        let rv = {
162            let mut tmpfile = options.open(&tmppath).map_err(Error::Internal)?;
163            let r = f(&mut tmpfile).map_err(Error::User)?;
164            tmpfile.sync_all().map_err(Error::Internal)?;
165            r
166        };
167        self.commit(&tmppath).map_err(Error::Internal)?;
168        Ok(rv)
169    }
170}
171
172#[cfg(all(unix, not(target_os = "redox")))]
173mod imp {
174    use super::safe_parent;
175
176    use rustix::fs::AtFlags;
177    use std::{fs, io, path};
178
179    pub fn replace_atomic(src: &path::Path, dst: &path::Path) -> io::Result<()> {
180        let src_parent_path = safe_parent(src).unwrap();
181        let dst_parent_path = safe_parent(dst).unwrap();
182        let src_child_path = src.file_name().unwrap();
183        let dst_child_path = dst.file_name().unwrap();
184
185        // Open the parent directories. If src and dst have the same parent
186        // path, open it once and reuse it.
187        let src_parent = fs::File::open(src_parent_path)?;
188        let dst_parent;
189        let dst_parent = if src_parent_path == dst_parent_path {
190            &src_parent
191        } else {
192            dst_parent = fs::File::open(dst_parent_path)?;
193            &dst_parent
194        };
195
196        // Do the `renameat`.
197        rustix::fs::renameat(&src_parent, src_child_path, dst_parent, dst_child_path)?;
198
199        // Fsync the parent directory (or directories, if they're different).
200        src_parent.sync_all()?;
201        if src_parent_path != dst_parent_path {
202            dst_parent.sync_all()?;
203        }
204
205        Ok(())
206    }
207
208    pub fn move_atomic(src: &path::Path, dst: &path::Path) -> io::Result<()> {
209        let src_parent_path = safe_parent(src).unwrap();
210        let dst_parent_path = safe_parent(dst).unwrap();
211        let src_child_path = src.file_name().unwrap();
212        let dst_child_path = dst.file_name().unwrap();
213
214        // Open the parent directories. If src and dst have the same parent
215        // path, open it once and reuse it.
216        let src_parent = fs::File::open(src_parent_path)?;
217        let dst_parent;
218        let dst_parent = if src_parent_path == dst_parent_path {
219            &src_parent
220        } else {
221            dst_parent = fs::File::open(dst_parent_path)?;
222            &dst_parent
223        };
224
225        // On Linux, use `renameat2` with `RENAME_NOREPLACE` if we have it, as
226        // that does an atomic rename.
227        #[cfg(any(target_os = "android", target_os = "linux"))]
228        {
229            use rustix::fs::RenameFlags;
230            use std::sync::atomic::AtomicBool;
231            use std::sync::atomic::Ordering::Relaxed;
232
233            static NO_RENAMEAT2: AtomicBool = AtomicBool::new(false);
234            if !NO_RENAMEAT2.load(Relaxed) {
235                match rustix::fs::renameat_with(
236                    &src_parent,
237                    src_child_path,
238                    dst_parent,
239                    dst_child_path,
240                    RenameFlags::NOREPLACE,
241                ) {
242                    Ok(()) => {
243                        // Fsync the parent directory (or directories, if
244                        // they're different).
245                        src_parent.sync_all()?;
246                        if src_parent_path != dst_parent_path {
247                            dst_parent.sync_all()?;
248                        }
249                        return Ok(());
250                    }
251                    Err(rustix::io::Errno::NOSYS) => {
252                        // The OS doesn't support `renameat2`; remember this so
253                        // that we don't bother calling it again.
254                        NO_RENAMEAT2.store(true, Relaxed);
255                    }
256                    Err(e) => return Err(e.into()),
257                }
258            }
259        }
260
261        // Otherwise, hard-link the src to the dst, and then delete the dst.
262        rustix::fs::linkat(
263            &src_parent,
264            src_child_path,
265            dst_parent,
266            dst_child_path,
267            AtFlags::empty(),
268        )?;
269        rustix::fs::unlinkat(&src_parent, src_child_path, AtFlags::empty())?;
270
271        // Fsync the parent directory (or directories, if they're different).
272        src_parent.sync_all()?;
273        if src_parent_path != dst_parent_path {
274            dst_parent.sync_all()?;
275        }
276
277        Ok(())
278    }
279}
280
281#[cfg(target_os = "redox")]
282mod imp {
283    use std::{fs, io, path};
284
285    pub fn replace_atomic(src: &path::Path, dst: &path::Path) -> io::Result<()> {
286        fs::rename(src, dst)
287    }
288
289    pub fn move_atomic(src: &path::Path, dst: &path::Path) -> io::Result<()> {
290        fs::hard_link(src, dst)?;
291        fs::remove_file(src)
292    }
293}
294
295#[cfg(windows)]
296mod imp {
297    extern crate windows_sys;
298
299    use std::ffi::OsStr;
300    use std::os::windows::ffi::OsStrExt;
301    use std::{io, path};
302
303    macro_rules! call {
304        ($e: expr) => {
305            if $e != 0 {
306                Ok(())
307            } else {
308                Err(io::Error::last_os_error())
309            }
310        };
311    }
312
313    fn path_to_windows_str<T: AsRef<OsStr>>(x: T) -> Vec<u16> {
314        x.as_ref().encode_wide().chain(Some(0)).collect()
315    }
316
317    pub fn replace_atomic(src: &path::Path, dst: &path::Path) -> io::Result<()> {
318        call!(unsafe {
319            windows_sys::Win32::Storage::FileSystem::MoveFileExW(
320                path_to_windows_str(src).as_ptr(),
321                path_to_windows_str(dst).as_ptr(),
322                windows_sys::Win32::Storage::FileSystem::MOVEFILE_WRITE_THROUGH
323                    | windows_sys::Win32::Storage::FileSystem::MOVEFILE_REPLACE_EXISTING,
324            )
325        })
326    }
327
328    pub fn move_atomic(src: &path::Path, dst: &path::Path) -> io::Result<()> {
329        call!(unsafe {
330            windows_sys::Win32::Storage::FileSystem::MoveFileExW(
331                path_to_windows_str(src).as_ptr(),
332                path_to_windows_str(dst).as_ptr(),
333                windows_sys::Win32::Storage::FileSystem::MOVEFILE_WRITE_THROUGH,
334            )
335        })
336    }
337}
338
339/// Move `src` to `dst`. If `dst` exists, it will be silently overwritten.
340///
341/// Both paths must reside on the same filesystem for the operation to be atomic.
342pub fn replace_atomic(src: &path::Path, dst: &path::Path) -> io::Result<()> {
343    imp::replace_atomic(src, dst)
344}
345
346/// Move `src` to `dst`. An error will be returned if `dst` exists.
347///
348/// Both paths must reside on the same filesystem for the operation to be atomic.
349pub fn move_atomic(src: &path::Path, dst: &path::Path) -> io::Result<()> {
350    imp::move_atomic(src, dst)
351}