Skip to main content

rustrails_storage/service/
mod.rs

1//! Pluggable storage backends.
2
3use std::{sync::Arc, time::Duration};
4
5use async_trait::async_trait;
6use bytes::Bytes;
7use thiserror::Error;
8use url::Url;
9
10pub mod disk;
11pub mod gcs;
12pub mod memory;
13pub mod mirror;
14pub mod s3;
15
16pub use disk::DiskService;
17pub use gcs::GcsService;
18pub use memory::MemoryService;
19pub use mirror::MirrorService;
20pub use s3::S3Service;
21
22/// Errors returned by storage services.
23#[derive(Debug, Error)]
24pub enum StorageError {
25    /// The key was not found.
26    #[error("storage key not found: {0}")]
27    NotFound(String),
28    /// The key already exists.
29    #[error("storage key already exists: {0}")]
30    DuplicateKey(String),
31    /// A lower-level I/O error occurred.
32    #[error("i/o failure for {path}: {source}")]
33    Io {
34        /// The path or key associated with the failure.
35        path: String,
36        /// The underlying I/O error.
37        #[source]
38        source: std::io::Error,
39    },
40    /// An object-store backend returned an error.
41    #[error("object store failure for {path}: {message}")]
42    ObjectStore {
43        /// The path or key associated with the failure.
44        path: String,
45        /// The backend error message.
46        message: String,
47    },
48    /// The generated URL was invalid.
49    #[error("invalid storage url: {0}")]
50    InvalidUrl(String),
51}
52
53/// Shared trait implemented by each storage backend.
54#[async_trait]
55pub trait StorageService: Send + Sync {
56    /// Returns the configured service name.
57    fn name(&self) -> &str;
58
59    /// Uploads a new object.
60    ///
61    /// # Errors
62    ///
63    /// Returns an error when the key already exists or the backend write fails.
64    async fn upload(&self, key: &str, data: Bytes) -> Result<(), StorageError>;
65
66    /// Downloads an existing object.
67    ///
68    /// # Errors
69    ///
70    /// Returns an error when the key does not exist or the backend read fails.
71    async fn download(&self, key: &str) -> Result<Bytes, StorageError>;
72
73    /// Deletes an object if it exists.
74    ///
75    /// # Errors
76    ///
77    /// Returns an error when the backend delete fails.
78    async fn delete(&self, key: &str) -> Result<(), StorageError>;
79
80    /// Returns whether the object exists.
81    ///
82    /// # Errors
83    ///
84    /// Returns an error when the backend existence check fails unexpectedly.
85    async fn exists(&self, key: &str) -> Result<bool, StorageError>;
86
87    /// Generates a download URL.
88    ///
89    /// # Errors
90    ///
91    /// Returns an error when the URL cannot be generated.
92    async fn url(&self, key: &str, expires_in: Duration) -> Result<Url, StorageError>;
93}
94
95/// Shared trait object type for dynamic services.
96pub type DynStorageService = Arc<dyn StorageService>;
97
98pub(crate) fn checked_key(key: &str) -> Result<&str, StorageError> {
99    let trimmed = key.trim();
100    if trimmed.is_empty() {
101        return Err(StorageError::InvalidUrl(
102            "storage key must not be empty".to_owned(),
103        ));
104    }
105    Ok(trimmed)
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_checked_key_rejects_empty_values() {
114        assert!(checked_key("   ").is_err());
115    }
116
117    #[test]
118    fn test_checked_key_returns_trimmed_input() {
119        assert_eq!(checked_key("abc").expect("key should be valid"), "abc");
120    }
121}