Skip to main content

ferro_storage/drivers/
memory.rs

1//! In-memory storage driver for testing.
2
3use crate::storage::{FileMetadata, PutOptions, StorageDriver, Visibility};
4use crate::Error;
5use async_trait::async_trait;
6use bytes::Bytes;
7use dashmap::DashMap;
8use std::collections::HashSet;
9use std::sync::Arc;
10use std::time::SystemTime;
11
12/// Stored file data.
13#[derive(Clone)]
14struct StoredFile {
15    contents: Bytes,
16    #[allow(dead_code)]
17    visibility: Visibility,
18    content_type: Option<String>,
19    created_at: SystemTime,
20}
21
22/// In-memory storage driver.
23#[derive(Clone)]
24pub struct MemoryDriver {
25    files: Arc<DashMap<String, StoredFile>>,
26    url_base: Option<String>,
27}
28
29impl Default for MemoryDriver {
30    fn default() -> Self {
31        Self::new()
32    }
33}
34
35impl MemoryDriver {
36    /// Create a new memory driver.
37    pub fn new() -> Self {
38        Self {
39            files: Arc::new(DashMap::new()),
40            url_base: None,
41        }
42    }
43
44    /// Set the base URL.
45    pub fn with_url_base(mut self, url: impl Into<String>) -> Self {
46        self.url_base = Some(url.into());
47        self
48    }
49
50    /// Clear all files.
51    pub fn clear(&self) {
52        self.files.clear();
53    }
54
55    /// Get number of stored files.
56    pub fn len(&self) -> usize {
57        self.files.len()
58    }
59
60    /// Check if storage is empty.
61    pub fn is_empty(&self) -> bool {
62        self.files.is_empty()
63    }
64
65    /// Normalize path (remove leading slash, normalize separators).
66    fn normalize_path(path: &str) -> String {
67        path.trim_start_matches('/').replace('\\', "/")
68    }
69}
70
71#[async_trait]
72impl StorageDriver for MemoryDriver {
73    async fn exists(&self, path: &str) -> Result<bool, Error> {
74        let path = Self::normalize_path(path);
75        Ok(self.files.contains_key(&path))
76    }
77
78    async fn get(&self, path: &str) -> Result<Bytes, Error> {
79        let path = Self::normalize_path(path);
80        self.files
81            .get(&path)
82            .map(|f| f.contents.clone())
83            .ok_or_else(|| Error::not_found(&path))
84    }
85
86    async fn put(&self, path: &str, contents: Bytes, options: PutOptions) -> Result<(), Error> {
87        let path = Self::normalize_path(path);
88        self.files.insert(
89            path,
90            StoredFile {
91                contents,
92                visibility: options.visibility,
93                content_type: options.content_type,
94                created_at: SystemTime::now(),
95            },
96        );
97        Ok(())
98    }
99
100    async fn delete(&self, path: &str) -> Result<(), Error> {
101        let path = Self::normalize_path(path);
102        self.files
103            .remove(&path)
104            .ok_or_else(|| Error::not_found(&path))?;
105        Ok(())
106    }
107
108    async fn copy(&self, from: &str, to: &str) -> Result<(), Error> {
109        let from = Self::normalize_path(from);
110        let to = Self::normalize_path(to);
111
112        let file = self
113            .files
114            .get(&from)
115            .ok_or_else(|| Error::not_found(&from))?
116            .clone();
117
118        self.files.insert(to, file);
119        Ok(())
120    }
121
122    async fn size(&self, path: &str) -> Result<u64, Error> {
123        let path = Self::normalize_path(path);
124        self.files
125            .get(&path)
126            .map(|f| f.contents.len() as u64)
127            .ok_or_else(|| Error::not_found(&path))
128    }
129
130    async fn metadata(&self, path: &str) -> Result<FileMetadata, Error> {
131        let normalized = Self::normalize_path(path);
132        let file = self
133            .files
134            .get(&normalized)
135            .ok_or_else(|| Error::not_found(&normalized))?;
136
137        let mut meta =
138            FileMetadata::new(path, file.contents.len() as u64).with_last_modified(file.created_at);
139
140        if let Some(ref content_type) = file.content_type {
141            meta = meta.with_mime_type(content_type);
142        }
143
144        Ok(meta)
145    }
146
147    async fn url(&self, path: &str) -> Result<String, Error> {
148        let path = Self::normalize_path(path);
149        match &self.url_base {
150            Some(base) => Ok(format!("{}/{}", base.trim_end_matches('/'), path)),
151            None => Ok(format!("memory://{path}")),
152        }
153    }
154
155    async fn temporary_url(
156        &self,
157        path: &str,
158        _expiration: std::time::Duration,
159    ) -> Result<String, Error> {
160        self.url(path).await
161    }
162
163    async fn files(&self, directory: &str) -> Result<Vec<String>, Error> {
164        let dir = Self::normalize_path(directory);
165        let prefix = if dir.is_empty() {
166            String::new()
167        } else {
168            format!("{dir}/")
169        };
170
171        let mut files = Vec::new();
172        for entry in self.files.iter() {
173            let path = entry.key();
174            if path.starts_with(&prefix) || (prefix.is_empty() && !path.contains('/')) {
175                let relative = path.strip_prefix(&prefix).unwrap_or(path);
176                // Only include files directly in this directory
177                if !relative.contains('/') {
178                    files.push(relative.to_string());
179                }
180            }
181        }
182
183        Ok(files)
184    }
185
186    async fn all_files(&self, directory: &str) -> Result<Vec<String>, Error> {
187        let dir = Self::normalize_path(directory);
188        let prefix = if dir.is_empty() {
189            String::new()
190        } else {
191            format!("{dir}/")
192        };
193
194        let mut files = Vec::new();
195        for entry in self.files.iter() {
196            let path = entry.key();
197            if path.starts_with(&prefix) {
198                let relative = path.strip_prefix(&prefix).unwrap_or(path);
199                files.push(relative.to_string());
200            } else if prefix.is_empty() {
201                files.push(path.clone());
202            }
203        }
204
205        Ok(files)
206    }
207
208    async fn directories(&self, directory: &str) -> Result<Vec<String>, Error> {
209        let dir = Self::normalize_path(directory);
210        let prefix = if dir.is_empty() {
211            String::new()
212        } else {
213            format!("{dir}/")
214        };
215
216        let mut dirs: HashSet<String> = HashSet::new();
217        for entry in self.files.iter() {
218            let path = entry.key();
219            if path.starts_with(&prefix) {
220                let relative = path.strip_prefix(&prefix).unwrap_or(path);
221                if let Some(slash_idx) = relative.find('/') {
222                    dirs.insert(relative[..slash_idx].to_string());
223                }
224            }
225        }
226
227        Ok(dirs.into_iter().collect())
228    }
229
230    async fn make_directory(&self, _path: &str) -> Result<(), Error> {
231        // No-op for memory driver - directories are implicit
232        Ok(())
233    }
234
235    async fn delete_directory(&self, path: &str) -> Result<(), Error> {
236        let dir = Self::normalize_path(path);
237        let prefix = format!("{dir}/");
238
239        let keys_to_remove: Vec<String> = self
240            .files
241            .iter()
242            .filter(|entry| entry.key().starts_with(&prefix))
243            .map(|entry| entry.key().clone())
244            .collect();
245
246        for key in keys_to_remove {
247            self.files.remove(&key);
248        }
249
250        Ok(())
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[tokio::test]
259    async fn test_memory_driver_put_get() {
260        let driver = MemoryDriver::new();
261
262        driver
263            .put("test.txt", Bytes::from("hello"), PutOptions::new())
264            .await
265            .unwrap();
266
267        let contents = driver.get("test.txt").await.unwrap();
268        assert_eq!(contents, Bytes::from("hello"));
269    }
270
271    #[tokio::test]
272    async fn test_memory_driver_exists() {
273        let driver = MemoryDriver::new();
274
275        assert!(!driver.exists("missing.txt").await.unwrap());
276
277        driver
278            .put("exists.txt", Bytes::from("data"), PutOptions::new())
279            .await
280            .unwrap();
281
282        assert!(driver.exists("exists.txt").await.unwrap());
283    }
284
285    #[tokio::test]
286    async fn test_memory_driver_delete() {
287        let driver = MemoryDriver::new();
288
289        driver
290            .put("to_delete.txt", Bytes::from("data"), PutOptions::new())
291            .await
292            .unwrap();
293
294        driver.delete("to_delete.txt").await.unwrap();
295        assert!(!driver.exists("to_delete.txt").await.unwrap());
296    }
297
298    #[tokio::test]
299    async fn test_memory_driver_copy() {
300        let driver = MemoryDriver::new();
301
302        driver
303            .put("original.txt", Bytes::from("content"), PutOptions::new())
304            .await
305            .unwrap();
306
307        driver.copy("original.txt", "copy.txt").await.unwrap();
308
309        assert_eq!(
310            driver.get("copy.txt").await.unwrap(),
311            Bytes::from("content")
312        );
313    }
314
315    #[tokio::test]
316    async fn test_memory_driver_url() {
317        let driver = MemoryDriver::new().with_url_base("https://cdn.example.com");
318
319        let url = driver.url("images/photo.jpg").await.unwrap();
320        assert_eq!(url, "https://cdn.example.com/images/photo.jpg");
321    }
322
323    #[tokio::test]
324    async fn test_memory_driver_directories() {
325        let driver = MemoryDriver::new();
326
327        driver
328            .put("images/a.jpg", Bytes::from("a"), PutOptions::new())
329            .await
330            .unwrap();
331        driver
332            .put("images/b.jpg", Bytes::from("b"), PutOptions::new())
333            .await
334            .unwrap();
335        driver
336            .put("docs/readme.md", Bytes::from("readme"), PutOptions::new())
337            .await
338            .unwrap();
339
340        let dirs = driver.directories("").await.unwrap();
341        assert!(dirs.contains(&"images".to_string()));
342        assert!(dirs.contains(&"docs".to_string()));
343    }
344
345    #[tokio::test]
346    async fn test_memory_driver_clear() {
347        let driver = MemoryDriver::new();
348
349        driver
350            .put("a.txt", Bytes::from("a"), PutOptions::new())
351            .await
352            .unwrap();
353        driver
354            .put("b.txt", Bytes::from("b"), PutOptions::new())
355            .await
356            .unwrap();
357
358        assert_eq!(driver.len(), 2);
359
360        driver.clear();
361        assert!(driver.is_empty());
362    }
363}