use std::fs::{self, File};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
pub struct AtomicFile {
target: PathBuf,
temp: PathBuf,
file: Option<File>,
}
impl AtomicFile {
pub fn new<P: AsRef<Path>>(path: P) -> io::Result<Self> {
let target = path.as_ref().to_path_buf();
let temp = target.with_extension(format!("tmp.{}", std::process::id()));
let file = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&temp)?;
Ok(AtomicFile {
target,
temp,
file: Some(file),
})
}
pub fn commit(mut self) -> io::Result<()> {
if let Some(ref file) = self.file {
file.sync_all()?;
}
self.file = None;
let result = fs::rename(&self.temp, &self.target);
if result.is_ok() {
std::mem::forget(self);
}
result
}
#[allow(dead_code)]
pub fn cancel(mut self) -> io::Result<()> {
self.file = None;
let result = fs::remove_file(&self.temp);
std::mem::forget(self);
result
}
}
impl Write for AtomicFile {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self.file.as_mut() {
Some(file) => file.write(buf),
None => Err(io::Error::other("File already closed")),
}
}
fn flush(&mut self) -> io::Result<()> {
match self.file.as_mut() {
Some(file) => file.flush(),
None => Ok(()),
}
}
}
impl Drop for AtomicFile {
fn drop(&mut self) {
let _ = fs::remove_file(&self.temp);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn test_atomic_write_commit() {
let test_path = "/tmp/whi_test_atomic_commit.txt";
let _ = fs::remove_file(test_path);
{
let mut atomic = AtomicFile::new(test_path).unwrap();
atomic.write_all(b"test content").unwrap();
atomic.commit().unwrap();
}
assert!(Path::new(test_path).exists());
let content = fs::read_to_string(test_path).unwrap();
assert_eq!(content, "test content");
let temp_path = format!("{}.tmp.{}", test_path, std::process::id());
assert!(!Path::new(&temp_path).exists());
fs::remove_file(test_path).unwrap();
}
#[test]
fn test_atomic_write_cancel() {
let test_path = "/tmp/whi_test_atomic_cancel.txt";
let _ = fs::remove_file(test_path);
{
let mut atomic = AtomicFile::new(test_path).unwrap();
atomic.write_all(b"test content").unwrap();
atomic.cancel().unwrap();
}
assert!(!Path::new(test_path).exists());
let temp_path = format!("{}.tmp.{}", test_path, std::process::id());
assert!(!Path::new(&temp_path).exists());
}
#[test]
fn test_atomic_write_drop_cleanup() {
let test_path = "/tmp/whi_test_atomic_drop.txt";
let _ = fs::remove_file(test_path);
{
let mut atomic = AtomicFile::new(test_path).unwrap();
atomic.write_all(b"test content").unwrap();
}
assert!(!Path::new(test_path).exists());
let temp_path = format!("{}.tmp.{}", test_path, std::process::id());
assert!(!Path::new(&temp_path).exists());
}
#[test]
fn test_atomic_write_overwrites_existing() {
let test_path = "/tmp/whi_test_atomic_overwrite.txt";
fs::write(test_path, b"initial content").unwrap();
{
let mut atomic = AtomicFile::new(test_path).unwrap();
atomic.write_all(b"new content").unwrap();
atomic.commit().unwrap();
}
let content = fs::read_to_string(test_path).unwrap();
assert_eq!(content, "new content");
fs::remove_file(test_path).unwrap();
}
}