ricecoder_files/
writer.rs1use 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#[derive(Debug, Clone)]
13pub struct SafeWriter {
14 verifier: ContentVerifier,
15 conflict_resolver: ConflictResolver,
16}
17
18impl SafeWriter {
19 pub fn new() -> Self {
21 SafeWriter {
22 verifier: ContentVerifier::new(),
23 conflict_resolver: ConflictResolver::new(),
24 }
25 }
26
27 pub async fn write(
39 &self,
40 path: &Path,
41 content: &str,
42 conflict_resolution: ConflictResolution,
43 ) -> Result<FileOperation, FileError> {
44 self.validate_content(content)?;
46
47 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 let _backup_path = if path.exists() {
59 let backup = self.create_backup(path).await?;
60 Some(backup)
61 } else {
62 None
63 };
64
65 let operation = match self.write_atomic(path, content).await {
67 Ok(op) => op,
68 Err(e) => {
69 return Err(e);
72 }
73 };
74
75 self.verifier.verify_write(path, content).await?;
77
78 Ok(operation)
79 }
80
81 fn validate_content(&self, content: &str) -> Result<(), FileError> {
91 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 async fn write_atomic(&self, path: &Path, content: &str) -> Result<FileOperation, FileError> {
112 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 let temp_path = self.temp_path(path);
121
122 fs::write(&temp_path, content).await?;
124
125 fs::rename(&temp_path, path).await?;
127
128 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 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 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 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 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 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 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 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 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 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 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 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}