139 lines
3.9 KiB
Rust
139 lines
3.9 KiB
Rust
use std::fs;
|
|
use std::io::Write;
|
|
use std::path::Path;
|
|
|
|
use anyhow::Result;
|
|
|
|
/// Atomically write `data` (bytes) to `path`, overwriting if the file already exists.
|
|
///
|
|
/// Uses `tempfile::NamedTempFile` to create a temporary file in the same directory,
|
|
/// then atomically renames it to the target path via `persist`.
|
|
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)?;
|
|
|
|
// Create a temp file in the same directory (same filesystem → atomic rename).
|
|
let mut tmp = tempfile::NamedTempFile::new_in(parent)?;
|
|
|
|
// Set permissions before writing content (avoid permission window).
|
|
#[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()?;
|
|
|
|
// Atomically rename to the target path.
|
|
tmp.persist(path)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::fs;
|
|
use tempfile::TempDir;
|
|
|
|
fn tmp() -> TempDir {
|
|
tempfile::tempdir().unwrap()
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// atomic_write
|
|
// -----------------------------------------------------------------------
|
|
|
|
#[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();
|
|
// Only the target file should remain; no `.tmp.*` leftovers.
|
|
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"");
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Unix-specific: file permissions
|
|
// -----------------------------------------------------------------------
|
|
|
|
#[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);
|
|
}
|
|
}
|
|
}
|