1use std::{
4 fs,
5 path::{Path, PathBuf},
6};
7
8use bytes::Bytes;
9
10use super::StorageBackend;
11use crate::error::{Error, Result};
12
13#[derive(Debug, Clone)]
25pub struct LocalBackend {
26 root: PathBuf,
27}
28
29impl LocalBackend {
30 pub fn new(root: impl AsRef<Path>) -> Result<Self> {
38 let root = root.as_ref().to_path_buf();
39 fs::create_dir_all(&root).map_err(|e| Error::io(e, &root))?;
40 Ok(Self { root })
41 }
42
43 pub fn root(&self) -> &Path {
45 &self.root
46 }
47
48 fn resolve_path(&self, key: &str) -> PathBuf {
50 self.root.join(key)
51 }
52}
53
54impl StorageBackend for LocalBackend {
55 fn list(&self, prefix: &str) -> Result<Vec<String>> {
56 let search_path = self.resolve_path(prefix);
57
58 let (dir_to_search, file_prefix) = if search_path.is_dir() {
61 (search_path, String::new())
62 } else {
63 let parent = search_path
64 .parent()
65 .map(|p| p.to_path_buf())
66 .unwrap_or_else(|| self.root.clone());
67 let prefix_name = search_path
68 .file_name()
69 .map(|s| s.to_string_lossy().to_string())
70 .unwrap_or_default();
71 (parent, prefix_name)
72 };
73
74 if !dir_to_search.exists() {
75 return Ok(Vec::new());
76 }
77
78 let mut results = Vec::new();
79 self.list_recursive(&dir_to_search, &file_prefix, &mut results)?;
80 Ok(results)
81 }
82
83 fn get(&self, key: &str) -> Result<Bytes> {
84 let path = self.resolve_path(key);
85 let data = fs::read(&path).map_err(|e| Error::io(e, &path))?;
86 Ok(Bytes::from(data))
87 }
88
89 fn put(&self, key: &str, data: Bytes) -> Result<()> {
90 let path = self.resolve_path(key);
91
92 if let Some(parent) = path.parent() {
94 fs::create_dir_all(parent).map_err(|e| Error::io(e, parent))?;
95 }
96
97 fs::write(&path, &data).map_err(|e| Error::io(e, &path))
98 }
99
100 fn delete(&self, key: &str) -> Result<()> {
101 let path = self.resolve_path(key);
102 if path.exists() {
103 fs::remove_file(&path).map_err(|e| Error::io(e, &path))?;
104 }
105 Ok(())
106 }
107
108 fn exists(&self, key: &str) -> Result<bool> {
109 let path = self.resolve_path(key);
110 Ok(path.exists())
111 }
112
113 fn size(&self, key: &str) -> Result<u64> {
114 let path = self.resolve_path(key);
115 let metadata = fs::metadata(&path).map_err(|e| Error::io(e, &path))?;
116 Ok(metadata.len())
117 }
118}
119
120impl LocalBackend {
121 fn list_recursive(&self, dir: &Path, prefix: &str, results: &mut Vec<String>) -> Result<()> {
123 let entries = fs::read_dir(dir).map_err(|e| Error::io(e, dir))?;
124
125 for entry in entries {
126 let entry = entry.map_err(|e| Error::io(e, dir))?;
127 let path = entry.path();
128 let file_name = entry.file_name().to_string_lossy().to_string();
129
130 if !prefix.is_empty() && !file_name.starts_with(prefix) {
132 continue;
133 }
134
135 if path.is_file() {
136 if let Ok(relative) = path.strip_prefix(&self.root) {
138 results.push(relative.to_string_lossy().to_string());
139 }
140 } else if path.is_dir() {
141 self.list_recursive(&path, "", results)?;
143 }
144 }
145
146 Ok(())
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn test_new_backend() {
156 let temp_dir = tempfile::tempdir()
157 .ok()
158 .unwrap_or_else(|| panic!("Should create temp dir"));
159 let backend = LocalBackend::new(temp_dir.path());
160 assert!(backend.is_ok());
161 }
162
163 #[test]
164 fn test_put_and_get() {
165 let temp_dir = tempfile::tempdir()
166 .ok()
167 .unwrap_or_else(|| panic!("Should create temp dir"));
168 let backend = LocalBackend::new(temp_dir.path())
169 .ok()
170 .unwrap_or_else(|| panic!("Should create backend"));
171
172 let data = Bytes::from("hello world");
173 backend
174 .put("test.txt", data.clone())
175 .ok()
176 .unwrap_or_else(|| panic!("Should put data"));
177
178 let retrieved = backend
179 .get("test.txt")
180 .ok()
181 .unwrap_or_else(|| panic!("Should get data"));
182 assert_eq!(retrieved, data);
183 }
184
185 #[test]
186 fn test_put_creates_directories() {
187 let temp_dir = tempfile::tempdir()
188 .ok()
189 .unwrap_or_else(|| panic!("Should create temp dir"));
190 let backend = LocalBackend::new(temp_dir.path())
191 .ok()
192 .unwrap_or_else(|| panic!("Should create backend"));
193
194 let data = Bytes::from("nested data");
195 backend
196 .put("a/b/c/test.txt", data)
197 .ok()
198 .unwrap_or_else(|| panic!("Should put nested data"));
199
200 assert!(backend
201 .exists("a/b/c/test.txt")
202 .ok()
203 .unwrap_or_else(|| panic!("Should check exists")));
204 }
205
206 #[test]
207 fn test_exists() {
208 let temp_dir = tempfile::tempdir()
209 .ok()
210 .unwrap_or_else(|| panic!("Should create temp dir"));
211 let backend = LocalBackend::new(temp_dir.path())
212 .ok()
213 .unwrap_or_else(|| panic!("Should create backend"));
214
215 assert!(!backend
216 .exists("nonexistent.txt")
217 .ok()
218 .unwrap_or_else(|| panic!("Should check exists")));
219
220 backend
221 .put("exists.txt", Bytes::from("data"))
222 .ok()
223 .unwrap_or_else(|| panic!("Should put data"));
224
225 assert!(backend
226 .exists("exists.txt")
227 .ok()
228 .unwrap_or_else(|| panic!("Should check exists")));
229 }
230
231 #[test]
232 fn test_delete() {
233 let temp_dir = tempfile::tempdir()
234 .ok()
235 .unwrap_or_else(|| panic!("Should create temp dir"));
236 let backend = LocalBackend::new(temp_dir.path())
237 .ok()
238 .unwrap_or_else(|| panic!("Should create backend"));
239
240 backend
241 .put("to_delete.txt", Bytes::from("data"))
242 .ok()
243 .unwrap_or_else(|| panic!("Should put data"));
244
245 assert!(backend
246 .exists("to_delete.txt")
247 .ok()
248 .unwrap_or_else(|| panic!("Should exist")));
249
250 backend
251 .delete("to_delete.txt")
252 .ok()
253 .unwrap_or_else(|| panic!("Should delete"));
254
255 assert!(!backend
256 .exists("to_delete.txt")
257 .ok()
258 .unwrap_or_else(|| panic!("Should not exist")));
259 }
260
261 #[test]
262 fn test_delete_nonexistent_is_ok() {
263 let temp_dir = tempfile::tempdir()
264 .ok()
265 .unwrap_or_else(|| panic!("Should create temp dir"));
266 let backend = LocalBackend::new(temp_dir.path())
267 .ok()
268 .unwrap_or_else(|| panic!("Should create backend"));
269
270 let result = backend.delete("does_not_exist.txt");
272 assert!(result.is_ok());
273 }
274
275 #[test]
276 fn test_size() {
277 let temp_dir = tempfile::tempdir()
278 .ok()
279 .unwrap_or_else(|| panic!("Should create temp dir"));
280 let backend = LocalBackend::new(temp_dir.path())
281 .ok()
282 .unwrap_or_else(|| panic!("Should create backend"));
283
284 let data = Bytes::from("12345678901234567890"); backend
286 .put("sized.txt", data)
287 .ok()
288 .unwrap_or_else(|| panic!("Should put data"));
289
290 let size = backend
291 .size("sized.txt")
292 .ok()
293 .unwrap_or_else(|| panic!("Should get size"));
294 assert_eq!(size, 20);
295 }
296
297 #[test]
298 fn test_list_empty() {
299 let temp_dir = tempfile::tempdir()
300 .ok()
301 .unwrap_or_else(|| panic!("Should create temp dir"));
302 let backend = LocalBackend::new(temp_dir.path())
303 .ok()
304 .unwrap_or_else(|| panic!("Should create backend"));
305
306 let files = backend
307 .list("")
308 .ok()
309 .unwrap_or_else(|| panic!("Should list"));
310 assert!(files.is_empty());
311 }
312
313 #[test]
314 fn test_list_files() {
315 let temp_dir = tempfile::tempdir()
316 .ok()
317 .unwrap_or_else(|| panic!("Should create temp dir"));
318 let backend = LocalBackend::new(temp_dir.path())
319 .ok()
320 .unwrap_or_else(|| panic!("Should create backend"));
321
322 backend
323 .put("file1.txt", Bytes::from("a"))
324 .ok()
325 .unwrap_or_else(|| panic!("Should put"));
326 backend
327 .put("file2.txt", Bytes::from("b"))
328 .ok()
329 .unwrap_or_else(|| panic!("Should put"));
330 backend
331 .put("other.txt", Bytes::from("c"))
332 .ok()
333 .unwrap_or_else(|| panic!("Should put"));
334
335 let all_files = backend
336 .list("")
337 .ok()
338 .unwrap_or_else(|| panic!("Should list"));
339 assert_eq!(all_files.len(), 3);
340
341 let file_files = backend
342 .list("file")
343 .ok()
344 .unwrap_or_else(|| panic!("Should list"));
345 assert_eq!(file_files.len(), 2);
346 }
347
348 #[test]
349 fn test_list_nested() {
350 let temp_dir = tempfile::tempdir()
351 .ok()
352 .unwrap_or_else(|| panic!("Should create temp dir"));
353 let backend = LocalBackend::new(temp_dir.path())
354 .ok()
355 .unwrap_or_else(|| panic!("Should create backend"));
356
357 backend
358 .put("dir1/file1.txt", Bytes::from("a"))
359 .ok()
360 .unwrap_or_else(|| panic!("Should put"));
361 backend
362 .put("dir1/file2.txt", Bytes::from("b"))
363 .ok()
364 .unwrap_or_else(|| panic!("Should put"));
365 backend
366 .put("dir2/file3.txt", Bytes::from("c"))
367 .ok()
368 .unwrap_or_else(|| panic!("Should put"));
369
370 let all_files = backend
371 .list("")
372 .ok()
373 .unwrap_or_else(|| panic!("Should list"));
374 assert_eq!(all_files.len(), 3);
375
376 let dir1_files = backend
377 .list("dir1")
378 .ok()
379 .unwrap_or_else(|| panic!("Should list"));
380 assert_eq!(dir1_files.len(), 2);
381 }
382
383 #[test]
384 fn test_root() {
385 let temp_dir = tempfile::tempdir()
386 .ok()
387 .unwrap_or_else(|| panic!("Should create temp dir"));
388 let backend = LocalBackend::new(temp_dir.path())
389 .ok()
390 .unwrap_or_else(|| panic!("Should create backend"));
391
392 assert_eq!(backend.root(), temp_dir.path());
393 }
394
395 #[test]
396 fn test_get_nonexistent_error() {
397 let temp_dir = tempfile::tempdir()
398 .ok()
399 .unwrap_or_else(|| panic!("Should create temp dir"));
400 let backend = LocalBackend::new(temp_dir.path())
401 .ok()
402 .unwrap_or_else(|| panic!("Should create backend"));
403
404 let result = backend.get("nonexistent.txt");
405 assert!(result.is_err());
406 }
407
408 #[test]
409 fn test_size_nonexistent_error() {
410 let temp_dir = tempfile::tempdir()
411 .ok()
412 .unwrap_or_else(|| panic!("Should create temp dir"));
413 let backend = LocalBackend::new(temp_dir.path())
414 .ok()
415 .unwrap_or_else(|| panic!("Should create backend"));
416
417 let result = backend.size("nonexistent.txt");
418 assert!(result.is_err());
419 }
420
421 #[test]
422 fn test_debug() {
423 let temp_dir = tempfile::tempdir()
424 .ok()
425 .unwrap_or_else(|| panic!("Should create temp dir"));
426 let backend = LocalBackend::new(temp_dir.path())
427 .ok()
428 .unwrap_or_else(|| panic!("Should create backend"));
429
430 let debug_str = format!("{:?}", backend);
431 assert!(debug_str.contains("LocalBackend"));
432 }
433
434 #[test]
435 fn test_clone() {
436 let temp_dir = tempfile::tempdir()
437 .ok()
438 .unwrap_or_else(|| panic!("Should create temp dir"));
439 let backend = LocalBackend::new(temp_dir.path())
440 .ok()
441 .unwrap_or_else(|| panic!("Should create backend"));
442
443 let cloned = backend.clone();
444 assert_eq!(cloned.root(), backend.root());
445 }
446
447 #[test]
448 fn test_list_nonexistent_prefix() {
449 let temp_dir = tempfile::tempdir()
450 .ok()
451 .unwrap_or_else(|| panic!("Should create temp dir"));
452 let backend = LocalBackend::new(temp_dir.path())
453 .ok()
454 .unwrap_or_else(|| panic!("Should create backend"));
455
456 let result = backend
457 .list("nonexistent/path/")
458 .ok()
459 .unwrap_or_else(|| panic!("Should list"));
460 assert!(result.is_empty());
461 }
462
463 #[test]
464 fn test_list_with_file_prefix() {
465 let temp_dir = tempfile::tempdir()
466 .ok()
467 .unwrap_or_else(|| panic!("Should create temp dir"));
468 let backend = LocalBackend::new(temp_dir.path())
469 .ok()
470 .unwrap_or_else(|| panic!("Should create backend"));
471
472 backend
473 .put("train_data.parquet", Bytes::from("a"))
474 .ok()
475 .unwrap_or_else(|| panic!("Should put"));
476 backend
477 .put("train_labels.parquet", Bytes::from("b"))
478 .ok()
479 .unwrap_or_else(|| panic!("Should put"));
480 backend
481 .put("test_data.parquet", Bytes::from("c"))
482 .ok()
483 .unwrap_or_else(|| panic!("Should put"));
484
485 let train_files = backend
487 .list("train")
488 .ok()
489 .unwrap_or_else(|| panic!("Should list"));
490 assert_eq!(train_files.len(), 2);
491 }
492
493 #[test]
494 fn test_deeply_nested_structure() {
495 let temp_dir = tempfile::tempdir()
496 .ok()
497 .unwrap_or_else(|| panic!("Should create temp dir"));
498 let backend = LocalBackend::new(temp_dir.path())
499 .ok()
500 .unwrap_or_else(|| panic!("Should create backend"));
501
502 backend
503 .put("a/b/c/d/e/f/deep.txt", Bytes::from("deep content"))
504 .ok()
505 .unwrap_or_else(|| panic!("Should put"));
506
507 let exists = backend
508 .exists("a/b/c/d/e/f/deep.txt")
509 .ok()
510 .unwrap_or_else(|| panic!("Should check exists"));
511 assert!(exists);
512
513 let content = backend
514 .get("a/b/c/d/e/f/deep.txt")
515 .ok()
516 .unwrap_or_else(|| panic!("Should get"));
517 assert_eq!(content, Bytes::from("deep content"));
518 }
519
520 #[test]
523 fn test_put_overwrite() {
524 let temp_dir = tempfile::tempdir()
525 .ok()
526 .unwrap_or_else(|| panic!("temp dir"));
527 let backend = LocalBackend::new(temp_dir.path())
528 .ok()
529 .unwrap_or_else(|| panic!("backend"));
530
531 backend
532 .put("file.txt", Bytes::from("original"))
533 .ok()
534 .unwrap_or_else(|| panic!("put 1"));
535 backend
536 .put("file.txt", Bytes::from("updated"))
537 .ok()
538 .unwrap_or_else(|| panic!("put 2"));
539
540 let content = backend
541 .get("file.txt")
542 .ok()
543 .unwrap_or_else(|| panic!("get"));
544 assert_eq!(content, Bytes::from("updated"));
545 }
546
547 #[test]
548 fn test_list_subdir_as_prefix() {
549 let temp_dir = tempfile::tempdir()
550 .ok()
551 .unwrap_or_else(|| panic!("temp dir"));
552 let backend = LocalBackend::new(temp_dir.path())
553 .ok()
554 .unwrap_or_else(|| panic!("backend"));
555
556 backend
557 .put("data/train/a.txt", Bytes::from("a"))
558 .ok()
559 .unwrap_or_else(|| panic!("put a"));
560 backend
561 .put("data/train/b.txt", Bytes::from("b"))
562 .ok()
563 .unwrap_or_else(|| panic!("put b"));
564 backend
565 .put("data/test/c.txt", Bytes::from("c"))
566 .ok()
567 .unwrap_or_else(|| panic!("put c"));
568
569 let train_files = backend
571 .list("data/train")
572 .ok()
573 .unwrap_or_else(|| panic!("list"));
574 assert_eq!(train_files.len(), 2);
575 }
576
577 #[test]
578 fn test_list_with_trailing_slash() {
579 let temp_dir = tempfile::tempdir()
580 .ok()
581 .unwrap_or_else(|| panic!("temp dir"));
582 let backend = LocalBackend::new(temp_dir.path())
583 .ok()
584 .unwrap_or_else(|| panic!("backend"));
585
586 backend
587 .put("subdir/file1.txt", Bytes::from("1"))
588 .ok()
589 .unwrap_or_else(|| panic!("put"));
590 backend
591 .put("subdir/file2.txt", Bytes::from("2"))
592 .ok()
593 .unwrap_or_else(|| panic!("put"));
594
595 let files = backend
597 .list("subdir/")
598 .ok()
599 .unwrap_or_else(|| panic!("list"));
600 assert_eq!(files.len(), 2);
601 }
602
603 #[test]
604 fn test_delete_and_recreate() {
605 let temp_dir = tempfile::tempdir()
606 .ok()
607 .unwrap_or_else(|| panic!("temp dir"));
608 let backend = LocalBackend::new(temp_dir.path())
609 .ok()
610 .unwrap_or_else(|| panic!("backend"));
611
612 backend
613 .put("file.txt", Bytes::from("v1"))
614 .ok()
615 .unwrap_or_else(|| panic!("put"));
616 backend
617 .delete("file.txt")
618 .ok()
619 .unwrap_or_else(|| panic!("delete"));
620 backend
621 .put("file.txt", Bytes::from("v2"))
622 .ok()
623 .unwrap_or_else(|| panic!("put again"));
624
625 let content = backend
626 .get("file.txt")
627 .ok()
628 .unwrap_or_else(|| panic!("get"));
629 assert_eq!(content, Bytes::from("v2"));
630 }
631
632 #[test]
633 fn test_size_zero_length_file() {
634 let temp_dir = tempfile::tempdir()
635 .ok()
636 .unwrap_or_else(|| panic!("temp dir"));
637 let backend = LocalBackend::new(temp_dir.path())
638 .ok()
639 .unwrap_or_else(|| panic!("backend"));
640
641 backend
642 .put("empty.txt", Bytes::new())
643 .ok()
644 .unwrap_or_else(|| panic!("put"));
645
646 let size = backend
647 .size("empty.txt")
648 .ok()
649 .unwrap_or_else(|| panic!("size"));
650 assert_eq!(size, 0);
651 }
652
653 #[test]
654 fn test_exists_directory() {
655 let temp_dir = tempfile::tempdir()
656 .ok()
657 .unwrap_or_else(|| panic!("temp dir"));
658 let backend = LocalBackend::new(temp_dir.path())
659 .ok()
660 .unwrap_or_else(|| panic!("backend"));
661
662 backend
664 .put("subdir/file.txt", Bytes::from("data"))
665 .ok()
666 .unwrap_or_else(|| panic!("put"));
667
668 let exists = backend
670 .exists("subdir")
671 .ok()
672 .unwrap_or_else(|| panic!("exists"));
673 let _ = exists; }
676
677 #[test]
678 fn test_multiple_puts_same_directory() {
679 let temp_dir = tempfile::tempdir()
680 .ok()
681 .unwrap_or_else(|| panic!("temp dir"));
682 let backend = LocalBackend::new(temp_dir.path())
683 .ok()
684 .unwrap_or_else(|| panic!("backend"));
685
686 for i in 0..10 {
687 backend
688 .put(
689 &format!("dir/file{}.txt", i),
690 Bytes::from(format!("content{}", i)),
691 )
692 .ok()
693 .unwrap_or_else(|| panic!("put"));
694 }
695
696 let files = backend.list("dir").ok().unwrap_or_else(|| panic!("list"));
697 assert_eq!(files.len(), 10);
698 }
699
700 #[test]
701 fn test_get_large_file() {
702 let temp_dir = tempfile::tempdir()
703 .ok()
704 .unwrap_or_else(|| panic!("temp dir"));
705 let backend = LocalBackend::new(temp_dir.path())
706 .ok()
707 .unwrap_or_else(|| panic!("backend"));
708
709 let data: Vec<u8> = (0..1_000_000).map(|i| (i % 256) as u8).collect();
711 backend
712 .put("large.bin", Bytes::from(data.clone()))
713 .ok()
714 .unwrap_or_else(|| panic!("put"));
715
716 let retrieved = backend
717 .get("large.bin")
718 .ok()
719 .unwrap_or_else(|| panic!("get"));
720 assert_eq!(retrieved.len(), data.len());
721 assert_eq!(&retrieved[..], &data[..]);
722 }
723
724 #[test]
725 fn test_list_returns_relative_paths() {
726 let temp_dir = tempfile::tempdir()
727 .ok()
728 .unwrap_or_else(|| panic!("temp dir"));
729 let backend = LocalBackend::new(temp_dir.path())
730 .ok()
731 .unwrap_or_else(|| panic!("backend"));
732
733 backend
734 .put("data/train.parquet", Bytes::from("a"))
735 .ok()
736 .unwrap_or_else(|| panic!("put"));
737
738 let files = backend.list("").ok().unwrap_or_else(|| panic!("list"));
739 assert!(!files.is_empty());
740 assert!(!files[0].starts_with('/'));
742 }
743
744 #[test]
745 fn test_special_characters_in_filename() {
746 let temp_dir = tempfile::tempdir()
747 .ok()
748 .unwrap_or_else(|| panic!("temp dir"));
749 let backend = LocalBackend::new(temp_dir.path())
750 .ok()
751 .unwrap_or_else(|| panic!("backend"));
752
753 backend
755 .put("file with spaces.txt", Bytes::from("data"))
756 .ok()
757 .unwrap_or_else(|| panic!("put"));
758
759 let content = backend
760 .get("file with spaces.txt")
761 .ok()
762 .unwrap_or_else(|| panic!("get"));
763 assert_eq!(content, Bytes::from("data"));
764 }
765
766 #[test]
769 fn test_list_with_multiple_prefixes() {
770 let temp_dir = tempfile::tempdir()
771 .ok()
772 .unwrap_or_else(|| panic!("temp dir"));
773 let backend = LocalBackend::new(temp_dir.path())
774 .ok()
775 .unwrap_or_else(|| panic!("backend"));
776
777 backend
779 .put("train_data.parquet", Bytes::from("a"))
780 .ok()
781 .unwrap_or_else(|| panic!("put"));
782 backend
783 .put("train_labels.parquet", Bytes::from("b"))
784 .ok()
785 .unwrap_or_else(|| panic!("put"));
786 backend
787 .put("test_data.parquet", Bytes::from("c"))
788 .ok()
789 .unwrap_or_else(|| panic!("put"));
790 backend
791 .put("test_labels.parquet", Bytes::from("d"))
792 .ok()
793 .unwrap_or_else(|| panic!("put"));
794 backend
795 .put("validation.parquet", Bytes::from("e"))
796 .ok()
797 .unwrap_or_else(|| panic!("put"));
798
799 assert_eq!(backend.list("train").ok().unwrap().len(), 2);
800 assert_eq!(backend.list("test").ok().unwrap().len(), 2);
801 assert_eq!(backend.list("valid").ok().unwrap().len(), 1);
802 assert_eq!(backend.list("").ok().unwrap().len(), 5);
803 }
804
805 #[test]
806 fn test_list_deep_nested_directory() {
807 let temp_dir = tempfile::tempdir()
808 .ok()
809 .unwrap_or_else(|| panic!("temp dir"));
810 let backend = LocalBackend::new(temp_dir.path())
811 .ok()
812 .unwrap_or_else(|| panic!("backend"));
813
814 backend
815 .put("a/b/c/file1.txt", Bytes::from("1"))
816 .ok()
817 .unwrap_or_else(|| panic!("put"));
818 backend
819 .put("a/b/c/file2.txt", Bytes::from("2"))
820 .ok()
821 .unwrap_or_else(|| panic!("put"));
822 backend
823 .put("a/b/file3.txt", Bytes::from("3"))
824 .ok()
825 .unwrap_or_else(|| panic!("put"));
826 backend
827 .put("a/file4.txt", Bytes::from("4"))
828 .ok()
829 .unwrap_or_else(|| panic!("put"));
830
831 let all = backend.list("").ok().unwrap();
833 assert_eq!(all.len(), 4);
834
835 let a_files = backend.list("a").ok().unwrap();
837 assert_eq!(a_files.len(), 4);
838
839 let ab_files = backend.list("a/b").ok().unwrap();
841 assert_eq!(ab_files.len(), 3);
842
843 let abc_files = backend.list("a/b/c").ok().unwrap();
845 assert_eq!(abc_files.len(), 2);
846 }
847
848 #[test]
849 fn test_put_binary_data() {
850 let temp_dir = tempfile::tempdir()
851 .ok()
852 .unwrap_or_else(|| panic!("temp dir"));
853 let backend = LocalBackend::new(temp_dir.path())
854 .ok()
855 .unwrap_or_else(|| panic!("backend"));
856
857 let binary_data: Vec<u8> = (0..256).map(|i| i as u8).collect();
859 backend
860 .put("binary.bin", Bytes::from(binary_data.clone()))
861 .ok()
862 .unwrap_or_else(|| panic!("put"));
863
864 let retrieved = backend.get("binary.bin").ok().unwrap();
865 assert_eq!(retrieved.as_ref(), binary_data.as_slice());
866 }
867
868 #[test]
869 fn test_size_consistency() {
870 let temp_dir = tempfile::tempdir()
871 .ok()
872 .unwrap_or_else(|| panic!("temp dir"));
873 let backend = LocalBackend::new(temp_dir.path())
874 .ok()
875 .unwrap_or_else(|| panic!("backend"));
876
877 let data = Bytes::from("Hello, World!");
878 backend.put("hello.txt", data.clone()).ok().unwrap();
879
880 let size = backend.size("hello.txt").ok().unwrap();
881 let content = backend.get("hello.txt").ok().unwrap();
882
883 assert_eq!(size, content.len() as u64);
884 assert_eq!(size, data.len() as u64);
885 }
886
887 #[test]
888 fn test_list_empty_directory() {
889 let temp_dir = tempfile::tempdir()
890 .ok()
891 .unwrap_or_else(|| panic!("temp dir"));
892 let backend = LocalBackend::new(temp_dir.path())
893 .ok()
894 .unwrap_or_else(|| panic!("backend"));
895
896 backend.put("empty_dir/temp.txt", Bytes::from("temp")).ok();
898 backend.delete("empty_dir/temp.txt").ok();
899
900 let files = backend.list("empty_dir").ok().unwrap_or_default();
902 assert!(files.is_empty());
903 }
904
905 #[test]
906 fn test_multiple_backends_same_root() {
907 let temp_dir = tempfile::tempdir()
908 .ok()
909 .unwrap_or_else(|| panic!("temp dir"));
910
911 let backend1 = LocalBackend::new(temp_dir.path())
912 .ok()
913 .unwrap_or_else(|| panic!("backend1"));
914 let backend2 = LocalBackend::new(temp_dir.path())
915 .ok()
916 .unwrap_or_else(|| panic!("backend2"));
917
918 backend1
920 .put("shared.txt", Bytes::from("shared data"))
921 .ok()
922 .unwrap();
923
924 let content = backend2.get("shared.txt").ok().unwrap();
925 assert_eq!(content, Bytes::from("shared data"));
926
927 assert!(backend1.exists("shared.txt").ok().unwrap());
929 assert!(backend2.exists("shared.txt").ok().unwrap());
930 }
931
932 #[test]
933 fn test_resolve_path_consistency() {
934 let temp_dir = tempfile::tempdir()
935 .ok()
936 .unwrap_or_else(|| panic!("temp dir"));
937 let backend = LocalBackend::new(temp_dir.path())
938 .ok()
939 .unwrap_or_else(|| panic!("backend"));
940
941 let data = Bytes::from("test");
942
943 backend.put("dir/file.txt", data.clone()).ok().unwrap();
945
946 assert!(backend.exists("dir/file.txt").ok().unwrap());
948
949 let content = backend.get("dir/file.txt").ok().unwrap();
951 assert_eq!(content, data);
952 }
953
954 #[test]
955 fn test_list_returns_sorted_or_consistent() {
956 let temp_dir = tempfile::tempdir()
957 .ok()
958 .unwrap_or_else(|| panic!("temp dir"));
959 let backend = LocalBackend::new(temp_dir.path())
960 .ok()
961 .unwrap_or_else(|| panic!("backend"));
962
963 for name in ["zebra.txt", "apple.txt", "mango.txt", "banana.txt"] {
964 backend.put(name, Bytes::from(name)).ok().unwrap();
965 }
966
967 let files1 = backend.list("").ok().unwrap();
968 let files2 = backend.list("").ok().unwrap();
969
970 assert_eq!(files1.len(), files2.len());
972 }
973
974 #[test]
975 fn test_put_creates_intermediate_directories() {
976 let temp_dir = tempfile::tempdir()
977 .ok()
978 .unwrap_or_else(|| panic!("temp dir"));
979 let backend = LocalBackend::new(temp_dir.path())
980 .ok()
981 .unwrap_or_else(|| panic!("backend"));
982
983 let deep_path = "a/b/c/d/e/f/g/h/i/j/file.txt";
985 backend.put(deep_path, Bytes::from("deep")).ok().unwrap();
986
987 assert!(backend.exists(deep_path).ok().unwrap());
988 assert_eq!(backend.get(deep_path).ok().unwrap(), Bytes::from("deep"));
989 }
990
991 #[test]
992 fn test_exists_for_directory_path() {
993 let temp_dir = tempfile::tempdir()
994 .ok()
995 .unwrap_or_else(|| panic!("temp dir"));
996 let backend = LocalBackend::new(temp_dir.path())
997 .ok()
998 .unwrap_or_else(|| panic!("backend"));
999
1000 backend
1001 .put("dir/file.txt", Bytes::from("data"))
1002 .ok()
1003 .unwrap();
1004
1005 let result = backend.exists("dir");
1007 assert!(result.is_ok());
1008 assert!(result.ok().unwrap());
1010 }
1011}