Skip to main content

alimentar/backend/
local.rs

1//! Local filesystem storage backend.
2
3use std::{
4    fs,
5    path::{Path, PathBuf},
6};
7
8use bytes::Bytes;
9
10use super::StorageBackend;
11use crate::error::{Error, Result};
12
13/// A storage backend using the local filesystem.
14///
15/// All keys are relative to the configured root directory.
16///
17/// # Example
18///
19/// ```no_run
20/// use alimentar::backend::LocalBackend;
21///
22/// let backend = LocalBackend::new("/data/datasets").unwrap();
23/// ```
24#[derive(Debug, Clone)]
25pub struct LocalBackend {
26    root: PathBuf,
27}
28
29impl LocalBackend {
30    /// Creates a new local backend with the given root directory.
31    ///
32    /// Creates the directory if it doesn't exist.
33    ///
34    /// # Errors
35    ///
36    /// Returns an error if the directory cannot be created or accessed.
37    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    /// Returns the root directory.
44    pub fn root(&self) -> &Path {
45        &self.root
46    }
47
48    /// Resolves a key to a full filesystem path.
49    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        // If the prefix is a directory, list its contents
59        // If it's a file prefix, list matching files in parent
60        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        // Create parent directories
93        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    /// Recursively lists files, collecting paths relative to root.
122    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            // Check prefix filter
131            if !prefix.is_empty() && !file_name.starts_with(prefix) {
132                continue;
133            }
134
135            if path.is_file() {
136                // Get path relative to root
137                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                // Recurse into subdirectory (no prefix filter for subdirs)
142                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        // Deleting a non-existent file should not error
271        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"); // 20 bytes
285        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        // List with file prefix
486        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    // === Additional coverage tests ===
521
522    #[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        // List with directory prefix
570        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        // List with trailing slash (directory)
596        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        // Create a file in a subdirectory
663        backend
664            .put("subdir/file.txt", Bytes::from("data"))
665            .ok()
666            .unwrap_or_else(|| panic!("put"));
667
668        // Check if the directory "exists" (as a path component)
669        let exists = backend
670            .exists("subdir")
671            .ok()
672            .unwrap_or_else(|| panic!("exists"));
673        // exists checks for file, not directory
674        let _ = exists; // Either way is acceptable for directory existence
675    }
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        // Create a 1MB file
710        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        // Paths should be relative, not absolute
741        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        // Test with spaces and underscores
754        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    // === Additional local backend tests ===
767
768    #[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        // Create files with different prefixes
778        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        // List from root should find all files
832        let all = backend.list("").ok().unwrap();
833        assert_eq!(all.len(), 4);
834
835        // List from a/ should find all in a/
836        let a_files = backend.list("a").ok().unwrap();
837        assert_eq!(a_files.len(), 4);
838
839        // List from a/b should find 3
840        let ab_files = backend.list("a/b").ok().unwrap();
841        assert_eq!(ab_files.len(), 3);
842
843        // List from a/b/c should find 2
844        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        // Binary data with null bytes and non-UTF8 sequences
858        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        // Create an empty subdirectory by creating and deleting a file
897        backend.put("empty_dir/temp.txt", Bytes::from("temp")).ok();
898        backend.delete("empty_dir/temp.txt").ok();
899
900        // List should return empty
901        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        // Write with one, read with another
919        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        // Both should see the file
928        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        // Write with one path style
944        backend.put("dir/file.txt", data.clone()).ok().unwrap();
945
946        // Read with same path
947        assert!(backend.exists("dir/file.txt").ok().unwrap());
948
949        // Get with same path
950        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        // Should be consistent between calls
971        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        // Deep path that doesn't exist
984        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        // exists() checks if path exists (could be file or dir)
1006        let result = backend.exists("dir");
1007        assert!(result.is_ok());
1008        // "dir" exists as a directory
1009        assert!(result.ok().unwrap());
1010    }
1011}