use std::fs;
use std::io::Write;
use std::path::Path;
use anyhow::Result;
pub fn atomic_write(path: &Path, data: &[u8], mode: Option<u32>) -> Result<()> {
let Some(parent) = path.parent() else {
anyhow::bail!("无效文件路径:{}", path.display());
};
fs::create_dir_all(parent)?;
let mut tmp = tempfile::NamedTempFile::new_in(parent)?;
#[cfg(unix)]
if let Some(m) = mode {
use std::os::unix::fs::PermissionsExt;
tmp.as_file()
.set_permissions(fs::Permissions::from_mode(m))?;
}
tmp.write_all(data)?;
tmp.as_file().flush()?;
tmp.as_file().sync_all()?;
tmp.persist(path)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn tmp() -> TempDir {
tempfile::tempdir().unwrap()
}
#[test]
fn write_creates_file_with_correct_content() {
let dir = tmp();
let path = dir.path().join("test.bin");
let data = b"hello world";
atomic_write(&path, data, None).unwrap();
assert_eq!(fs::read(&path).unwrap(), data);
}
#[test]
fn write_creates_parent_directories() {
let dir = tmp();
let path = dir.path().join("a").join("b").join("deep.bin");
atomic_write(&path, b"nested", None).unwrap();
assert_eq!(fs::read(&path).unwrap(), b"nested");
}
#[test]
fn overwrite_replaces_existing_file() {
let dir = tmp();
let path = dir.path().join("overwrite.bin");
atomic_write(&path, b"first", None).unwrap();
atomic_write(&path, b"second", None).unwrap();
assert_eq!(fs::read(&path).unwrap(), b"second");
}
#[test]
fn no_temp_file_left_after_success() {
let dir = tmp();
let path = dir.path().join("clean.bin");
atomic_write(&path, b"ok", None).unwrap();
let entries: Vec<_> = fs::read_dir(dir.path())
.unwrap()
.filter_map(|e| e.ok())
.collect();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].file_name(), "clean.bin");
}
#[test]
fn write_empty_data() {
let dir = tmp();
let path = dir.path().join("empty.bin");
atomic_write(&path, b"", None).unwrap();
assert_eq!(fs::read(&path).unwrap(), b"");
}
#[cfg(unix)]
mod unix {
use super::*;
use std::os::unix::fs::PermissionsExt;
#[test]
fn write_sets_file_permissions() {
let dir = tmp();
let path = dir.path().join("secret.bin");
atomic_write(&path, b"secret", Some(0o600)).unwrap();
let perms = fs::metadata(&path).unwrap().permissions();
assert_eq!(perms.mode() & 0o777, 0o600);
}
#[test]
fn write_sets_readonly_permissions() {
let dir = tmp();
let path = dir.path().join("readonly.bin");
atomic_write(&path, b"ro", Some(0o400)).unwrap();
let perms = fs::metadata(&path).unwrap().permissions();
assert_eq!(perms.mode() & 0o777, 0o400);
}
}
}