atomicfile/
lib.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8use std::fs;
9use std::fs::File;
10#[cfg(unix)]
11use std::fs::Permissions;
12use std::io;
13#[cfg(unix)]
14use std::os::unix::fs::PermissionsExt;
15use std::path::Path;
16use std::path::PathBuf;
17
18use tempfile::NamedTempFile;
19
20/// Create a temp file and then rename it into the specified path to
21/// achieve atomicity. The temp file is created in the same directory
22/// as path to ensure the rename is not cross filesystem. If fysnc is
23/// true, the file will be fsynced before and after renaming, and the
24/// directory will by fsynced after renaming.
25///
26/// mode_perms is required but does nothing on windows. mode_perms is
27/// not automatically umasked.
28///
29/// The renamed file is returned. Any further data written to the file
30/// will not be atomic since the file is already visibile to readers.
31///
32/// Note that the rename operation will fail on windows if the
33/// destination file exists and is open.
34pub fn atomic_write(
35    path: &Path,
36    #[allow(dead_code)] mode_perms: u32,
37    fsync: bool,
38    op: impl FnOnce(&mut File) -> io::Result<()>,
39) -> io::Result<File> {
40    let mut af = AtomicFile::open(path, mode_perms, fsync)?;
41    op(af.as_file())?;
42    af.save()
43}
44
45pub struct AtomicFile {
46    file: NamedTempFile,
47    path: PathBuf,
48    dir: PathBuf,
49    fsync: bool,
50}
51
52impl AtomicFile {
53    pub fn open(path: &Path, #[allow(dead_code)] mode_perms: u32, fsync: bool) -> io::Result<Self> {
54        let dir = match path.parent() {
55            Some(dir) => dir,
56            None => return Err(io::Error::from(io::ErrorKind::InvalidInput)),
57        };
58
59        let mut temp = NamedTempFile::new_in(dir)?;
60        let f = temp.as_file_mut();
61
62        #[cfg(unix)]
63        f.set_permissions(Permissions::from_mode(mode_perms))?;
64
65        Ok(Self {
66            file: temp,
67            path: path.to_path_buf(),
68            dir: dir.to_path_buf(),
69            fsync,
70        })
71    }
72
73    pub fn as_file(&mut self) -> &mut File {
74        self.file.as_file_mut()
75    }
76
77    pub fn save(self) -> io::Result<File> {
78        let (mut temp, path, dir, fsync) = (self.file, self.path, self.dir, self.fsync);
79        let f = temp.as_file_mut();
80
81        if fsync {
82            f.sync_data()?;
83        }
84
85        let max_retries = if cfg!(windows) { 5u16 } else { 0 };
86        let mut retry = 0;
87        loop {
88            match temp.persist(&path) {
89                Ok(persisted) => {
90                    if fsync {
91                        persisted.sync_all()?;
92
93                        // Also sync the directory on Unix.
94                        // Windows does not support syncing a directory.
95                        #[cfg(unix)]
96                        {
97                            if let Ok(opened) = fs::OpenOptions::new().read(true).open(dir) {
98                                let _ = opened.sync_all();
99                            }
100                        }
101                    }
102
103                    break Ok(persisted);
104                }
105                Err(e) => {
106                    if retry == max_retries || e.error.kind() != io::ErrorKind::PermissionDenied {
107                        break Err(e.error);
108                    }
109
110                    // Windows fails with "Access Denied" if destination file is open.
111                    // Retry a few times.
112                    tracing::info!(
113                        retry,
114                        ?path,
115                        "atomic_write rename failed with EPERM. Will retry.",
116                    );
117                    std::thread::sleep(std::time::Duration::from_millis(1 << retry));
118                    temp = e.file;
119
120                    retry += 1;
121                }
122            }
123        }
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use std::io::Write;
130    #[cfg(unix)]
131    use std::os::unix::prelude::MetadataExt;
132
133    use tempfile::tempdir;
134
135    use super::*;
136
137    #[test]
138    fn test_atomic_write() -> io::Result<()> {
139        let td = tempdir()?;
140
141        let foo_path = td.path().join("foo");
142        atomic_write(&foo_path, 0o640, false, |f| {
143            f.write_all(b"sushi")?;
144            Ok(())
145        })?;
146
147        // Sanity check that we wrote contents and the temp file is gone.
148        assert_eq!("sushi", std::fs::read_to_string(&foo_path)?);
149        assert_eq!(1, std::fs::read_dir(td.path())?.count());
150
151        // Make sure we can set the mode perms on unix.
152        #[cfg(unix)]
153        assert_eq!(
154            0o640,
155            0o777 & std::fs::File::open(&foo_path)?.metadata()?.mode()
156        );
157
158        Ok(())
159    }
160}