rustrails-storage 0.1.2

File storage (ActiveStorage equivalent)
Documentation
//! Pluggable storage backends.

use std::{sync::Arc, time::Duration};

use async_trait::async_trait;
use bytes::Bytes;
use thiserror::Error;
use url::Url;

pub mod disk;
pub mod gcs;
pub mod memory;
pub mod mirror;
pub mod s3;

pub use disk::DiskService;
pub use gcs::GcsService;
pub use memory::MemoryService;
pub use mirror::MirrorService;
pub use s3::S3Service;

/// Errors returned by storage services.
#[derive(Debug, Error)]
pub enum StorageError {
    /// The key was not found.
    #[error("storage key not found: {0}")]
    NotFound(String),
    /// The key already exists.
    #[error("storage key already exists: {0}")]
    DuplicateKey(String),
    /// A lower-level I/O error occurred.
    #[error("i/o failure for {path}: {source}")]
    Io {
        /// The path or key associated with the failure.
        path: String,
        /// The underlying I/O error.
        #[source]
        source: std::io::Error,
    },
    /// An object-store backend returned an error.
    #[error("object store failure for {path}: {message}")]
    ObjectStore {
        /// The path or key associated with the failure.
        path: String,
        /// The backend error message.
        message: String,
    },
    /// The generated URL was invalid.
    #[error("invalid storage url: {0}")]
    InvalidUrl(String),
}

/// Shared trait implemented by each storage backend.
#[async_trait]
pub trait StorageService: Send + Sync {
    /// Returns the configured service name.
    fn name(&self) -> &str;

    /// Uploads a new object.
    ///
    /// # Errors
    ///
    /// Returns an error when the key already exists or the backend write fails.
    async fn upload(&self, key: &str, data: Bytes) -> Result<(), StorageError>;

    /// Downloads an existing object.
    ///
    /// # Errors
    ///
    /// Returns an error when the key does not exist or the backend read fails.
    async fn download(&self, key: &str) -> Result<Bytes, StorageError>;

    /// Deletes an object if it exists.
    ///
    /// # Errors
    ///
    /// Returns an error when the backend delete fails.
    async fn delete(&self, key: &str) -> Result<(), StorageError>;

    /// Returns whether the object exists.
    ///
    /// # Errors
    ///
    /// Returns an error when the backend existence check fails unexpectedly.
    async fn exists(&self, key: &str) -> Result<bool, StorageError>;

    /// Generates a download URL.
    ///
    /// # Errors
    ///
    /// Returns an error when the URL cannot be generated.
    async fn url(&self, key: &str, expires_in: Duration) -> Result<Url, StorageError>;
}

/// Shared trait object type for dynamic services.
pub type DynStorageService = Arc<dyn StorageService>;

pub(crate) fn checked_key(key: &str) -> Result<&str, StorageError> {
    let trimmed = key.trim();
    if trimmed.is_empty() {
        return Err(StorageError::InvalidUrl(
            "storage key must not be empty".to_owned(),
        ));
    }
    Ok(trimmed)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_checked_key_rejects_empty_values() {
        assert!(checked_key("   ").is_err());
    }

    #[test]
    fn test_checked_key_returns_trimmed_input() {
        assert_eq!(checked_key("abc").expect("key should be valid"), "abc");
    }
}