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}