use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::Path;
pub(super) fn create_new(path: &Path, contents: &str) -> Result<(), String> {
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
.map_err(|error| format!("cannot create {}: {error}", path.display()))?;
file.write_all(contents.as_bytes())
.map_err(|error| format!("cannot write {}: {error}", path.display()))
}
pub(super) fn atomic_write(path: &Path, contents: &str) -> Result<(), String> {
static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let sequence = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let name = path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("file");
let temporary = path.with_file_name(format!(
".{name}.nichlink-{}-{sequence}.tmp",
std::process::id()
));
let outcome = create_new(&temporary, contents).and_then(|()| {
fs::rename(&temporary, path)
.map_err(|error| format!("cannot replace {}: {error}", path.display()))
});
if outcome.is_err() {
let _ = fs::remove_file(&temporary);
}
outcome
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn fixture(label: &str) -> PathBuf {
static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let sequence = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let root = std::env::temp_dir().join(format!(
"nichlink-atomic-{label}-{}-{sequence}",
std::process::id()
));
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(&root).expect("fixture directory");
root
}
#[test]
fn writing_replaces_the_target_and_touches_nothing_else() {
let root = fixture("sibling");
let target = root.join("face.rs");
fs::write(&target, "old").expect("target");
let neighbour = root.join("face.nichlink.tmp");
fs::write(&neighbour, "someone else's bytes").expect("neighbour");
atomic_write(&target, "new").expect("atomic write");
assert_eq!(fs::read_to_string(&target).expect("target"), "new");
assert_eq!(
fs::read_to_string(&neighbour).expect("the neighbour survives"),
"someone else's bytes"
);
let leftovers = fs::read_dir(&root)
.expect("read the directory")
.filter_map(Result::ok)
.filter(|entry| entry.file_name().to_string_lossy().contains("nichlink-"))
.count();
assert_eq!(leftovers, 0, "no temporary of our own is left behind");
let _ = fs::remove_dir_all(&root);
}
}