safe_write/
lib.rs

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