use std::fs::{self, File};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
pub fn write_atomic(path: &Path, contents: &[u8]) -> io::Result<()> {
if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
fs::create_dir_all(parent)?;
}
let temp = temp_path(path);
match write_then_rename(&temp, path, contents) {
Ok(()) => Ok(()),
Err(error) => {
let _ = fs::remove_file(&temp);
Err(error)
}
}
}
fn write_then_rename(
temp: &Path,
path: &Path,
contents: &[u8],
) -> io::Result<()> {
let mut file = File::create(temp)?;
file.write_all(contents)?;
file.sync_all()?;
drop(file);
fs::rename(temp, path)
}
fn temp_path(path: &Path) -> PathBuf {
let mut name = path.file_name().unwrap_or_default().to_os_string();
name.push(format!(".tmp.{}", std::process::id()));
path.with_file_name(name)
}
#[cfg(test)]
mod tests {
use super::*;
fn scratch_dir(label: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"calcli-fs-{}-{}",
label,
std::process::id()
));
let _ = fs::remove_dir_all(&dir);
dir
}
#[test]
fn writes_a_new_file_and_creates_missing_parents() {
let dir = scratch_dir("new");
let path = dir.join("nested").join("state.toml");
write_atomic(&path, b"hello").unwrap();
assert_eq!(fs::read_to_string(&path).unwrap(), "hello");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn replaces_an_existing_file_leaving_no_temporary_behind() {
let dir = scratch_dir("replace");
let path = dir.join("state.toml");
write_atomic(&path, b"old").unwrap();
write_atomic(&path, b"new").unwrap();
assert_eq!(fs::read_to_string(&path).unwrap(), "new");
let leftovers: Vec<_> = fs::read_dir(&dir)
.unwrap()
.filter_map(Result::ok)
.map(|entry| entry.file_name())
.filter(|name| name != "state.toml")
.collect();
assert!(leftovers.is_empty(), "temporary files left: {leftovers:?}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn a_failed_write_leaves_the_previous_contents_intact() {
let dir = scratch_dir("failed");
let path = dir.join("state.toml");
write_atomic(&path, b"good").unwrap();
fs::create_dir_all(temp_path(&path)).unwrap();
assert!(write_atomic(&path, b"bad").is_err());
assert_eq!(fs::read_to_string(&path).unwrap(), "good");
let _ = fs::remove_dir_all(&dir);
}
}