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    let mut temp_file = OpenOptions::new()
55        .write(true)
56        .create_new(true)
57        .open(&temp_path)?;
58
59    // Write content
60    temp_file.write_all(content)?;
61    // Flush to OS buffers
62    temp_file.flush()?;
63    temp_file.sync_all()?;
64    // Close the file
65    drop(temp_file);
66
67    #[cfg(windows)]
68    {
69        if path.exists() {
70            fs::remove_file(path)?;
71        }
72    }
73    fs::rename(&temp_path, path)?;
74    Ok(())
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use tempfile::TempDir;
81
82    #[test]
83    fn test_basic_write() -> io::Result<()> {
84        let temp_dir = TempDir::new()?;
85        let test_path = temp_dir.path().join("test.txt");
86
87        let content = b"Hello, World!";
88        safe_write(&test_path, content)?;
89
90        // Verify the content was written correctly
91        let read_content = fs::read(&test_path)?;
92        assert_eq!(content, read_content.as_slice());
93
94        Ok(())
95    }
96
97    #[test]
98    fn test_nested_directory_creation() -> io::Result<()> {
99        let temp_dir = TempDir::new()?;
100        let test_path = temp_dir.path().join("nested/dirs/test.txt");
101
102        let content = b"Nested content";
103        safe_write(&test_path, content)?;
104
105        assert!(test_path.exists());
106        let read_content = fs::read(&test_path)?;
107        assert_eq!(content, read_content.as_slice());
108
109        Ok(())
110    }
111
112    #[test]
113    fn test_overwrite_existing() -> io::Result<()> {
114        let temp_dir = TempDir::new()?;
115        let test_path = temp_dir.path().join("overwrite.txt");
116
117        // Write initial content
118        safe_write(&test_path, b"Initial content")?;
119
120        // Overwrite with new content
121        let new_content = b"New content";
122        safe_write(&test_path, new_content)?;
123
124        let read_content = fs::read(&test_path)?;
125        assert_eq!(new_content, read_content.as_slice());
126
127        Ok(())
128    }
129}