#![forbid(unsafe_code)]
use std::fs;
use std::io::Write;
use std::path::Path;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
pub fn atomic_write_text(path: &Path, content: &str) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
for attempt in 0..64 {
let temporary = unique_temp_path(path, attempt);
let file = fs::OpenOptions::new().create_new(true).write(true).open(&temporary);
let mut file = match file {
Ok(file) => file,
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(err) => return Err(err),
};
file.write_all(content.as_bytes())?;
file.sync_all()?;
replace_file(&temporary, path)?;
sync_parent_directory(path)?;
return Ok(());
}
Err(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
format!("unable to allocate unique temp file for {}", path.display()),
))
}
#[cfg(unix)]
fn sync_parent_directory(path: &Path) -> std::io::Result<()> {
use std::fs::File;
let parent = path.parent().unwrap_or_else(|| Path::new("."));
let directory = File::open(parent)?;
directory.sync_all()
}
#[cfg(not(unix))]
fn sync_parent_directory(_path: &Path) -> std::io::Result<()> {
Ok(())
}
fn unique_temp_path(path: &Path, attempt: u32) -> std::path::PathBuf {
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
let parent = path.parent().unwrap_or_else(|| Path::new("."));
let stem = path.file_name().and_then(|name| name.to_str()).unwrap_or("state");
let pid = std::process::id();
let nanos = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_nanos();
let ticket = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
parent.join(format!(".{stem}.{pid}.{nanos}.{ticket}.{attempt}.tmp"))
}
fn replace_file(source: &Path, destination: &Path) -> std::io::Result<()> {
#[cfg(windows)]
{
if destination.exists() {
match fs::remove_file(destination) {
Ok(()) => {}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err),
}
}
}
fs::rename(source, destination)
}