Skip to main content

azalia_remi/
lib.rs

1// 🐻‍❄️🪚 azalia: Noelware's Rust commons library.
2// Copyright (c) 2024-2025 Noelware, LLC. <team@noelware.org>
3//
4// Permission is hereby granted, free of charge, to any person obtaining a copy
5// of this software and associated documentation files (the "Software"), to deal
6// in the Software without restriction, including without limitation the rights
7// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8// copies of the Software, and to permit persons to whom the Software is
9// furnished to do so, subject to the following conditions:
10//
11// The above copyright notice and this permission notice shall be included in all
12// copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20// SOFTWARE.
21
22//! <div align="center">
23//!     <h3>🐻‍❄️🪚 <code>azalia-remi</code></h3>
24//!     <h4>Unified storage services for <a href="https://docs.rs/remi/*/remi/trait.StorageService.html">remi::StorageService</a> of official crates</h4>
25//!     <hr />
26//! </div>
27//!
28//! ## 🐻‍❄️🪚 `azalia-remi`
29//!
30//! **azalia-remi** adds a unified storage service on top of **remi-rs** for allow configuring multiple storage
31//! services but only uses one from what the end user wants.
32//!
33//! This uses Cargo's crate features to implicitilly allow you to pick out which Remi-based crates to implement
34//! into your applications. You can use the `features = ["all"]` in your Cargo.toml's definition of `azalia-remi`
35//! to include all crates.
36//!
37//! ## Example
38//! ```no_run
39//! // Cargo.toml:
40//! //
41//! // [dependencies]
42//! // tokio = { version = "*", features = ["full"] }
43//! // azalia-remi = { version = "^0", features = ["fs"] }
44//!
45//! use azalia_remi::{
46//!     StorageService,
47//!     Config,
48//!
49//!     core::StorageService as _,
50//!     fs
51//! };
52//!
53//! # #[tokio::main]
54//! # async fn main() {
55//! let config = fs::StorageConfig {
56//!     directory: "/data".into(),
57//! };
58//!
59//! let service = StorageService::Filesystem(fs::StorageService::with_config(config));
60//! service.init().await.unwrap(); // initialize the fs version of remi
61//!
62//! // do whatever you want
63//! # }
64//! ```
65
66#![doc(html_logo_url = "https://cdn.floofy.dev/images/trans.png")]
67#![cfg_attr(any(noeldoc, docsrs), feature(doc_cfg))]
68#![allow(non_camel_case_types)]
69
70use remi::{ListBlobsRequest, UploadRequest};
71use std::{borrow::Cow, path::Path};
72
73pub use remi as core;
74
75#[cfg(feature = "gridfs")]
76#[cfg_attr(any(docsrs, noeldoc), doc(cfg(feature = "gridfs")))]
77pub use remi_gridfs as gridfs;
78
79#[cfg(feature = "azure")]
80#[cfg_attr(any(docsrs, noeldoc), doc(cfg(feature = "azure")))]
81pub use remi_azure as azure;
82
83#[cfg(feature = "s3")]
84#[cfg_attr(any(docsrs, noeldoc), doc(cfg(feature = "s3")))]
85pub use remi_s3 as s3;
86
87#[cfg(feature = "fs")]
88#[cfg_attr(any(docsrs, noeldoc), doc(cfg(feature = "fs")))]
89pub use remi_fs as fs;
90
91macro_rules! mk_storage_service_impl {
92    (
93        $(#[$meta:meta])*
94        $($feat:literal => $field:ident as $ty:ty {
95            $(#[$error_meta:meta])*
96            Error: $error:ty;
97
98            $(#[$config_meta:meta])*
99            Config: $config:ty;
100
101            Display: |$f:ident, $error_name:ident| $display:expr;
102        })*
103    ) => {
104        $(#[$meta])*
105        pub enum StorageService {
106            $(
107                #[cfg(feature = $feat)]
108                #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = $feat)))]
109                $field($ty),
110            )*
111
112            __non_exhaustive
113        }
114
115        /// Error variant when using methods from [`StorageService`].
116        #[derive(Debug)]
117        #[allow(non_camel_case_types)]
118        pub enum Error {
119            $(
120                #[cfg(feature = $feat)]
121                #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = $feat)))]
122                $(#[$error_meta])*
123                $field($error),
124            )*
125
126            __non_exhaustive,
127        }
128
129        impl ::std::fmt::Display for Error {
130            #[allow(unused)]
131            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
132                match self {
133                    $(
134                        #[cfg(feature = $feat)]
135                        Error::$field(err) => {
136                            let $error_name = err;
137                            let $f = f;
138
139                            $display
140                        },
141                    )*
142
143                    _ => unreachable!()
144                }
145            }
146        }
147
148        impl ::std::error::Error for Error {
149            fn source(&self) -> Option<&(dyn ::std::error::Error + 'static)> {
150                match self {
151                    $(
152                        #[cfg(feature = $feat)]
153                        Error::$field(err) => Some(err),
154                    )*
155
156                    _ => None
157                }
158            }
159        }
160
161        $(
162            #[cfg(feature = $feat)]
163            #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = $feat)))]
164            impl ::core::convert::From<$error> for Error {
165                fn from(value: $error) -> Self {
166                    Error::$field(value)
167                }
168            }
169        )*
170
171        #[derive(Debug, Clone, Default)]
172        #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
173        #[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
174        #[non_exhaustive]
175        pub enum Config {
176            $(
177                #[cfg(feature = $feat)]
178                #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = $feat)))]
179                #[doc = concat!("Configuration variant that can be used to configure the [`StorageService::", stringify!($field), "`] variant.")]
180                $(#[$config_meta])*
181                $field($config),
182            )*
183
184            #[default]
185            Unknown,
186        }
187    };
188}
189
190mk_storage_service_impl! {
191    /// Represents a unified [`StorageService`][remi::StorageService] that can be
192    /// either the following:
193    ///
194    #[cfg_attr(feature = "gridfs", doc = "* [`remi_gridfs::StorageService`]")]
195    #[cfg_attr(feature = "azure", doc = "* [`remi_azure::StorageService`]")]
196    #[cfg_attr(feature = "fs", doc = "* [`remi_fs::StorageService`]")]
197    #[cfg_attr(feature = "s3", doc = "* [`remi_s3::StorageService`]")]
198    #[allow(non_camel_case_types)]
199    #[derive(Clone)]
200
201    "gridfs" => Gridfs as ::remi_gridfs::StorageService {
202        /// Error variant that can happen when using [`remi_gridfs::StorageService`].
203        Error: ::remi_gridfs::mongodb::error::Error;
204        Config: ::remi_gridfs::StorageConfig;
205        Display: |f, err| match &*err.kind {
206            ::remi_gridfs::mongodb::error::ErrorKind::Custom(msg) => {
207                if let Some(msg) = msg.downcast_ref::<&str>() {
208                    f.write_str(msg)
209                } else if let Some(msg) = msg.downcast_ref::<String>() {
210                    f.write_str(msg)
211                } else {
212                    ::std::fmt::Display::fmt(err, f)
213                }
214            },
215
216            _ => ::std::fmt::Display::fmt(err, f),
217        };
218    }
219
220    "azure" => Azure as ::remi_azure::StorageService {
221        /// Error variant that can happen when using [`remi_azure::StorageService`].
222        Error: ::remi_azure::core::storage::Error;
223        Config: ::remi_azure::StorageConfig;
224        Display: |f, err| ::std::fmt::Display::fmt(err, f);
225    }
226
227    "fs" => Filesystem as ::remi_fs::StorageService {
228        /// Error variant that can happen when using [`remi_fs::StorageService`].
229        Error: ::std::io::Error;
230        Config: ::remi_fs::StorageConfig;
231        Display: |f, err| ::std::fmt::Display::fmt(err, f);
232    }
233
234    "s3" => S3 as ::remi_s3::StorageService {
235        /// Error variant that can happen when using [`remi_s3::StorageService`].
236        Error: ::remi_s3::Error;
237        Config: ::remi_s3::StorageConfig;
238        Display: |f, err| ::std::fmt::Display::fmt(err, f);
239    }
240}
241
242impl StorageService {
243    #[cfg(feature = "fs")]
244    #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "fs")))]
245    /// Returns a reference to [`remi_fs::StorageService`] if we are in the [`StorageService::Filesystem`]
246    /// variant, returns `None` otherwise.
247    pub fn as_filesystem(&self) -> Option<&remi_fs::StorageService> {
248        match *self {
249            Self::Filesystem(ref fs) => Some(fs),
250            _ => None,
251        }
252    }
253
254    #[cfg(feature = "s3")]
255    #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "s3")))]
256    /// Returns a reference to [`remi_s3::StorageService`] if we are in the [`StorageService::S3`]
257    /// variant, returns `None` otherwise.
258    pub fn as_s3(&self) -> Option<&remi_s3::StorageService> {
259        match *self {
260            Self::S3(ref s3) => Some(s3),
261            _ => None,
262        }
263    }
264
265    #[cfg(feature = "azure")]
266    #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "azure")))]
267    /// Returns a reference to [`remi_azure::StorageService`] if we are in the [`StorageService::Azure`]
268    /// variant, returns `None` otherwise.
269    pub fn as_azure(&self) -> Option<&remi_azure::StorageService> {
270        match *self {
271            Self::Azure(ref azure) => Some(azure),
272            _ => None,
273        }
274    }
275
276    #[cfg(feature = "gridfs")]
277    #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "gridfs")))]
278    /// Returns a reference to [`remi_gridfs::StorageService`] if we are in the [`StorageService::Gridfs`]
279    /// variant, returns `None` otherwise.
280    pub fn as_gridfs(&self) -> Option<&remi_gridfs::StorageService> {
281        match *self {
282            Self::Gridfs(ref gridfs) => Some(gridfs),
283            _ => None,
284        }
285    }
286}
287
288#[remi::async_trait]
289#[allow(unused)]
290impl remi::StorageService for StorageService {
291    type Error = Error;
292
293    fn name(&self) -> Cow<'static, str> {
294        let name = match self {
295            #[cfg(feature = "fs")]
296            StorageService::Filesystem(service) => service.name(),
297
298            #[cfg(feature = "s3")]
299            StorageService::S3(service) => service.name(),
300
301            #[cfg(feature = "azure")]
302            StorageService::Azure(service) => service.name(),
303
304            #[cfg(feature = "gridfs")]
305            StorageService::Gridfs(service) => service.name(),
306
307            _ => Cow::Borrowed("<unknown>"),
308        };
309
310        Cow::Owned(format!("azalia:remi[{name}]"))
311    }
312
313    async fn init(&self) -> Result<(), Self::Error> {
314        match self {
315            #[cfg(feature = "fs")]
316            StorageService::Filesystem(service) => service.init().await.map_err(From::from),
317
318            #[cfg(feature = "s3")]
319            StorageService::S3(service) => service.init().await.map_err(From::from),
320
321            #[cfg(feature = "azure")]
322            StorageService::Azure(service) => service.init().await.map_err(From::from),
323
324            #[cfg(feature = "gridfs")]
325            StorageService::Gridfs(service) => service.init().await.map_err(From::from),
326
327            _ => unreachable!(),
328        }
329    }
330
331    async fn open<P: AsRef<Path> + Send>(&self, path: P) -> Result<Option<remi::Bytes>, Self::Error> {
332        match self {
333            #[cfg(feature = "fs")]
334            StorageService::Filesystem(service) => service.open(path).await.map_err(From::from),
335
336            #[cfg(feature = "s3")]
337            StorageService::S3(service) => service.open(path).await.map_err(From::from),
338
339            #[cfg(feature = "azure")]
340            StorageService::Azure(service) => service.open(path).await.map_err(From::from),
341
342            #[cfg(feature = "gridfs")]
343            StorageService::Gridfs(service) => service.open(path).await.map_err(From::from),
344
345            _ => unreachable!(),
346        }
347    }
348
349    async fn blob<P: AsRef<Path> + Send>(&self, path: P) -> Result<Option<remi::Blob>, Self::Error> {
350        match self {
351            #[cfg(feature = "fs")]
352            StorageService::Filesystem(service) => service.blob(path).await.map_err(From::from),
353
354            #[cfg(feature = "s3")]
355            StorageService::S3(service) => service.blob(path).await.map_err(From::from),
356
357            #[cfg(feature = "azure")]
358            StorageService::Azure(service) => service.blob(path).await.map_err(From::from),
359
360            #[cfg(feature = "gridfs")]
361            StorageService::Gridfs(service) => service.blob(path).await.map_err(From::from),
362
363            _ => unreachable!(),
364        }
365    }
366
367    async fn blobs<P: AsRef<Path> + Send>(
368        &self,
369        path: Option<P>,
370        options: Option<ListBlobsRequest>,
371    ) -> Result<Vec<remi::Blob>, Self::Error> {
372        match self {
373            #[cfg(feature = "fs")]
374            StorageService::Filesystem(service) => service.blobs(path, options).await.map_err(From::from),
375
376            #[cfg(feature = "s3")]
377            StorageService::S3(service) => service.blobs(path, options).await.map_err(From::from),
378
379            #[cfg(feature = "azure")]
380            StorageService::Azure(service) => service.blobs(path, options).await.map_err(From::from),
381
382            #[cfg(feature = "gridfs")]
383            StorageService::Gridfs(service) => service.blobs(path, options).await.map_err(From::from),
384
385            _ => unreachable!(),
386        }
387    }
388
389    async fn delete<P: AsRef<Path> + Send>(&self, path: P) -> Result<(), Self::Error> {
390        match self {
391            #[cfg(feature = "fs")]
392            StorageService::Filesystem(service) => service.delete(path).await.map_err(From::from),
393
394            #[cfg(feature = "s3")]
395            StorageService::S3(service) => service.delete(path).await.map_err(From::from),
396
397            #[cfg(feature = "azure")]
398            StorageService::Azure(service) => service.delete(path).await.map_err(From::from),
399
400            #[cfg(feature = "gridfs")]
401            StorageService::Gridfs(service) => service.delete(path).await.map_err(From::from),
402
403            _ => unreachable!(),
404        }
405    }
406
407    async fn exists<P: AsRef<Path> + Send>(&self, path: P) -> Result<bool, Self::Error> {
408        match self {
409            #[cfg(feature = "fs")]
410            StorageService::Filesystem(service) => service.exists(path).await.map_err(From::from),
411
412            #[cfg(feature = "s3")]
413            StorageService::S3(service) => service.exists(path).await.map_err(From::from),
414
415            #[cfg(feature = "azure")]
416            StorageService::Azure(service) => service.exists(path).await.map_err(From::from),
417
418            #[cfg(feature = "gridfs")]
419            StorageService::Gridfs(service) => service.exists(path).await.map_err(From::from),
420
421            _ => unreachable!(),
422        }
423    }
424
425    async fn upload<P: AsRef<Path> + Send>(&self, path: P, request: UploadRequest) -> Result<(), Self::Error> {
426        match self {
427            #[cfg(feature = "fs")]
428            StorageService::Filesystem(service) => service.upload(path, request).await.map_err(From::from),
429
430            #[cfg(feature = "s3")]
431            StorageService::S3(service) => service.upload(path, request).await.map_err(From::from),
432
433            #[cfg(feature = "azure")]
434            StorageService::Azure(service) => service.upload(path, request).await.map_err(From::from),
435
436            #[cfg(feature = "gridfs")]
437            StorageService::Gridfs(service) => service.upload(path, request).await.map_err(From::from),
438
439            _ => unreachable!(),
440        }
441    }
442
443    #[cfg(feature = "unstable")]
444    #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "unstable")))]
445    async fn healthcheck(&self) -> Result<(), Self::Error> {
446        match self {
447            #[cfg(feature = "fs")]
448            StorageService::Filesystem(service) => service.healthcheck().await.map_err(From::from),
449
450            #[cfg(feature = "s3")]
451            StorageService::S3(service) => service.healthcheck().await.map_err(From::from),
452
453            #[cfg(feature = "azure")]
454            StorageService::Azure(service) => service.healthcheck().await.map_err(From::from),
455
456            #[cfg(feature = "gridfs")]
457            StorageService::Gridfs(service) => service.healthcheck().await.map_err(From::from),
458
459            _ => unreachable!(),
460        }
461    }
462}