use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use crate::domain::error::{Error, Result};
pub fn write_atomic(path: &Path, contents: &str, what: &str) -> Result<()> {
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
{
fs::create_dir_all(parent).map_err(|error| {
Error::io(format!("create {what} directory"), error)
})?;
}
let temp = temp_path(path);
fs::write(&temp, contents)
.map_err(|error| Error::io(format!("write {what}"), error))?;
replace_atomically(&temp, path)
.map_err(|error| Error::io(format!("replace {what}"), error))
}
fn temp_path(dest: &Path) -> PathBuf {
let mut name = dest.as_os_str().to_os_string();
name.push(".tmp");
PathBuf::from(name)
}
fn replace_atomically(tmp: &Path, dest: &Path) -> io::Result<()> {
match fs::rename(tmp, dest) {
Ok(()) => Ok(()),
Err(_) if dest.exists() => {
fs::remove_file(dest)?;
fs::rename(tmp, dest)
}
Err(error) => Err(error),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn temp_dir(tag: &str) -> PathBuf {
let dir = std::env::temp_dir()
.join(format!("hop-fs-{tag}-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
dir
}
#[test]
fn writes_a_new_file_and_creates_its_directory() {
let dir = temp_dir("new");
let file = dir.join("nested").join("config.toml");
write_atomic(&file, "a = 1", "config").unwrap();
assert_eq!(fs::read_to_string(&file).unwrap(), "a = 1");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn replaces_an_existing_file_and_leaves_no_temp_behind() {
let dir = temp_dir("replace");
let file = dir.join("config.toml");
write_atomic(&file, "old", "config").unwrap();
write_atomic(&file, "new", "config").unwrap();
assert_eq!(fs::read_to_string(&file).unwrap(), "new");
assert!(!temp_path(&file).exists(), "the temp file is renamed away");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn the_previous_content_survives_a_failed_write() {
let dir = temp_dir("survive");
let file = dir.join("config.toml");
write_atomic(&file, "keep me", "config").unwrap();
fs::create_dir_all(temp_path(&file)).unwrap();
assert!(write_atomic(&file, "lost", "config").is_err());
assert_eq!(fs::read_to_string(&file).unwrap(), "keep me");
let _ = fs::remove_dir_all(&dir);
}
}