Skip to main content

loco_rs/storage/
mod.rs

1//! # Storage Module
2//!
3//! This module defines a generic storage abstraction represented by the
4//! [`Storage`] struct. It provides methods for performing common storage
5//! operations such as upload, download, delete, rename, and copy.
6//!
7//! ## Storage Strategy
8//!
9//! The [`Storage`] struct is designed to work with different storage
10//! strategies. A storage strategy defines the behavior of the storage
11//! operations. Strategies implement the [`strategies::StorageStrategy`].
12//! The selected strategy can be dynamically changed at runtime.
13mod contents;
14pub mod drivers;
15pub mod strategies;
16pub mod stream;
17use std::{
18    collections::BTreeMap,
19    path::{Path, PathBuf},
20};
21
22use bytes::Bytes;
23
24use self::{drivers::StoreDriver, stream::BytesStream};
25
26#[derive(thiserror::Error, Debug)]
27#[allow(clippy::module_name_repetitions)]
28#[non_exhaustive]
29pub enum StorageError {
30    #[error("store not found by the given key: {0}")]
31    StoreNotFound(String),
32
33    #[error(transparent)]
34    Store(#[from] Box<opendal::Error>),
35
36    #[error("Unable to read data from file {}", path.display().to_string())]
37    UnableToReadBytes { path: PathBuf },
38
39    #[error("secondaries errors")]
40    Multi(BTreeMap<String, String>),
41
42    #[error(transparent)]
43    Any(#[from] Box<dyn std::error::Error + Send + Sync>),
44}
45
46pub type StorageResult<T> = std::result::Result<T, StorageError>;
47
48impl From<opendal::Error> for StorageError {
49    fn from(val: opendal::Error) -> Self {
50        Self::Store(Box::new(val))
51    }
52}
53
54pub struct Storage {
55    pub stores: BTreeMap<String, Box<dyn StoreDriver>>,
56    pub strategy: Box<dyn strategies::StorageStrategy>,
57}
58
59impl Storage {
60    /// Creates a new storage instance with a single store and the default
61    /// strategy.
62    ///
63    /// # Examples
64    ///```
65    /// use loco_rs::storage;
66    ///
67    /// let storage = storage::Storage::single(storage::drivers::mem::new());
68    /// ```
69    #[must_use]
70    pub fn single(store: Box<dyn StoreDriver>) -> Self {
71        let default_key = "store";
72        Self {
73            strategy: Box::new(strategies::single::SingleStrategy::new(default_key)),
74            stores: BTreeMap::from([(default_key.to_string(), store)]),
75        }
76    }
77
78    /// Creates a new storage instance with the provided stores and strategy.
79    #[must_use]
80    pub fn new(
81        stores: BTreeMap<String, Box<dyn StoreDriver>>,
82        strategy: Box<dyn strategies::StorageStrategy>,
83    ) -> Self {
84        Self { stores, strategy }
85    }
86
87    /// Uploads content to the storage at the specified path.
88    ///
89    /// This method uses the selected strategy for the upload operation.
90    ///
91    /// # Examples
92    ///```
93    /// use loco_rs::storage;
94    /// use std::path::Path;
95    /// use bytes::Bytes;
96    /// pub async fn upload() {
97    ///     let storage = storage::Storage::single(storage::drivers::mem::new());
98    ///     let path = Path::new("example.txt");
99    ///     let content = "Loco!";
100    ///     let result = storage.upload(path, &Bytes::from(content)).await;
101    ///     assert!(result.is_ok());
102    /// }
103    /// ```
104    ///
105    /// # Errors
106    ///
107    /// This method returns an error if the upload operation fails or if there
108    /// is an issue with the strategy configuration.
109    pub async fn upload(&self, path: &Path, content: &Bytes) -> StorageResult<()> {
110        self.upload_with_strategy(path, content, &*self.strategy)
111            .await
112    }
113
114    /// Uploads content to the storage at the specified path using a specific
115    /// strategy.
116    ///
117    /// This method allows specifying a custom strategy for the upload
118    /// operation.
119    ///
120    /// # Errors
121    ///
122    /// This method returns an error if the upload operation fails or if there
123    /// is an issue with the strategy configuration.
124    pub async fn upload_with_strategy(
125        &self,
126        path: &Path,
127        content: &Bytes,
128        strategy: &dyn strategies::StorageStrategy,
129    ) -> StorageResult<()> {
130        strategy.upload(self, path, content).await
131    }
132
133    /// Downloads content from the storage at the specified path.
134    ///
135    /// This method uses the selected strategy for the download operation.
136    ///
137    /// # Examples
138    ///```
139    /// use loco_rs::storage;
140    /// use std::path::Path;
141    /// use bytes::Bytes;
142    /// pub async fn download() {
143    ///     let storage = storage::Storage::single(storage::drivers::mem::new());
144    ///     let path = Path::new("example.txt");
145    ///     let content = "Loco!";
146    ///     storage.upload(path, &Bytes::from(content)).await;
147    ///
148    ///     let result: String = storage.download(path).await.unwrap();
149    ///     assert_eq!(result, "Loco!");
150    /// }
151    /// ```
152    ///
153    /// # Errors
154    ///
155    /// This method returns an error if the download operation fails or if there
156    /// is an issue with the strategy configuration.
157    pub async fn download<T: TryFrom<contents::Contents>>(&self, path: &Path) -> StorageResult<T> {
158        self.download_with_policy(path, &*self.strategy).await
159    }
160
161    /// Downloads content from the storage at the specified path using a
162    /// specific strategy.
163    ///
164    /// This method allows specifying a custom strategy for the download
165    /// operation.
166    ///
167    /// # Errors
168    ///
169    /// This method returns an error if the download operation fails or if there
170    /// is an issue with the strategy configuration.
171    pub async fn download_with_policy<T: TryFrom<contents::Contents>>(
172        &self,
173        path: &Path,
174        strategy: &dyn strategies::StorageStrategy,
175    ) -> StorageResult<T> {
176        let res = strategy.download(self, path).await?;
177        contents::Contents::from(res).try_into().map_or_else(
178            |_| {
179                Err(StorageError::UnableToReadBytes {
180                    path: path.to_path_buf(),
181                })
182            },
183            |content| Ok(content),
184        )
185    }
186
187    /// Deletes content from the storage at the specified path.
188    ///
189    /// This method uses the selected strategy for the delete operation.
190    ///
191    /// # Examples
192    ///```
193    /// use loco_rs::storage;
194    /// use std::path::Path;
195    /// use bytes::Bytes;
196    /// pub async fn download() {
197    ///     let storage = storage::Storage::single(storage::drivers::mem::new());
198    ///     let path = Path::new("example.txt");
199    ///     let content = "Loco!";
200    ///     storage.upload(path, &Bytes::from(content)).await;
201    ///
202    ///     let result = storage.delete(path).await;
203    ///     assert!(result.is_ok());
204    /// }
205    /// ```
206    ///
207    /// # Errors
208    ///
209    /// This method returns an error if the delete operation fails or if there
210    /// is an issue with the strategy configuration.
211    pub async fn delete(&self, path: &Path) -> StorageResult<()> {
212        self.delete_with_policy(path, &*self.strategy).await
213    }
214
215    /// Deletes content from the storage at the specified path using a specific
216    /// strategy.
217    ///
218    /// This method allows specifying a custom strategy for the delete
219    /// operation.
220    ///
221    /// # Errors
222    ///
223    /// This method returns an error if the delete operation fails or if there
224    /// is an issue with the strategy configuration.    
225    pub async fn delete_with_policy(
226        &self,
227        path: &Path,
228        strategy: &dyn strategies::StorageStrategy,
229    ) -> StorageResult<()> {
230        strategy.delete(self, path).await
231    }
232
233    /// Renames content from one path to another in the storage.
234    ///
235    /// This method uses the selected strategy for the rename operation.
236    ///
237    /// # Examples
238    ///```
239    /// use loco_rs::storage;
240    /// use std::path::Path;
241    /// use bytes::Bytes;
242    /// pub async fn download() {
243    ///     let storage = storage::Storage::single(storage::drivers::mem::new());
244    ///     let path = Path::new("example.txt");
245    ///     let content = "Loco!";
246    ///     storage.upload(path, &Bytes::from(content)).await;
247    ///     
248    ///     let new_path = Path::new("new_path.txt");
249    ///     let store = storage.as_store("default").unwrap();
250    ///     assert!(storage.rename(&path, &new_path).await.is_ok());
251    ///     assert!(!store.exists(&path).await.unwrap());
252    ///     assert!(store.exists(&new_path).await.unwrap());
253    /// }
254    /// ```
255    ///
256    /// # Errors
257    ///
258    /// This method returns an error if the rename operation fails or if there
259    /// is an issue with the strategy configuration.
260    pub async fn rename(&self, from: &Path, to: &Path) -> StorageResult<()> {
261        self.rename_with_policy(from, to, &*self.strategy).await
262    }
263
264    /// Renames content from one path to another in the storage using a specific
265    /// strategy.
266    ///
267    /// This method allows specifying a custom strategy for the rename
268    /// operation.
269    ///
270    /// # Errors
271    ///
272    /// This method returns an error if the rename operation fails or if there
273    /// is an issue with the strategy configuration.
274    pub async fn rename_with_policy(
275        &self,
276        from: &Path,
277        to: &Path,
278        strategy: &dyn strategies::StorageStrategy,
279    ) -> StorageResult<()> {
280        strategy.rename(self, from, to).await
281    }
282
283    /// Copies content from one path to another in the storage.
284    ///
285    /// This method uses the selected strategy for the copy operation.
286    ///
287    /// # Examples
288    ///```
289    /// use loco_rs::storage;
290    /// use std::path::Path;
291    /// use bytes::Bytes;
292    /// pub async fn download() {
293    ///     let storage = storage::Storage::single(storage::drivers::mem::new());
294    ///     let path = Path::new("example.txt");
295    ///     let content = "Loco!";
296    ///     storage.upload(path, &Bytes::from(content)).await;
297    ///     
298    ///     let new_path = Path::new("new_path.txt");
299    ///     let store = storage.as_store("default").unwrap();
300    ///     assert!(storage.copy(&path, &new_path).await.is_ok());
301    ///     assert!(store.exists(&path).await.unwrap());
302    ///     assert!(store.exists(&new_path).await.unwrap());
303    /// }
304    /// ```
305    ///
306    /// # Errors
307    ///
308    /// This method returns an error if the copy operation fails or if there is
309    /// an issue with the strategy configuration.
310    pub async fn copy(&self, from: &Path, to: &Path) -> StorageResult<()> {
311        self.copy_with_policy(from, to, &*self.strategy).await
312    }
313
314    /// Copies content from one path to another in the storage using a specific
315    /// strategy.
316    ///
317    /// This method allows specifying a custom strategy for the copy operation.
318    ///
319    /// # Errors
320    ///
321    /// This method returns an error if the copy operation fails or if there is
322    /// an issue with the strategy configuration.
323    pub async fn copy_with_policy(
324        &self,
325        from: &Path,
326        to: &Path,
327        strategy: &dyn strategies::StorageStrategy,
328    ) -> StorageResult<()> {
329        strategy.copy(self, from, to).await
330    }
331
332    /// Returns a reference to the store with the specified name if exists.
333    ///
334    /// # Examples
335    ///```
336    /// use loco_rs::storage;
337    /// use std::path::Path;
338    /// use bytes::Bytes;
339    /// pub async fn download() {
340    ///     let storage = storage::Storage::single(storage::drivers::mem::new());
341    ///     assert!(storage.as_store("default").is_some());
342    ///     assert!(storage.as_store("store_2").is_none());
343    /// }
344    /// ```
345    ///
346    /// # Returns
347    /// Return None if the given name not found.
348    #[must_use]
349    pub fn as_store(&self, name: &str) -> Option<&dyn StoreDriver> {
350        self.stores.get(name).map(|s| &**s)
351    }
352
353    /// Returns a reference to the store with the specified name.
354    ///
355    /// # Examples
356    ///```
357    /// use loco_rs::storage;
358    /// use std::path::Path;
359    /// use bytes::Bytes;
360    /// pub async fn download() {
361    ///     let storage = storage::Storage::single(storage::drivers::mem::new());
362    ///     assert!(storage.as_store_err("default").is_ok());
363    ///     assert!(storage.as_store_err("store_2").is_err());
364    /// }
365    /// ```
366    ///
367    /// # Errors
368    ///
369    /// Return an error if the given store name not exists
370    // REVIEW(nd): not sure bout the name 'as_store_err' -- it returns result
371    pub fn as_store_err(&self, name: &str) -> StorageResult<&dyn StoreDriver> {
372        self.as_store(name)
373            .ok_or(StorageError::StoreNotFound(name.to_string()))
374    }
375
376    /// Downloads content from storage as a stream, enabling efficient
377    /// handling of large files without loading them entirely into memory.
378    ///
379    /// This method uses the selected strategy for the download operation.
380    ///
381    /// # Examples
382    ///```
383    /// use loco_rs::storage;
384    /// use std::path::Path;
385    /// pub async fn stream_download() {
386    ///     let storage = storage::Storage::single(storage::drivers::mem::new());
387    ///     let path = Path::new("large_file.mp4");
388    ///     
389    ///     let stream = storage.download_stream(path).await.unwrap();
390    ///     // Stream can be converted to axum Body for HTTP response
391    ///     // let body = stream.into_body();
392    /// }
393    /// ```
394    ///
395    /// # Errors
396    ///
397    /// This method returns an error if the download operation fails or if there
398    /// is an issue with the strategy configuration.
399    pub async fn download_stream(&self, path: &Path) -> StorageResult<BytesStream> {
400        self.download_stream_with_policy(path, &*self.strategy)
401            .await
402    }
403
404    /// Downloads content from storage as a stream using a specific strategy.
405    ///
406    /// # Errors
407    ///
408    /// This method returns an error if the download operation fails or if there
409    /// is an issue with the strategy configuration.
410    pub async fn download_stream_with_policy(
411        &self,
412        path: &Path,
413        strategy: &dyn strategies::StorageStrategy,
414    ) -> StorageResult<BytesStream> {
415        strategy.download_stream(self, path).await
416    }
417
418    /// Uploads content from a stream to storage, enabling efficient
419    /// handling of large files without loading them entirely into memory.
420    ///
421    /// This method uses the selected strategy for the upload operation.
422    ///
423    /// # Examples
424    ///```
425    /// use loco_rs::storage;
426    /// use std::path::Path;
427    /// pub async fn stream_upload(stream: storage::stream::BytesStream) {
428    ///     let storage = storage::Storage::single(storage::drivers::mem::new());
429    ///     let path = Path::new("large_file.mp4");
430    ///     
431    ///     storage.upload_stream(path, stream).await.unwrap();
432    /// }
433    /// ```
434    ///
435    /// # Errors
436    ///
437    /// This method returns an error if the upload operation fails or if there
438    /// is an issue with the strategy configuration.
439    pub async fn upload_stream(&self, path: &Path, stream: BytesStream) -> StorageResult<()> {
440        self.upload_stream_with_policy(path, stream, &*self.strategy)
441            .await
442    }
443
444    /// Uploads content from a stream using a specific strategy.
445    ///
446    /// # Errors
447    ///
448    /// This method returns an error if the upload operation fails or if there
449    /// is an issue with the strategy configuration.
450    pub async fn upload_stream_with_policy(
451        &self,
452        path: &Path,
453        stream: BytesStream,
454        strategy: &dyn strategies::StorageStrategy,
455    ) -> StorageResult<()> {
456        strategy.upload_stream(self, path, stream).await
457    }
458}