1use anyhow::Result;
2use async_trait::async_trait;
3use std::path::Path;
4
5#[async_trait]
6pub trait FileSystemOperations: Send + Sync {
7 async fn create_dir(&self, path: &Path) -> Result<()>;
8 async fn copy_dir(&self, from: &Path, to: &Path) -> Result<()>;
9 async fn write_file(&self, path: &Path, content: &str) -> Result<()>;
10 async fn read_file(&self, path: &Path) -> Result<String>;
11 async fn exists(&self, path: &Path) -> Result<bool>;
12 async fn remove_dir(&self, path: &Path) -> Result<()>;
13 async fn remove_file(&self, path: &Path) -> Result<()>;
14 async fn copy_file(&self, from: &Path, to: &Path) -> Result<()>;
15 async fn read_dir(&self, path: &Path) -> Result<Vec<std::path::PathBuf>>;
16}
17
18pub struct DefaultFileSystem;
19
20#[async_trait]
21impl FileSystemOperations for DefaultFileSystem {
22 async fn create_dir(&self, path: &Path) -> Result<()> {
23 tokio::fs::create_dir_all(path).await?;
24 Ok(())
25 }
26
27 async fn copy_dir(&self, from: &Path, to: &Path) -> Result<()> {
28 self.create_dir(to).await?;
29
30 let mut entries = tokio::fs::read_dir(from).await?;
31 while let Some(entry) = entries.next_entry().await? {
32 let path = entry.path();
33 let relative_path = path.strip_prefix(from)?;
34 let dst_path = to.join(relative_path);
35
36 if entry.file_type().await?.is_dir() {
37 self.copy_dir(&path, &dst_path).await?;
38 } else {
39 if let Some(parent) = dst_path.parent() {
40 self.create_dir(parent).await?;
41 }
42 self.copy_file(&path, &dst_path).await?;
43 }
44 }
45
46 Ok(())
47 }
48
49 async fn write_file(&self, path: &Path, content: &str) -> Result<()> {
50 if let Some(parent) = path.parent() {
51 self.create_dir(parent).await?;
52 }
53 tokio::fs::write(path, content).await?;
54 Ok(())
55 }
56
57 async fn read_file(&self, path: &Path) -> Result<String> {
58 let content = tokio::fs::read_to_string(path).await?;
59 Ok(content)
60 }
61
62 async fn exists(&self, path: &Path) -> Result<bool> {
63 Ok(tokio::fs::try_exists(path).await.unwrap_or(false))
64 }
65
66 async fn remove_dir(&self, path: &Path) -> Result<()> {
67 tokio::fs::remove_dir_all(path).await?;
68 Ok(())
69 }
70
71 async fn remove_file(&self, path: &Path) -> Result<()> {
72 tokio::fs::remove_file(path).await?;
73 Ok(())
74 }
75
76 async fn copy_file(&self, from: &Path, to: &Path) -> Result<()> {
77 tokio::fs::copy(from, to).await?;
78 Ok(())
79 }
80
81 async fn read_dir(&self, path: &Path) -> Result<Vec<std::path::PathBuf>> {
82 let mut entries = tokio::fs::read_dir(path).await?;
83 let mut paths = Vec::new();
84
85 while let Some(entry) = entries.next_entry().await? {
86 paths.push(entry.path());
87 }
88
89 Ok(paths)
90 }
91}
92
93#[cfg(test)]
94pub(crate) mod tests {
95 use super::*;
96 use std::collections::HashMap;
97 use std::path::PathBuf;
98 use std::sync::{Arc, Mutex};
99
100 #[derive(Clone)]
101 pub struct MockFileSystem {
102 files: Arc<Mutex<HashMap<String, String>>>,
103 dirs: Arc<Mutex<Vec<String>>>,
104 }
105
106 impl MockFileSystem {
107 pub fn new() -> Self {
108 Self {
109 files: Arc::new(Mutex::new(HashMap::new())),
110 dirs: Arc::new(Mutex::new(Vec::new())),
111 }
112 }
113
114 pub fn with_file(self, path: &str, content: &str) -> Self {
115 self.files
116 .lock()
117 .unwrap()
118 .insert(path.to_string(), content.to_string());
119 self
120 }
121
122 pub fn with_dir(self, path: &str) -> Self {
123 self.dirs.lock().unwrap().push(path.to_string());
124 self
125 }
126
127 pub fn get_files(&self) -> HashMap<String, String> {
128 self.files.lock().unwrap().clone()
129 }
130
131 pub fn get_dirs(&self) -> Vec<String> {
132 self.dirs.lock().unwrap().clone()
133 }
134
135 pub fn set_files(&self, files: HashMap<PathBuf, String>) {
136 let mut file_map = self.files.lock().unwrap();
137 file_map.clear();
138 for (path, content) in files {
139 let path_str = path.to_string_lossy().to_string();
140 if content == "dir" {
141 self.dirs.lock().unwrap().push(path_str);
142 } else {
143 file_map.insert(path_str, content);
144 }
145 }
146 }
147 }
148
149 #[async_trait]
150 impl FileSystemOperations for MockFileSystem {
151 async fn create_dir(&self, path: &Path) -> Result<()> {
152 let path_str = path.to_string_lossy().to_string();
153 self.dirs.lock().unwrap().push(path_str);
154 Ok(())
155 }
156
157 async fn copy_dir(&self, from: &Path, to: &Path) -> Result<()> {
158 let from_str = from.to_string_lossy().to_string();
159 let to_str = to.to_string_lossy().to_string();
160
161 let files = self.files.lock().unwrap();
163 let mut new_files = HashMap::new();
164
165 for (path, content) in files.iter() {
166 if path.starts_with(&from_str) {
167 let relative = path.strip_prefix(&from_str).unwrap();
168 let new_path = format!("{to_str}{relative}");
169 new_files.insert(new_path, content.clone());
170 }
171 }
172
173 drop(files);
174 self.files.lock().unwrap().extend(new_files);
175
176 let dirs = self.dirs.lock().unwrap();
178 let mut new_dirs = Vec::new();
179
180 for dir in dirs.iter() {
181 if dir.starts_with(&from_str) {
182 let relative = dir.strip_prefix(&from_str).unwrap();
183 let new_dir = format!("{to_str}{relative}");
184 new_dirs.push(new_dir);
185 }
186 }
187
188 drop(dirs);
189 self.dirs.lock().unwrap().extend(new_dirs);
190 self.dirs.lock().unwrap().push(to_str);
191 Ok(())
192 }
193
194 async fn write_file(&self, path: &Path, content: &str) -> Result<()> {
195 let path_str = path.to_string_lossy().to_string();
196 self.files
197 .lock()
198 .unwrap()
199 .insert(path_str, content.to_string());
200 Ok(())
201 }
202
203 async fn read_file(&self, path: &Path) -> Result<String> {
204 let path_str = path.to_string_lossy().to_string();
205 self.files
206 .lock()
207 .unwrap()
208 .get(&path_str)
209 .cloned()
210 .ok_or_else(|| anyhow::anyhow!("File not found: {}", path_str))
211 }
212
213 async fn exists(&self, path: &Path) -> Result<bool> {
214 let path_str = path.to_string_lossy().to_string();
215 let files = self.files.lock().unwrap();
216 let dirs = self.dirs.lock().unwrap();
217 Ok(files.contains_key(&path_str) || dirs.contains(&path_str))
218 }
219
220 async fn remove_dir(&self, path: &Path) -> Result<()> {
221 let path_str = path.to_string_lossy().to_string();
222 self.dirs
223 .lock()
224 .unwrap()
225 .retain(|p| !p.starts_with(&path_str));
226 self.files
227 .lock()
228 .unwrap()
229 .retain(|p, _| !p.starts_with(&path_str));
230 Ok(())
231 }
232
233 async fn remove_file(&self, path: &Path) -> Result<()> {
234 let path_str = path.to_string_lossy().to_string();
235 self.files
236 .lock()
237 .unwrap()
238 .remove(&path_str)
239 .ok_or_else(|| anyhow::anyhow!("File not found: {}", path_str))?;
240 Ok(())
241 }
242
243 async fn copy_file(&self, from: &Path, to: &Path) -> Result<()> {
244 let from_str = from.to_string_lossy().to_string();
245 let to_str = to.to_string_lossy().to_string();
246
247 let content = self
248 .files
249 .lock()
250 .unwrap()
251 .get(&from_str)
252 .cloned()
253 .ok_or_else(|| anyhow::anyhow!("Source file not found: {}", from_str))?;
254
255 self.files.lock().unwrap().insert(to_str, content);
256 Ok(())
257 }
258
259 async fn read_dir(&self, path: &Path) -> Result<Vec<std::path::PathBuf>> {
260 let path_str = path.to_string_lossy().to_string();
261 let files = self.files.lock().unwrap();
262 let dirs = self.dirs.lock().unwrap();
263
264 if !dirs.contains(&path_str) {
266 return Err(anyhow::anyhow!("Directory not found: {}", path_str));
267 }
268
269 let mut entries = Vec::new();
270
271 for file_path in files.keys() {
272 if file_path.starts_with(&path_str) && file_path != &path_str {
273 let relative = file_path
274 .strip_prefix(&path_str)
275 .unwrap()
276 .trim_start_matches('/');
277 if !relative.contains('/') {
278 entries.push(std::path::PathBuf::from(file_path));
279 }
280 }
281 }
282
283 for dir_path in dirs.iter() {
284 if dir_path.starts_with(&path_str) && dir_path != &path_str {
285 let relative = dir_path
286 .strip_prefix(&path_str)
287 .unwrap()
288 .trim_start_matches('/');
289 if !relative.contains('/') {
290 entries.push(std::path::PathBuf::from(dir_path));
291 }
292 }
293 }
294
295 Ok(entries)
296 }
297 }
298
299 #[cfg(test)]
300 mod integration_tests {
301 use super::*;
302 use std::path::Path;
303 use std::sync::Arc;
304 use tempfile::TempDir;
305
306 #[tokio::test]
307 async fn test_default_file_system_create_and_read_file() {
308 let temp_dir = TempDir::new().unwrap();
309 let fs = DefaultFileSystem;
310
311 let file_path = temp_dir.path().join("test.txt");
312 let content = "Hello, world!";
313
314 fs.write_file(&file_path, content).await.unwrap();
316
317 let read_content = fs.read_file(&file_path).await.unwrap();
319 assert_eq!(read_content, content);
320
321 assert!(fs.exists(&file_path).await.unwrap());
323 }
324
325 #[tokio::test]
326 async fn test_default_file_system_create_dir() {
327 let temp_dir = TempDir::new().unwrap();
328 let fs = DefaultFileSystem;
329
330 let dir_path = temp_dir.path().join("test_dir");
331
332 fs.create_dir(&dir_path).await.unwrap();
334
335 assert!(fs.exists(&dir_path).await.unwrap());
337 }
338
339 #[tokio::test]
340 async fn test_default_file_system_copy_file() {
341 let temp_dir = TempDir::new().unwrap();
342 let fs = DefaultFileSystem;
343
344 let source_path = temp_dir.path().join("source.txt");
345 let dest_path = temp_dir.path().join("dest.txt");
346 let content = "Test content";
347
348 fs.write_file(&source_path, content).await.unwrap();
350
351 fs.copy_file(&source_path, &dest_path).await.unwrap();
353
354 assert!(fs.exists(&source_path).await.unwrap());
356 assert!(fs.exists(&dest_path).await.unwrap());
357
358 let dest_content = fs.read_file(&dest_path).await.unwrap();
359 assert_eq!(dest_content, content);
360 }
361
362 #[tokio::test]
363 async fn test_default_file_system_copy_dir() {
364 let temp_dir = TempDir::new().unwrap();
365 let fs = DefaultFileSystem;
366
367 let source_dir = temp_dir.path().join("source_dir");
368 let dest_dir = temp_dir.path().join("dest_dir");
369
370 fs.create_dir(&source_dir).await.unwrap();
372 fs.write_file(&source_dir.join("file1.txt"), "content1")
373 .await
374 .unwrap();
375 fs.create_dir(&source_dir.join("subdir")).await.unwrap();
376 fs.write_file(&source_dir.join("subdir").join("file2.txt"), "content2")
377 .await
378 .unwrap();
379
380 fs.copy_dir(&source_dir, &dest_dir).await.unwrap();
382
383 assert!(fs.exists(&dest_dir).await.unwrap());
385 assert!(fs.exists(&dest_dir.join("file1.txt")).await.unwrap());
386 assert!(fs.exists(&dest_dir.join("subdir")).await.unwrap());
387 assert!(
388 fs.exists(&dest_dir.join("subdir").join("file2.txt"))
389 .await
390 .unwrap()
391 );
392
393 let content1 = fs.read_file(&dest_dir.join("file1.txt")).await.unwrap();
395 assert_eq!(content1, "content1");
396
397 let content2 = fs
398 .read_file(&dest_dir.join("subdir").join("file2.txt"))
399 .await
400 .unwrap();
401 assert_eq!(content2, "content2");
402 }
403
404 #[tokio::test]
405 async fn test_default_file_system_read_dir() {
406 let temp_dir = TempDir::new().unwrap();
407 let fs = DefaultFileSystem;
408
409 let test_dir = temp_dir.path().join("test_dir");
410 fs.create_dir(&test_dir).await.unwrap();
411
412 fs.write_file(&test_dir.join("file1.txt"), "content1")
414 .await
415 .unwrap();
416 fs.write_file(&test_dir.join("file2.txt"), "content2")
417 .await
418 .unwrap();
419 fs.create_dir(&test_dir.join("subdir")).await.unwrap();
420
421 let entries = fs.read_dir(&test_dir).await.unwrap();
423
424 assert_eq!(entries.len(), 3);
426
427 let entry_names: Vec<String> = entries
429 .iter()
430 .map(|p| p.file_name().unwrap().to_string_lossy().to_string())
431 .collect();
432
433 assert!(entry_names.contains(&"file1.txt".to_string()));
434 assert!(entry_names.contains(&"file2.txt".to_string()));
435 assert!(entry_names.contains(&"subdir".to_string()));
436 }
437
438 #[tokio::test]
439 async fn test_default_file_system_remove_dir() {
440 let temp_dir = TempDir::new().unwrap();
441 let fs = DefaultFileSystem;
442
443 let test_dir = temp_dir.path().join("test_dir");
444
445 fs.create_dir(&test_dir).await.unwrap();
447 fs.write_file(&test_dir.join("file.txt"), "content")
448 .await
449 .unwrap();
450
451 assert!(fs.exists(&test_dir).await.unwrap());
453
454 fs.remove_dir(&test_dir).await.unwrap();
456
457 assert!(!fs.exists(&test_dir).await.unwrap());
459 }
460
461 #[tokio::test]
462 async fn test_mock_file_system() {
463 let mock_fs = MockFileSystem::new()
464 .with_file("/test/file.txt", "test content")
465 .with_dir("/test");
466
467 assert!(mock_fs.exists(Path::new("/test/file.txt")).await.unwrap());
469
470 assert!(mock_fs.exists(Path::new("/test")).await.unwrap());
472
473 let content = mock_fs
475 .read_file(Path::new("/test/file.txt"))
476 .await
477 .unwrap();
478 assert_eq!(content, "test content");
479
480 mock_fs
482 .write_file(Path::new("/test/new.txt"), "new content")
483 .await
484 .unwrap();
485 let new_content = mock_fs.read_file(Path::new("/test/new.txt")).await.unwrap();
486 assert_eq!(new_content, "new content");
487
488 mock_fs.create_dir(Path::new("/new_dir")).await.unwrap();
490 assert!(mock_fs.exists(Path::new("/new_dir")).await.unwrap());
491
492 mock_fs
494 .copy_file(Path::new("/test/file.txt"), Path::new("/test/copy.txt"))
495 .await
496 .unwrap();
497 let copied = mock_fs
498 .read_file(Path::new("/test/copy.txt"))
499 .await
500 .unwrap();
501 assert_eq!(copied, "test content");
502
503 mock_fs.remove_dir(Path::new("/test")).await.unwrap();
505 assert!(!mock_fs.exists(Path::new("/test")).await.unwrap());
506 assert!(!mock_fs.exists(Path::new("/test/file.txt")).await.unwrap());
507 }
508
509 #[tokio::test]
510 async fn test_mock_file_system_copy_dir() {
511 let mock_fs = MockFileSystem::new()
512 .with_dir("/source")
513 .with_file("/source/file1.txt", "content1")
514 .with_dir("/source/subdir")
515 .with_file("/source/subdir/file2.txt", "content2");
516
517 mock_fs
519 .copy_dir(Path::new("/source"), Path::new("/dest"))
520 .await
521 .unwrap();
522
523 assert!(mock_fs.exists(Path::new("/dest")).await.unwrap());
525 assert_eq!(
526 mock_fs
527 .read_file(Path::new("/dest/file1.txt"))
528 .await
529 .unwrap(),
530 "content1"
531 );
532 assert_eq!(
533 mock_fs
534 .read_file(Path::new("/dest/subdir/file2.txt"))
535 .await
536 .unwrap(),
537 "content2"
538 );
539 }
540
541 #[tokio::test]
542 async fn test_mock_file_system_read_dir() {
543 let mock_fs = MockFileSystem::new()
544 .with_dir("/test")
545 .with_file("/test/file1.txt", "content1")
546 .with_file("/test/file2.txt", "content2")
547 .with_dir("/test/subdir")
548 .with_file("/test/subdir/nested.txt", "nested");
549
550 let entries = mock_fs.read_dir(Path::new("/test")).await.unwrap();
552
553 assert_eq!(entries.len(), 3);
555
556 let entry_strs: Vec<String> = entries
558 .iter()
559 .map(|p| p.to_string_lossy().to_string())
560 .collect();
561
562 assert!(entry_strs.contains(&"/test/file1.txt".to_string()));
563 assert!(entry_strs.contains(&"/test/file2.txt".to_string()));
564 assert!(entry_strs.contains(&"/test/subdir".to_string()));
565
566 assert!(!entry_strs.contains(&"/test/subdir/nested.txt".to_string()));
568 }
569
570 #[tokio::test]
571 async fn test_default_file_system_remove_file() {
572 let temp_dir = TempDir::new().unwrap();
573 let fs = DefaultFileSystem;
574
575 let file_path = temp_dir.path().join("test.txt");
576 let content = "Test content";
577
578 fs.write_file(&file_path, content).await.unwrap();
580
581 assert!(fs.exists(&file_path).await.unwrap());
583
584 fs.remove_file(&file_path).await.unwrap();
586
587 assert!(!fs.exists(&file_path).await.unwrap());
589
590 let result = fs.remove_file(&file_path).await;
592 assert!(result.is_err());
593 }
594
595 #[tokio::test]
596 async fn test_mock_file_system_remove_file() {
597 let mock_fs = MockFileSystem::new().with_file("/test/file.txt", "test content");
598
599 assert!(mock_fs.exists(Path::new("/test/file.txt")).await.unwrap());
601
602 mock_fs
604 .remove_file(Path::new("/test/file.txt"))
605 .await
606 .unwrap();
607
608 assert!(!mock_fs.exists(Path::new("/test/file.txt")).await.unwrap());
610
611 let result = mock_fs.remove_file(Path::new("/test/file.txt")).await;
613 assert!(result.is_err());
614 assert!(result.unwrap_err().to_string().contains("File not found"));
615 }
616
617 #[tokio::test]
618 async fn test_file_system_operations_are_send_sync() {
619 fn assert_send_sync<T: Send + Sync>() {}
620
621 assert_send_sync::<DefaultFileSystem>();
622 assert_send_sync::<MockFileSystem>();
623 assert_send_sync::<Arc<dyn FileSystemOperations>>();
624 }
625 }
626}