use std::fs;
use std::fs::File;
#[cfg(unix)]
use std::fs::Permissions;
use std::io;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::path::PathBuf;
use tempfile::NamedTempFile;
pub fn atomic_write(
path: &Path,
#[allow(dead_code)] mode_perms: u32,
fsync: bool,
op: impl FnOnce(&mut File) -> io::Result<()>,
) -> io::Result<File> {
let mut af = AtomicFile::open(path, mode_perms, fsync)?;
op(af.as_file())?;
af.save()
}
pub struct AtomicFile {
file: NamedTempFile,
path: PathBuf,
dir: PathBuf,
fsync: bool,
}
impl AtomicFile {
pub fn open(path: &Path, #[allow(dead_code)] mode_perms: u32, fsync: bool) -> io::Result<Self> {
let dir = match path.parent() {
Some(dir) => dir,
None => return Err(io::Error::from(io::ErrorKind::InvalidInput)),
};
let mut temp = NamedTempFile::new_in(dir)?;
let f = temp.as_file_mut();
#[cfg(unix)]
f.set_permissions(Permissions::from_mode(mode_perms))?;
Ok(Self {
file: temp,
path: path.to_path_buf(),
dir: dir.to_path_buf(),
fsync,
})
}
pub fn as_file(&mut self) -> &mut File {
self.file.as_file_mut()
}
pub fn save(self) -> io::Result<File> {
let (mut temp, path, dir, fsync) = (self.file, self.path, self.dir, self.fsync);
let f = temp.as_file_mut();
if fsync {
f.sync_data()?;
}
let max_retries = if cfg!(windows) { 5u16 } else { 0 };
let mut retry = 0;
loop {
match temp.persist(&path) {
Ok(persisted) => {
if fsync {
persisted.sync_all()?;
#[cfg(unix)]
{
if let Ok(opened) = fs::OpenOptions::new().read(true).open(dir) {
let _ = opened.sync_all();
}
}
}
break Ok(persisted);
}
Err(e) => {
if retry == max_retries || e.error.kind() != io::ErrorKind::PermissionDenied {
break Err(e.error);
}
tracing::info!(
retry,
?path,
"atomic_write rename failed with EPERM. Will retry.",
);
std::thread::sleep(std::time::Duration::from_millis(1 << retry));
temp = e.file;
retry += 1;
}
}
}
}
}
#[cfg(test)]
mod tests {
use std::io::Write;
#[cfg(unix)]
use std::os::unix::prelude::MetadataExt;
use tempfile::tempdir;
use super::*;
#[test]
fn test_atomic_write() -> io::Result<()> {
let td = tempdir()?;
let foo_path = td.path().join("foo");
atomic_write(&foo_path, 0o640, false, |f| {
f.write_all(b"sushi")?;
Ok(())
})?;
assert_eq!("sushi", std::fs::read_to_string(&foo_path)?);
assert_eq!(1, std::fs::read_dir(td.path())?.count());
#[cfg(unix)]
assert_eq!(
0o640,
0o777 & std::fs::File::open(&foo_path)?.metadata()?.mode()
);
Ok(())
}
}