extendable_assets/filesystem/fallback.rs
1use std::sync::Arc;
2
3use async_trait::async_trait;
4
5use crate::filesystem::{Filesystem, FilesystemError};
6
7/// A filesystem implementation that tries multiple fallback filesystems in order.
8///
9/// `FallbackFilesystem` allows chaining multiple filesystem implementations together,
10/// attempting operations on each one in sequence until one succeeds. This is useful
11/// for implementing layered asset loading where assets might be located in different
12/// locations (e.g., first check a mod directory, then fall back to base game assets).
13pub struct FallbackFilesystem {
14 /// Ordered list of filesystem implementations to try.
15 ///
16 /// When any filesystem operation is requested, each filesystem in this list
17 /// is tried in order until one succeeds or all have failed.
18 fallbacks: Vec<Arc<dyn Filesystem>>,
19}
20
21impl FallbackFilesystem {
22 /// Creates a new `FallbackFilesystem` with the given list of fallback filesystems.
23 ///
24 /// The filesystems will be tried in the order they appear in the vector.
25 /// Earlier filesystems have priority over later ones.
26 ///
27 /// # Arguments
28 ///
29 /// * `fallbacks` - A vector of filesystem implementations to use as fallbacks
30 ///
31 /// # Example
32 ///
33 /// ```
34 /// # use extendable_assets::{ FallbackFilesystem, NativeFilesystem };
35 /// # use std::sync::Arc;
36 /// let mod_fs = Arc::new(NativeFilesystem::new("mods/"));
37 /// let base_fs = Arc::new(NativeFilesystem::new("assets/"));
38 /// let fallback = FallbackFilesystem::new(vec![mod_fs, base_fs]);
39 /// ```
40 #[inline]
41 pub fn new(fallbacks: Vec<Arc<dyn Filesystem>>) -> Self {
42 // Create a new instance with the provided fallback filesystems
43 Self { fallbacks }
44 }
45}
46
47#[async_trait]
48impl Filesystem for FallbackFilesystem {
49 /// Attempts to read bytes from the asset path using the fallback filesystems.
50 ///
51 /// Tries each filesystem in order until one successfully reads the asset.
52 /// Returns the bytes from the first successful read operation.
53 ///
54 /// # Arguments
55 ///
56 /// * `asset_path` - The path to the asset to read
57 ///
58 /// # Returns
59 ///
60 /// * `Ok(Vec<u8>)` - The asset bytes from the first successful filesystem
61 /// * `Err(FilesystemError::NotFound)` - If all filesystems fail to find the asset
62 async fn read_bytes(&self, asset_path: &str) -> Result<Vec<u8>, FilesystemError> {
63 // Try each fallback filesystem in order
64 for f in &self.fallbacks {
65 let r = f.read_bytes(asset_path).await;
66 // Return immediately on the first successful read
67 if r.is_ok() {
68 return r;
69 }
70 // Continue to next fallback if this one failed
71 }
72 // All fallbacks failed, asset not found in any filesystem
73 Err(FilesystemError::NotFound(asset_path.to_string()))
74 }
75
76 /// Attempts to write bytes to the asset path using the fallback filesystems.
77 ///
78 /// Tries each filesystem in order until one successfully writes the asset.
79 /// The write operation stops at the first successful filesystem.
80 ///
81 /// # Arguments
82 ///
83 /// * `asset_path` - The path where the asset should be written
84 /// * `data` - The bytes to write
85 ///
86 /// # Returns
87 ///
88 /// * `Ok(())` - If any filesystem successfully writes the data
89 /// * `Err(FilesystemError::WriteUnsupported)` - If all filesystems fail to write
90 async fn write_bytes(&self, asset_path: &str, data: &[u8]) -> Result<(), FilesystemError> {
91 // Try each fallback filesystem in order
92 for f in &self.fallbacks {
93 let r = f.write_bytes(asset_path, data).await;
94 // Return immediately on the first successful write
95 if r.is_ok() {
96 return r;
97 }
98 // Continue to next fallback if this one failed
99 }
100 // All fallbacks failed, unable to write to any filesystem
101 Err(FilesystemError::WriteUnsupported)
102 }
103}
104
105#[cfg(test)]
106mod test {
107 use super::*;
108 use crate::filesystem::NativeFilesystem;
109 use std::path::Path;
110 use std::sync::Arc;
111
112 /// Tests the fallback filesystem's ability to read files from multiple sources.
113 ///
114 /// Verifies that:
115 /// - Files are read from the first filesystem that contains them
116 /// - The fallback mechanism correctly tries filesystems in order
117 /// - Different files can be sourced from different fallback filesystems
118 #[test]
119 fn read_bytes() {
120 // Set up test directories relative to the cargo manifest directory
121 let tests_dir = Path::new(&env!("CARGO_MANIFEST_DIR")).join("tests");
122 let data_0_dir = tests_dir.join("test_data_0");
123 let data_1_dir = tests_dir.join("test_data_1");
124
125 // Create two separate filesystem instances for different test data directories
126 let data_0: Arc<dyn Filesystem> = Arc::new(NativeFilesystem::new(data_0_dir));
127 let data_1: Arc<dyn Filesystem> = Arc::new(NativeFilesystem::new(data_1_dir));
128
129 // Create fallback filesystem with data_1 as primary and data_0 as fallback
130 // This order is intentional to test the priority system
131 let fs: Arc<dyn Filesystem> = Arc::new(FallbackFilesystem::new(vec![data_1, data_0]));
132
133 // Test reading files that exist in different fallback locations
134 let hello = pollster::block_on(fs.read_bytes("hello.txt")).unwrap();
135 let goodbye = pollster::block_on(fs.read_bytes("goodbye.txt")).unwrap();
136 let word = pollster::block_on(fs.read_bytes("word.txt")).unwrap();
137
138 // Verify that files are read correctly from their respective sources
139 // hello.txt should come from data_1 (first priority)
140 assert_eq!(hello, b"Hello earth\n");
141 // goodbye.txt and word.txt should come from the first source that contains them
142 assert_eq!(goodbye, b"Goodbye world\n");
143 assert_eq!(word, b"assets\n");
144
145 // Test that non-existent files return NotFound error after trying all fallbacks
146 let missing = pollster::block_on(fs.read_bytes("MISSING"));
147 assert!(matches!(missing, Err(FilesystemError::NotFound(_))));
148 }
149}