use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use crate::error::{Error, Result};
static ATOMIC_WRITE_SEQ: AtomicU64 = AtomicU64::new(0);
const MAX_TEMP_NAME_ATTEMPTS: u32 = 100;
fn temp_path(target: &Path) -> PathBuf {
let seq = ATOMIC_WRITE_SEQ.fetch_add(1, Ordering::Relaxed);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let stem = target
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("run-json");
target.with_file_name(format!(".{stem}-{nanos:x}-{seq:x}.tmp"))
}
fn create_temp_file_exclusive(
mut next_candidate: impl FnMut() -> PathBuf,
) -> Result<(PathBuf, std::fs::File)> {
let mut attempts = 0u32;
loop {
let candidate = next_candidate();
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&candidate)
{
Ok(file) => return Ok((candidate, file)),
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
attempts += 1;
if attempts >= MAX_TEMP_NAME_ATTEMPTS {
return Err(Error::unknown(format!(
"{}: giving up after {MAX_TEMP_NAME_ATTEMPTS} tempfile-name collisions",
candidate.display()
)));
}
}
Err(error) => {
return Err(Error::unknown(format!(
"create {}: {error}",
candidate.display()
)));
}
}
}
}
pub(crate) fn atomic_write(target: &Path, contents: &str) -> Result<()> {
let parent = target
.parent()
.filter(|p| !p.as_os_str().is_empty())
.ok_or_else(|| {
Error::unknown(format!(
"{}: refusing to write -- no parent directory",
target.display()
))
})?;
std::fs::create_dir_all(parent)
.map_err(|error| Error::unknown(format!("create {}: {error}", parent.display())))?;
write_and_rename(target, contents, || temp_path(target))?;
let dir = std::fs::File::open(parent)
.map_err(|error| Error::unknown(format!("open {} for fsync: {error}", parent.display())))?;
dir.sync_all()
.map_err(|error| Error::unknown(format!("fsync {}: {error}", parent.display())))?;
Ok(())
}
pub(super) fn write_and_rename(
target: &Path,
contents: &str,
next_candidate: impl FnMut() -> PathBuf,
) -> Result<()> {
use std::io::Write as _;
let (tmp, mut file) = create_temp_file_exclusive(next_candidate)?;
let write_result = (|| -> Result<()> {
file.write_all(contents.as_bytes())
.map_err(|error| Error::unknown(format!("write {}: {error}", tmp.display())))?;
file.write_all(b"\n")
.map_err(|error| Error::unknown(format!("write {}: {error}", tmp.display())))?;
file.flush()
.map_err(|error| Error::unknown(format!("flush {}: {error}", tmp.display())))?;
file.sync_all()
.map_err(|error| Error::unknown(format!("fsync {}: {error}", tmp.display())))
})();
drop(file);
if let Err(error) = write_result {
let _ = std::fs::remove_file(&tmp);
return Err(error);
}
std::fs::rename(&tmp, target).map_err(|error| {
let rename_error = Error::unknown(format!(
"rename {} -> {}: {error}",
tmp.display(),
target.display()
));
let _ = std::fs::remove_file(&tmp);
rename_error
})
}