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}