use std::ffi::OsString;
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use uuid::Uuid;
pub fn unique_tmp_path(path: &Path) -> PathBuf {
let pid = std::process::id();
let rand8: String = Uuid::new_v4().simple().to_string()[..8].to_string();
let mut name: OsString = path.file_name().map(ToOwned::to_owned).unwrap_or_default();
name.push(format!(".{pid}.{rand8}.tmp"));
match path.parent() {
Some(parent) if !parent.as_os_str().is_empty() => parent.join(name),
_ => PathBuf::from(name),
}
}
pub fn atomic_write_bytes(path: &Path, payload: &[u8]) -> io::Result<()> {
atomic_write_bytes_mode(path, payload, None)
}
pub fn atomic_write_bytes_mode(path: &Path, payload: &[u8], mode: Option<u32>) -> io::Result<()> {
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
fs::create_dir_all(parent)?;
}
}
let tmp = unique_tmp_path(path);
let write_result = (|| -> io::Result<()> {
let mut file = create_tmp_file(&tmp, mode)?;
file.write_all(payload)?;
file.flush()?;
file.sync_all()?;
Ok(())
})();
if let Err(e) = write_result {
let _ = fs::remove_file(&tmp);
return Err(e);
}
if let Err(e) = fs::rename(&tmp, path) {
let _ = fs::remove_file(&tmp);
return Err(e);
}
Ok(())
}
#[cfg(unix)]
fn create_tmp_file(tmp: &Path, mode: Option<u32>) -> io::Result<fs::File> {
match mode {
Some(m) => {
use std::os::unix::fs::OpenOptionsExt;
fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(m)
.open(tmp)
}
None => fs::File::create(tmp),
}
}
#[cfg(not(unix))]
fn create_tmp_file(tmp: &Path, _mode: Option<u32>) -> io::Result<fs::File> {
fs::File::create(tmp)
}
pub fn atomic_write_text(path: &Path, text: &str) -> io::Result<()> {
atomic_write_bytes(path, text.as_bytes())
}
#[cfg(test)]
mod tests {
use super::*;
fn temp_dir(tag: &str) -> PathBuf {
let rand = Uuid::new_v4().simple().to_string();
std::env::temp_dir().join(format!("a2c-atomic-{tag}-{}-{rand}", std::process::id()))
}
#[test]
fn test_unique_tmp_path_shape_and_uniqueness() {
let target = Path::new("/some/dir/config.json");
let a = unique_tmp_path(target);
let b = unique_tmp_path(target);
assert_ne!(a, b, "两次生成的临时名必须不同");
let a_name = a.file_name().unwrap().to_str().unwrap();
assert!(
a_name.starts_with("config.json."),
"保留原文件名前缀: {a_name}"
);
assert!(a_name.ends_with(".tmp"), "以 .tmp 结尾: {a_name}");
assert_eq!(a.parent(), Some(Path::new("/some/dir")), "与目标同目录");
}
#[test]
fn test_atomic_write_roundtrip_and_no_tmp_residue() {
let dir = temp_dir("rt");
let target = dir.join("nested").join("data.bin");
let payload = b"hello atomic world";
atomic_write_bytes(&target, payload).expect("写入应成功");
assert_eq!(fs::read(&target).unwrap(), payload);
let residue: Vec<_> = fs::read_dir(target.parent().unwrap())
.unwrap()
.filter_map(Result::ok)
.filter(|e| e.file_name().to_string_lossy().ends_with(".tmp"))
.collect();
assert!(residue.is_empty(), "成功写入不得残留临时文件");
atomic_write_text(&target, "second").unwrap();
assert_eq!(fs::read_to_string(&target).unwrap(), "second");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_atomic_write_failure_leaves_no_tmp() {
let dir = temp_dir("fail");
fs::create_dir_all(&dir).unwrap();
let clash = dir.join("clash");
fs::write(&clash, b"i am a file").unwrap();
let target = clash.join("inside.txt");
let result = atomic_write_bytes(&target, b"x");
assert!(result.is_err(), "父路径是文件时写入应失败");
let residue: Vec<_> = fs::read_dir(&dir)
.unwrap()
.filter_map(Result::ok)
.filter(|e| e.file_name().to_string_lossy().ends_with(".tmp"))
.collect();
assert!(residue.is_empty(), "失败写入不得残留临时文件");
let _ = fs::remove_dir_all(&dir);
}
#[cfg(unix)]
#[test]
fn test_atomic_write_mode_sets_0600_at_creation() {
use std::os::unix::fs::PermissionsExt;
let dir = temp_dir("mode");
let target = dir.join("secret.json");
atomic_write_bytes_mode(&target, b"{}", Some(0o600)).expect("写入应成功");
let mode = fs::metadata(&target).unwrap().permissions().mode() & 0o777;
assert_eq!(
mode, 0o600,
"目标文件应以 0600 创建(rename 后无宽权限窗口)"
);
assert_eq!(fs::read(&target).unwrap(), b"{}");
let plain = dir.join("plain.txt");
atomic_write_bytes_mode(&plain, b"x", None).unwrap();
assert_eq!(fs::read(&plain).unwrap(), b"x");
let _ = fs::remove_dir_all(&dir);
}
}