safe_write/lib.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
//! A crate for safely writing files using an atomic write pattern.
//!
//! This crate implements a safe file writing strategy that helps prevent file corruption
//! in case of system crashes or power failures. It follows these steps:
//!
//! 1. Creates parent directories if they don't exist
//! 2. Writes content to a temporary file
//! 3. Ensures the content is fully written to disk
//! 4. Atomically renames the temporary file to the target path
//!
//! # Examples
//!
//! ```
//! use safe_write::safe_write;
//!
//! let content = b"Hello, World!";
//! safe_write("example.txt", content).expect("Failed to write file");
//! ```
//!
//! # Platform-specific behavior
//!
//! On Windows, if the target file exists, it will be explicitly removed before
//! the rename operation since Windows doesn't support atomic file replacement.
use fs_err as fs;
use std::io::{self, Write};
use std::path::Path;
use fs::OpenOptions;
/// Safely writes content to a file using an atomic write pattern.
///
/// # Arguments
///
/// * `path` - The path where the file should be written
/// * `content` - The bytes to write to the file
///
/// # Returns
///
/// Returns `io::Result<()>` which is:
/// * `Ok(())` if the write was successful
/// * `Err(e)` if any IO operation failed
pub fn safe_write<P: AsRef<Path>>(path: P, content: &[u8]) -> io::Result<()> {
let path = path.as_ref();
let parent = path.parent().unwrap_or_else(|| Path::new("."));
// Create parent directory if it doesn't exist
fs::create_dir_all(parent)?;
// Create a temporary file by appending .tmp to the original path
let temp_path = path.with_extension("tmp");
let mut temp_file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&temp_path)?;
// Write content
temp_file.write_all(content)?;
// Flush to OS buffers
temp_file.flush()?;
temp_file.sync_all()?;
// Close the file
drop(temp_file);
#[cfg(windows)]
{
if path.exists() {
fs::remove_file(path)?;
}
}
fs::rename(&temp_path, path)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_basic_write() -> io::Result<()> {
let temp_dir = TempDir::new()?;
let test_path = temp_dir.path().join("test.txt");
let content = b"Hello, World!";
safe_write(&test_path, content)?;
// Verify the content was written correctly
let read_content = fs::read(&test_path)?;
assert_eq!(content, read_content.as_slice());
Ok(())
}
#[test]
fn test_nested_directory_creation() -> io::Result<()> {
let temp_dir = TempDir::new()?;
let test_path = temp_dir.path().join("nested/dirs/test.txt");
let content = b"Nested content";
safe_write(&test_path, content)?;
assert!(test_path.exists());
let read_content = fs::read(&test_path)?;
assert_eq!(content, read_content.as_slice());
Ok(())
}
#[test]
fn test_overwrite_existing() -> io::Result<()> {
let temp_dir = TempDir::new()?;
let test_path = temp_dir.path().join("overwrite.txt");
// Write initial content
safe_write(&test_path, b"Initial content")?;
// Overwrite with new content
let new_content = b"New content";
safe_write(&test_path, new_content)?;
let read_content = fs::read(&test_path)?;
assert_eq!(new_content, read_content.as_slice());
Ok(())
}
}