105 lines
3.4 KiB
Rust
105 lines
3.4 KiB
Rust
use std::ffi::OsString;
|
|
use std::fs;
|
|
use std::io::{self, Write};
|
|
use std::path::Path;
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
|
|
static NEXT_TEMP_ID: AtomicU64 = AtomicU64::new(1);
|
|
|
|
/// Write `content` to `path` atomically: write to a temp file in the same
|
|
/// directory, then rename. Creates parent directories if missing.
|
|
pub fn atomic_write(path: &Path, content: &[u8]) -> io::Result<()> {
|
|
if let Some(parent) = path.parent() {
|
|
fs::create_dir_all(parent)?;
|
|
}
|
|
let parent = path.parent().unwrap_or_else(|| Path::new("."));
|
|
let file_name = path
|
|
.file_name()
|
|
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "path has no file name"))?;
|
|
|
|
loop {
|
|
let mut temp_name = OsString::from(".");
|
|
temp_name.push(file_name);
|
|
temp_name.push(format!(
|
|
".{}.{}.tmp",
|
|
std::process::id(),
|
|
NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed)
|
|
));
|
|
let tmp_path = parent.join(temp_name);
|
|
let mut file = match fs::File::create_new(&tmp_path) {
|
|
Ok(file) => file,
|
|
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
|
|
Err(error) => return Err(error),
|
|
};
|
|
let result = (|| {
|
|
file.write_all(content)?;
|
|
file.sync_all()?;
|
|
drop(file);
|
|
fs::rename(&tmp_path, path)
|
|
})();
|
|
if result.is_err() {
|
|
let _ = fs::remove_file(&tmp_path);
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/// Convenience wrapper for UTF-8 string content.
|
|
pub fn atomic_write_str(path: &Path, content: &str) -> io::Result<()> {
|
|
atomic_write(path, content.as_bytes())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use tempfile::TempDir;
|
|
|
|
#[test]
|
|
fn write_and_read_back() {
|
|
let dir = TempDir::new().unwrap();
|
|
let path = dir.path().join("test.txt");
|
|
atomic_write_str(&path, "hello world").unwrap();
|
|
assert_eq!(fs::read_to_string(&path).unwrap(), "hello world");
|
|
}
|
|
|
|
#[test]
|
|
fn creates_parent_directories() {
|
|
let dir = TempDir::new().unwrap();
|
|
let path = dir.path().join("a").join("b").join("c.txt");
|
|
atomic_write_str(&path, "nested").unwrap();
|
|
assert_eq!(fs::read_to_string(&path).unwrap(), "nested");
|
|
}
|
|
|
|
#[test]
|
|
fn overwrites_existing() {
|
|
let dir = TempDir::new().unwrap();
|
|
let path = dir.path().join("test.txt");
|
|
atomic_write_str(&path, "v1").unwrap();
|
|
atomic_write_str(&path, "v2").unwrap();
|
|
assert_eq!(fs::read_to_string(&path).unwrap(), "v2");
|
|
}
|
|
|
|
#[test]
|
|
fn concurrent_sibling_writes_do_not_share_a_temp_file() {
|
|
let dir = TempDir::new().unwrap();
|
|
let markdown = dir.path().join("a.en.md");
|
|
let metadata = dir.path().join("a.en.meta");
|
|
let barrier = std::sync::Arc::new(std::sync::Barrier::new(2));
|
|
|
|
let write = |path: std::path::PathBuf, content: &'static str| {
|
|
let barrier = barrier.clone();
|
|
std::thread::spawn(move || {
|
|
barrier.wait();
|
|
atomic_write_str(&path, content)
|
|
})
|
|
};
|
|
let first = write(markdown.clone(), "markdown");
|
|
let second = write(metadata.clone(), "metadata");
|
|
|
|
first.join().unwrap().unwrap();
|
|
second.join().unwrap().unwrap();
|
|
assert_eq!(fs::read_to_string(markdown).unwrap(), "markdown");
|
|
assert_eq!(fs::read_to_string(metadata).unwrap(), "metadata");
|
|
}
|
|
}
|