by_loco/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;
16use std::{
17    collections::BTreeMap,
18    path::{Path, PathBuf},
19};
20
21use bytes::Bytes;
22
23use self::drivers::StoreDriver;
24
25#[derive(thiserror::Error, Debug)]
26#[allow(clippy::module_name_repetitions)]
27pub enum StorageError {
28    #[error("store not found by the given key: {0}")]
29    StoreNotFound(String),
30
31    #[error(transparent)]
32    Store(#[from] opendal::Error),
33
34    #[error("Unable to read data from file {}", path.display().to_string())]
35    UnableToReadBytes { path: PathBuf },
36
37    #[error("secondaries errors")]
38    Multi(BTreeMap<String, String>),
39
40    #[error(transparent)]
41    Any(#[from] Box<dyn std::error::Error + Send + Sync>),
42}
43
44pub type StorageResult<T> = std::result::Result<T, StorageError>;
45
46pub struct Storage {
47    pub stores: BTreeMap<String, Box<dyn StoreDriver>>,
48    pub strategy: Box<dyn strategies::StorageStrategy>,
49}
50
51impl Storage {
52    /// Creates a new storage instance with a single store and the default
53    /// strategy.
54    ///
55    /// # Examples
56    ///```
57    /// use loco_rs::storage;
58    ///
59    /// let storage = storage::Storage::single(storage::drivers::mem::new());
60    /// ```
61    #[must_use]
62    pub fn single(store: Box<dyn StoreDriver>) -> Self {
63        let default_key = "store";
64        Self {
65            strategy: Box::new(strategies::single::SingleStrategy::new(default_key)),
66            stores: BTreeMap::from([(default_key.to_string(), store)]),
67        }
68    }
69
70    /// Creates a new storage instance with the provided stores and strategy.
71    #[must_use]
72    pub fn new(
73        stores: BTreeMap<String, Box<dyn StoreDriver>>,
74        strategy: Box<dyn strategies::StorageStrategy>,
75    ) -> Self {
76        Self { stores, strategy }
77    }
78
79    /// Uploads content to the storage at the specified path.
80    ///
81    /// This method uses the selected strategy for the upload operation.
82    ///
83    /// # Examples
84    ///```
85    /// use loco_rs::storage;
86    /// use std::path::Path;
87    /// use bytes::Bytes;
88    /// pub async fn upload() {
89    ///     let storage = storage::Storage::single(storage::drivers::mem::new());
90    ///     let path = Path::new("example.txt");
91    ///     let content = "Loco!";
92    ///     let result = storage.upload(path, &Bytes::from(content)).await;
93    ///     assert!(result.is_ok());
94    /// }
95    /// ```
96    ///
97    /// # Errors
98    ///
99    /// This method returns an error if the upload operation fails or if there
100    /// is an issue with the strategy configuration.
101    pub async fn upload(&self, path: &Path, content: &Bytes) -> StorageResult<()> {
102        self.upload_with_strategy(path, content, &*self.strategy)
103            .await
104    }
105
106    /// Uploads content to the storage at the specified path using a specific
107    /// strategy.
108    ///
109    /// This method allows specifying a custom strategy for the upload
110    /// operation.
111    ///
112    /// # Errors
113    ///
114    /// This method returns an error if the upload operation fails or if there
115    /// is an issue with the strategy configuration.
116    pub async fn upload_with_strategy(
117        &self,
118        path: &Path,
119        content: &Bytes,
120        strategy: &dyn strategies::StorageStrategy,
121    ) -> StorageResult<()> {
122        strategy.upload(self, path, content).await
123    }
124
125    /// Downloads content from the storage at the specified path.
126    ///
127    /// This method uses the selected strategy for the download operation.
128    ///
129    /// # Examples
130    ///```
131    /// use loco_rs::storage;
132    /// use std::path::Path;
133    /// use bytes::Bytes;
134    /// pub async fn download() {
135    ///     let storage = storage::Storage::single(storage::drivers::mem::new());
136    ///     let path = Path::new("example.txt");
137    ///     let content = "Loco!";
138    ///     storage.upload(path, &Bytes::from(content)).await;
139    ///
140    ///     let result: String = storage.download(path).await.unwrap();
141    ///     assert_eq!(result, "Loco!");
142    /// }
143    /// ```
144    ///
145    /// # Errors
146    ///
147    /// This method returns an error if the download operation fails or if there
148    /// is an issue with the strategy configuration.
149    pub async fn download<T: TryFrom<contents::Contents>>(&self, path: &Path) -> StorageResult<T> {
150        self.download_with_policy(path, &*self.strategy).await
151    }
152
153    /// Downloads content from the storage at the specified path using a
154    /// specific strategy.
155    ///
156    /// This method allows specifying a custom strategy for the download
157    /// operation.
158    ///
159    /// # Errors
160    ///
161    /// This method returns an error if the download operation fails or if there
162    /// is an issue with the strategy configuration.
163    pub async fn download_with_policy<T: TryFrom<contents::Contents>>(
164        &self,
165        path: &Path,
166        strategy: &dyn strategies::StorageStrategy,
167    ) -> StorageResult<T> {
168        let res = strategy.download(self, path).await?;
169        contents::Contents::from(res).try_into().map_or_else(
170            |_| {
171                Err(StorageError::UnableToReadBytes {
172                    path: path.to_path_buf(),
173                })
174            },
175            |content| Ok(content),
176        )
177    }
178
179    /// Deletes content from the storage at the specified path.
180    ///
181    /// This method uses the selected strategy for the delete operation.
182    ///
183    /// # Examples
184    ///```
185    /// use loco_rs::storage;
186    /// use std::path::Path;
187    /// use bytes::Bytes;
188    /// pub async fn download() {
189    ///     let storage = storage::Storage::single(storage::drivers::mem::new());
190    ///     let path = Path::new("example.txt");
191    ///     let content = "Loco!";
192    ///     storage.upload(path, &Bytes::from(content)).await;
193    ///
194    ///     let result = storage.delete(path).await;
195    ///     assert!(result.is_ok());
196    /// }
197    /// ```
198    ///
199    /// # Errors
200    ///
201    /// This method returns an error if the delete operation fails or if there
202    /// is an issue with the strategy configuration.
203    pub async fn delete(&self, path: &Path) -> StorageResult<()> {
204        self.delete_with_policy(path, &*self.strategy).await
205    }
206
207    /// Deletes content from the storage at the specified path using a specific
208    /// strategy.
209    ///
210    /// This method allows specifying a custom strategy for the delete
211    /// operation.
212    ///
213    /// # Errors
214    ///
215    /// This method returns an error if the delete operation fails or if there
216    /// is an issue with the strategy configuration.    
217    pub async fn delete_with_policy(
218        &self,
219        path: &Path,
220        strategy: &dyn strategies::StorageStrategy,
221    ) -> StorageResult<()> {
222        strategy.delete(self, path).await
223    }
224
225    /// Renames content from one path to another in the storage.
226    ///
227    /// This method uses the selected strategy for the rename operation.
228    ///
229    /// # Examples
230    ///```
231    /// use loco_rs::storage;
232    /// use std::path::Path;
233    /// use bytes::Bytes;
234    /// pub async fn download() {
235    ///     let storage = storage::Storage::single(storage::drivers::mem::new());
236    ///     let path = Path::new("example.txt");
237    ///     let content = "Loco!";
238    ///     storage.upload(path, &Bytes::from(content)).await;
239    ///     
240    ///     let new_path = Path::new("new_path.txt");
241    ///     let store = storage.as_store("default").unwrap();
242    ///     assert!(storage.rename(&path, &new_path).await.is_ok());
243    ///     assert!(!store.exists(&path).await.unwrap());
244    ///     assert!(store.exists(&new_path).await.unwrap());
245    /// }
246    /// ```
247    ///
248    /// # Errors
249    ///
250    /// This method returns an error if the rename operation fails or if there
251    /// is an issue with the strategy configuration.
252    pub async fn rename(&self, from: &Path, to: &Path) -> StorageResult<()> {
253        self.rename_with_policy(from, to, &*self.strategy).await
254    }
255
256    /// Renames content from one path to another in the storage using a specific
257    /// strategy.
258    ///
259    /// This method allows specifying a custom strategy for the rename
260    /// operation.
261    ///
262    /// # Errors
263    ///
264    /// This method returns an error if the rename operation fails or if there
265    /// is an issue with the strategy configuration.
266    pub async fn rename_with_policy(
267        &self,
268        from: &Path,
269        to: &Path,
270        strategy: &dyn strategies::StorageStrategy,
271    ) -> StorageResult<()> {
272        strategy.rename(self, from, to).await
273    }
274
275    /// Copies content from one path to another in the storage.
276    ///
277    /// This method uses the selected strategy for the copy operation.
278    ///
279    /// # Examples
280    ///```
281    /// use loco_rs::storage;
282    /// use std::path::Path;
283    /// use bytes::Bytes;
284    /// pub async fn download() {
285    ///     let storage = storage::Storage::single(storage::drivers::mem::new());
286    ///     let path = Path::new("example.txt");
287    ///     let content = "Loco!";
288    ///     storage.upload(path, &Bytes::from(content)).await;
289    ///     
290    ///     let new_path = Path::new("new_path.txt");
291    ///     let store = storage.as_store("default").unwrap();
292    ///     assert!(storage.copy(&path, &new_path).await.is_ok());
293    ///     assert!(store.exists(&path).await.unwrap());
294    ///     assert!(store.exists(&new_path).await.unwrap());
295    /// }
296    /// ```
297    ///
298    /// # Errors
299    ///
300    /// This method returns an error if the copy operation fails or if there is
301    /// an issue with the strategy configuration.
302    pub async fn copy(&self, from: &Path, to: &Path) -> StorageResult<()> {
303        self.copy_with_policy(from, to, &*self.strategy).await
304    }
305
306    /// Copies content from one path to another in the storage using a specific
307    /// strategy.
308    ///
309    /// This method allows specifying a custom strategy for the copy operation.
310    ///
311    /// # Errors
312    ///
313    /// This method returns an error if the copy operation fails or if there is
314    /// an issue with the strategy configuration.
315    pub async fn copy_with_policy(
316        &self,
317        from: &Path,
318        to: &Path,
319        strategy: &dyn strategies::StorageStrategy,
320    ) -> StorageResult<()> {
321        strategy.copy(self, from, to).await
322    }
323
324    /// Returns a reference to the store with the specified name if exists.
325    ///
326    /// # Examples
327    ///```
328    /// use loco_rs::storage;
329    /// use std::path::Path;
330    /// use bytes::Bytes;
331    /// pub async fn download() {
332    ///     let storage = storage::Storage::single(storage::drivers::mem::new());
333    ///     assert!(storage.as_store("default").is_some());
334    ///     assert!(storage.as_store("store_2").is_none());
335    /// }
336    /// ```
337    ///
338    /// # Returns
339    /// Return None if the given name not found.
340    #[must_use]
341    pub fn as_store(&self, name: &str) -> Option<&dyn StoreDriver> {
342        self.stores.get(name).map(|s| &**s)
343    }
344
345    /// Returns a reference to the store with the specified name.
346    ///
347    /// # Examples
348    ///```
349    /// use loco_rs::storage;
350    /// use std::path::Path;
351    /// use bytes::Bytes;
352    /// pub async fn download() {
353    ///     let storage = storage::Storage::single(storage::drivers::mem::new());
354    ///     assert!(storage.as_store_err("default").is_ok());
355    ///     assert!(storage.as_store_err("store_2").is_err());
356    /// }
357    /// ```
358    ///
359    /// # Errors
360    ///
361    /// Return an error if the given store name not exists
362    // REVIEW(nd): not sure bout the name 'as_store_err' -- it returns result
363    pub fn as_store_err(&self, name: &str) -> StorageResult<&dyn StoreDriver> {
364        self.as_store(name)
365            .ok_or(StorageError::StoreNotFound(name.to_string()))
366    }
367}