use std::io::{self, Write};
use std::path::{Path, PathBuf};
#[allow(clippy::redundant_pub_crate)]
pub(crate) fn atomic_write(path: &Path, contents: &str) -> io::Result<()> {
let absolute: PathBuf = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()?.join(path)
};
let parent = absolute.parent().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"atomic_write: path has no parent directory",
)
})?;
std::fs::create_dir_all(parent)?;
let mut tmp = tempfile::NamedTempFile::new_in(parent)?;
tmp.write_all(contents.as_bytes())?;
tmp.as_file().sync_all()?;
tmp.persist(&absolute).map_err(|e| {
linesmith_core::lsm_warn!(
"atomic_write: persist failed; orphaned temp at {} may be removed manually",
e.file.path().display(),
);
e.error
})?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn creates_new_file_with_contents() {
let tmp = TempDir::new().expect("tempdir");
let path = tmp.path().join("config.toml");
atomic_write(&path, "[line]\nsegments = []\n").expect("write");
let read = fs::read_to_string(&path).expect("read");
assert_eq!(read, "[line]\nsegments = []\n");
}
#[test]
fn replaces_existing_file_atomically() {
let tmp = TempDir::new().expect("tempdir");
let path = tmp.path().join("config.toml");
fs::write(&path, "old contents that should disappear").expect("seed");
atomic_write(&path, "new bytes").expect("write");
assert_eq!(fs::read_to_string(&path).expect("read"), "new bytes");
}
#[test]
fn creates_missing_parent_directory() {
let tmp = TempDir::new().expect("tempdir");
let nested = tmp.path().join("nested/subdir/config.toml");
atomic_write(&nested, "x").expect("write");
assert_eq!(fs::read_to_string(&nested).expect("read"), "x");
}
#[test]
fn does_not_leave_temp_file_on_success() {
let tmp = TempDir::new().expect("tempdir");
let path = tmp.path().join("config.toml");
atomic_write(&path, "x").expect("write");
let entries: Vec<_> = fs::read_dir(tmp.path())
.expect("read_dir")
.filter_map(|e| e.ok())
.map(|e| e.file_name())
.collect();
assert_eq!(
entries.len(),
1,
"expected only the destination file, got {entries:?}",
);
}
}