Skip to main content

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}