tl_cli/
fs.rs

1//! File system utilities.
2
3use anyhow::Result;
4use std::fs;
5use std::path::Path;
6
7/// Writes content to a file atomically using a temp file and rename.
8///
9/// This prevents file corruption if the process is interrupted (e.g., Ctrl+C).
10/// The temp file is created in the same directory as the target file to ensure
11/// the rename operation is atomic (same filesystem).
12///
13/// # Errors
14///
15/// Returns an error if the temp file cannot be written or renamed.
16pub fn atomic_write(file_path: &str, content: &str) -> Result<()> {
17    let path = Path::new(file_path);
18    let parent = path.parent().unwrap_or_else(|| Path::new("."));
19    let file_name = path.file_name().unwrap_or_default().to_string_lossy();
20    let temp_path = parent.join(format!(".{file_name}.tmp"));
21
22    // Write to temp file first
23    fs::write(&temp_path, content)?;
24
25    // Atomic rename (same filesystem)
26    fs::rename(&temp_path, file_path)?;
27
28    Ok(())
29}
30
31#[cfg(test)]
32#[allow(clippy::unwrap_used)]
33mod tests {
34    use super::*;
35    use tempfile::TempDir;
36
37    #[test]
38    fn test_atomic_write_creates_file() {
39        let temp_dir = TempDir::new().unwrap();
40        let file_path = temp_dir.path().join("test.txt");
41        let file_path_str = file_path.to_str().unwrap();
42
43        atomic_write(file_path_str, "Hello, World!").unwrap();
44
45        let content = fs::read_to_string(&file_path).unwrap();
46        assert_eq!(content, "Hello, World!");
47    }
48
49    #[test]
50    fn test_atomic_write_overwrites_existing() {
51        let temp_dir = TempDir::new().unwrap();
52        let file_path = temp_dir.path().join("test.txt");
53        let file_path_str = file_path.to_str().unwrap();
54
55        fs::write(&file_path, "Original content").unwrap();
56        atomic_write(file_path_str, "New content").unwrap();
57
58        let content = fs::read_to_string(&file_path).unwrap();
59        assert_eq!(content, "New content");
60    }
61
62    #[test]
63    fn test_atomic_write_no_temp_file_remains() {
64        let temp_dir = TempDir::new().unwrap();
65        let file_path = temp_dir.path().join("test.txt");
66        let file_path_str = file_path.to_str().unwrap();
67
68        atomic_write(file_path_str, "content").unwrap();
69
70        // Temp file should not exist after successful write
71        let temp_path = temp_dir.path().join(".test.txt.tmp");
72        assert!(!temp_path.exists());
73    }
74
75    #[test]
76    fn test_atomic_write_unicode_content() {
77        let temp_dir = TempDir::new().unwrap();
78        let file_path = temp_dir.path().join("test.txt");
79        let file_path_str = file_path.to_str().unwrap();
80
81        let content = "こんにちは世界!🌍";
82        atomic_write(file_path_str, content).unwrap();
83
84        let read_content = fs::read_to_string(&file_path).unwrap();
85        assert_eq!(read_content, content);
86    }
87}