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 std::time::Duration;
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 Wait<'a> {
path: &'a Path,
meta: Option<fs::Metadata>,
}
impl<'a> Wait<'a> {
pub fn from_path(path: &'a Path) -> io::Result<Self> {
let meta = match path.symlink_metadata() {
Ok(m) => Some(m),
Err(e) if e.kind() == io::ErrorKind::NotFound => None,
Err(e) => return Err(e),
};
Ok(Self { path, meta })
}
pub fn wait_for_change(&mut self) -> io::Result<()> {
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
#[cfg(windows)]
use std::os::windows::fs::MetadataExt;
tracing::debug!("waiting for atomic change: {}", self.path.display());
let mut new_wait;
'wait_loop: loop {
new_wait = Self::from_path(self.path)?;
match (&self.meta, new_wait.meta.as_ref()) {
(None, None) => {}
(Some(_), None) | (None, Some(_)) => {
tracing::trace!(" waited: existence changed");
break 'wait_loop;
}
(Some(new), Some(old)) => {
#[cfg(unix)]
if new.ino() != old.ino() {
tracing::trace!(" waited: inode changed");
break 'wait_loop;
}
#[cfg(windows)]
if new.last_write_time() != old.last_write_time()
|| new.creation_time() != old.creation_time()
{
tracing::trace!(" waited: mtime changed");
break 'wait_loop;
}
}
}
std::thread::sleep(Duration::from_millis(100));
}
self.meta = new_wait.meta;
Ok(())
}
}
pub struct AtomicFile {
file: NamedTempFile,
path: PathBuf,
dir: PathBuf,
fsync: bool,
}
impl AtomicFile {
pub fn open(
path: &Path,
#[allow(unused_variables)] 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)),
};
#[allow(unused_mut)]
let mut temp = NamedTempFile::new_in(dir)?;
#[cfg(unix)]
{
let f = temp.as_file_mut();
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> {
#[allow(unused_variables)]
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 std::sync::mpsc;
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(())
}
#[test]
fn test_wait_for_change() -> io::Result<()> {
let dir = tempdir()?;
let path = dir.path().join("f");
let (tx, rx) = mpsc::channel::<i32>();
std::thread::spawn({
let path = path.clone();
move || {
let mut wait = Wait::from_path(&path).unwrap();
wait.wait_for_change().unwrap();
tx.send(101).unwrap();
wait.wait_for_change().unwrap();
tx.send(102).unwrap();
wait.wait_for_change().unwrap();
tx.send(103).unwrap();
}
});
std::thread::sleep(Duration::from_millis(110));
assert!(rx.try_recv().is_err());
atomic_write(&path, 0o640, false, |_| Ok(()))?;
assert_eq!(rx.recv().unwrap(), 101);
atomic_write(&path, 0o640, false, |_| Ok(()))?;
assert_eq!(rx.recv().unwrap(), 102);
std::fs::remove_file(&path)?;
assert_eq!(rx.recv().unwrap(), 103);
Ok(())
}
}