ricecoder_files/
writer.rs

1//! Safe file writing with atomic operations and conflict resolution
2
3use crate::conflict::ConflictResolver;
4use crate::error::FileError;
5use crate::models::{ConflictResolution, FileOperation, OperationType};
6use crate::verifier::ContentVerifier;
7use std::path::Path;
8use tokio::fs;
9use uuid::Uuid;
10
11/// Implements atomic file write pattern with content verification
12#[derive(Debug, Clone)]
13pub struct SafeWriter {
14    verifier: ContentVerifier,
15    conflict_resolver: ConflictResolver,
16}
17
18impl SafeWriter {
19    /// Creates a new SafeWriter instance
20    pub fn new() -> Self {
21        SafeWriter {
22            verifier: ContentVerifier::new(),
23            conflict_resolver: ConflictResolver::new(),
24        }
25    }
26
27    /// Writes a file safely with atomic operations and conflict resolution
28    ///
29    /// # Arguments
30    ///
31    /// * `path` - Path to the file to write
32    /// * `content` - Content to write
33    /// * `conflict_resolution` - Strategy for handling conflicts
34    ///
35    /// # Returns
36    ///
37    /// A FileOperation describing what was done, or an error
38    pub async fn write(
39        &self,
40        path: &Path,
41        content: &str,
42        conflict_resolution: ConflictResolution,
43    ) -> Result<FileOperation, FileError> {
44        // 1. Validate content
45        self.validate_content(content)?;
46
47        // 2. Check for conflicts
48        if let Some(conflict_info) = self
49            .conflict_resolver
50            .detect_conflict(path, content)
51            .await?
52        {
53            self.conflict_resolver
54                .resolve(conflict_resolution, &conflict_info)?;
55        }
56
57        // 3. Create backup if file exists
58        let _backup_path = if path.exists() {
59            let backup = self.create_backup(path).await?;
60            Some(backup)
61        } else {
62            None
63        };
64
65        // 4. Write to temp file and atomically rename
66        let operation = match self.write_atomic(path, content).await {
67            Ok(op) => op,
68            Err(e) => {
69                // If write fails, we don't need to restore backup here
70                // The caller can handle rollback if needed
71                return Err(e);
72            }
73        };
74
75        // 5. Verify written content
76        self.verifier.verify_write(path, content).await?;
77
78        Ok(operation)
79    }
80
81    /// Validates content before writing
82    ///
83    /// # Arguments
84    ///
85    /// * `content` - Content to validate
86    ///
87    /// # Returns
88    ///
89    /// Ok(()) if valid, error otherwise
90    fn validate_content(&self, content: &str) -> Result<(), FileError> {
91        // Basic validation: check for valid UTF-8 (already guaranteed by &str)
92        // Additional validation can be added here
93        if content.len() > 1_000_000_000 {
94            return Err(FileError::InvalidContent(
95                "Content exceeds maximum size".to_string(),
96            ));
97        }
98        Ok(())
99    }
100
101    /// Writes content to a temporary file and atomically renames it
102    ///
103    /// # Arguments
104    ///
105    /// * `path` - Target path
106    /// * `content` - Content to write
107    ///
108    /// # Returns
109    ///
110    /// FileOperation describing the write, or an error
111    async fn write_atomic(&self, path: &Path, content: &str) -> Result<FileOperation, FileError> {
112        // Create parent directory if it doesn't exist
113        if let Some(parent) = path.parent() {
114            if !parent.as_os_str().is_empty() {
115                fs::create_dir_all(parent).await?;
116            }
117        }
118
119        // Generate temporary file path
120        let temp_path = self.temp_path(path);
121
122        // Write to temporary file
123        fs::write(&temp_path, content).await?;
124
125        // Atomically rename to target path
126        fs::rename(&temp_path, path).await?;
127
128        // Compute content hash
129        let content_hash = ContentVerifier::compute_hash(content);
130
131        Ok(FileOperation {
132            path: path.to_path_buf(),
133            operation: OperationType::Update,
134            content: Some(content.to_string()),
135            backup_path: None,
136            content_hash: Some(content_hash),
137        })
138    }
139
140    /// Generates a temporary file path
141    ///
142    /// # Arguments
143    ///
144    /// * `path` - Original file path
145    ///
146    /// # Returns
147    ///
148    /// Temporary file path
149    fn temp_path(&self, path: &Path) -> std::path::PathBuf {
150        let mut temp_path = path.to_path_buf();
151        let file_name = format!(
152            ".tmp-{}-{}",
153            Uuid::new_v4(),
154            path.file_name().and_then(|n| n.to_str()).unwrap_or("file")
155        );
156        temp_path.set_file_name(file_name);
157        temp_path
158    }
159
160    /// Creates a backup of an existing file
161    ///
162    /// # Arguments
163    ///
164    /// * `path` - Path to the file to backup
165    ///
166    /// # Returns
167    ///
168    /// Path to the backup file, or an error
169    async fn create_backup(&self, path: &Path) -> Result<std::path::PathBuf, FileError> {
170        let content = fs::read_to_string(path).await?;
171        let backup_path = self.backup_path(path);
172
173        // Create backup directory if needed
174        if let Some(parent) = backup_path.parent() {
175            fs::create_dir_all(parent).await?;
176        }
177
178        fs::write(&backup_path, &content).await?;
179        Ok(backup_path)
180    }
181
182    /// Generates a backup file path
183    ///
184    /// # Arguments
185    ///
186    /// * `path` - Original file path
187    ///
188    /// # Returns
189    ///
190    /// Backup file path
191    fn backup_path(&self, path: &Path) -> std::path::PathBuf {
192        let mut backup_path = path.to_path_buf();
193        let file_name = format!(
194            ".backup-{}-{}",
195            Uuid::new_v4(),
196            path.file_name().and_then(|n| n.to_str()).unwrap_or("file")
197        );
198        backup_path.set_file_name(file_name);
199        backup_path
200    }
201}
202
203impl Default for SafeWriter {
204    fn default() -> Self {
205        Self::new()
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[tokio::test]
214    async fn test_write_new_file() {
215        let writer = SafeWriter::new();
216        let temp_dir = tempfile::tempdir().unwrap();
217        let path = temp_dir.path().join("new.txt");
218
219        let content = "test content";
220        let result = writer
221            .write(&path, content, ConflictResolution::Overwrite)
222            .await;
223
224        assert!(result.is_ok());
225        let operation = result.unwrap();
226        assert_eq!(operation.path, path);
227        assert!(operation.content_hash.is_some());
228
229        // Verify file was written
230        let written = fs::read_to_string(&path).await.unwrap();
231        assert_eq!(written, content);
232    }
233
234    #[tokio::test]
235    async fn test_write_with_overwrite_strategy() {
236        let writer = SafeWriter::new();
237        let temp_dir = tempfile::tempdir().unwrap();
238        let path = temp_dir.path().join("existing.txt");
239
240        // Create existing file
241        fs::write(&path, "old content").await.unwrap();
242
243        let new_content = "new content";
244        let result = writer
245            .write(&path, new_content, ConflictResolution::Overwrite)
246            .await;
247
248        assert!(result.is_ok());
249
250        // Verify file was overwritten
251        let written = fs::read_to_string(&path).await.unwrap();
252        assert_eq!(written, new_content);
253    }
254
255    #[tokio::test]
256    async fn test_write_with_skip_strategy() {
257        let writer = SafeWriter::new();
258        let temp_dir = tempfile::tempdir().unwrap();
259        let path = temp_dir.path().join("existing.txt");
260
261        // Create existing file
262        fs::write(&path, "old content").await.unwrap();
263
264        let new_content = "new content";
265        let result = writer
266            .write(&path, new_content, ConflictResolution::Skip)
267            .await;
268
269        assert!(result.is_err());
270        match result {
271            Err(FileError::ConflictDetected(_)) => (),
272            _ => panic!("Expected ConflictDetected error"),
273        }
274
275        // Verify file was not changed
276        let written = fs::read_to_string(&path).await.unwrap();
277        assert_eq!(written, "old content");
278    }
279
280    #[tokio::test]
281    async fn test_write_creates_parent_directories() {
282        let writer = SafeWriter::new();
283        let temp_dir = tempfile::tempdir().unwrap();
284        let path = temp_dir.path().join("subdir/nested/file.txt");
285
286        let content = "test content";
287        let result = writer
288            .write(&path, content, ConflictResolution::Overwrite)
289            .await;
290
291        assert!(result.is_ok());
292        assert!(path.exists());
293
294        let written = fs::read_to_string(&path).await.unwrap();
295        assert_eq!(written, content);
296    }
297
298    #[tokio::test]
299    async fn test_write_invalid_content_too_large() {
300        let writer = SafeWriter::new();
301        let temp_dir = tempfile::tempdir().unwrap();
302        let path = temp_dir.path().join("large.txt");
303
304        // Create content larger than 1GB (simulated)
305        let large_content = "x".repeat(1_000_000_001);
306
307        let result = writer
308            .write(&path, &large_content, ConflictResolution::Overwrite)
309            .await;
310
311        assert!(result.is_err());
312        match result {
313            Err(FileError::InvalidContent(_)) => (),
314            _ => panic!("Expected InvalidContent error"),
315        }
316    }
317
318    #[test]
319    fn test_validate_content_valid() {
320        let writer = SafeWriter::new();
321        let result = writer.validate_content("valid content");
322        assert!(result.is_ok());
323    }
324
325    #[test]
326    fn test_validate_content_empty() {
327        let writer = SafeWriter::new();
328        let result = writer.validate_content("");
329        assert!(result.is_ok());
330    }
331
332    #[tokio::test]
333    async fn test_write_with_merge_strategy() {
334        let writer = SafeWriter::new();
335        let temp_dir = tempfile::tempdir().unwrap();
336        let path = temp_dir.path().join("merge.txt");
337
338        // Create existing file
339        fs::write(&path, "existing content").await.unwrap();
340
341        let new_content = "new content";
342        let result = writer
343            .write(&path, new_content, ConflictResolution::Merge)
344            .await;
345
346        assert!(result.is_ok());
347    }
348}