1use 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
20pub 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 #[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 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 assert_eq!("sushi", std::fs::read_to_string(&foo_path)?);
149 assert_eq!(1, std::fs::read_dir(td.path())?.count());
150
151 #[cfg(unix)]
153 assert_eq!(
154 0o640,
155 0o777 & std::fs::File::open(&foo_path)?.metadata()?.mode()
156 );
157
158 Ok(())
159 }
160}