Skip to main content

rustack_s3_core/
provider.rs

1//! S3 provider that owns the business logic state.
2//!
3//! [`RustackS3`] is the core S3 provider that owns all service state
4//! (buckets, objects, multipart uploads) and the storage backend.
5//! Individual S3 operations are implemented in the [`crate::ops`] submodules
6//! as `handle_*` methods on `RustackS3`.
7//!
8//! The server binary implements the `S3Handler` trait (from `rustack-s3-http`)
9//! for `RustackS3`, bridging the HTTP layer to these handler methods.
10
11use std::sync::Arc;
12
13use crate::{
14    config::S3Config, cors::CorsIndex, state::service::S3ServiceState, storage::InMemoryStorage,
15};
16
17/// The main S3 provider.
18///
19/// All fields are `Arc`-wrapped for cheap cloning and shared ownership
20/// across handler tasks.
21///
22/// # Examples
23///
24/// ```
25/// use rustack_s3_core::RustackS3;
26/// use rustack_s3_core::config::S3Config;
27///
28/// let provider = RustackS3::new(S3Config::default());
29/// assert!(!provider.config().gateway_listen.is_empty());
30/// ```
31#[derive(Debug, Clone)]
32pub struct RustackS3 {
33    /// Bucket and object metadata state.
34    pub(crate) state: Arc<S3ServiceState>,
35    /// Object body storage (in-memory with disk spillover).
36    pub(crate) storage: Arc<InMemoryStorage>,
37    /// Per-bucket CORS rule index for request-time matching.
38    pub(crate) cors_index: Arc<CorsIndex>,
39    /// Provider configuration.
40    pub(crate) config: Arc<S3Config>,
41}
42
43impl RustackS3 {
44    /// Create a new S3 provider with the given configuration.
45    ///
46    /// Initializes an empty service state, a storage backend configured with
47    /// the memory threshold from `config`, and an empty CORS index.
48    #[must_use]
49    pub fn new(config: S3Config) -> Self {
50        let storage = InMemoryStorage::new(config.s3_max_memory_object_size);
51        Self {
52            state: Arc::new(S3ServiceState::new()),
53            storage: Arc::new(storage),
54            cors_index: Arc::new(CorsIndex::new()),
55            config: Arc::new(config),
56        }
57    }
58
59    /// Returns a reference to the service state.
60    #[must_use]
61    pub fn state(&self) -> &S3ServiceState {
62        &self.state
63    }
64
65    /// Returns a reference to the storage backend.
66    #[must_use]
67    pub fn storage(&self) -> &InMemoryStorage {
68        &self.storage
69    }
70
71    /// Returns a reference to the CORS index.
72    #[must_use]
73    pub fn cors_index(&self) -> &CorsIndex {
74        &self.cors_index
75    }
76
77    /// Returns a reference to the provider configuration.
78    #[must_use]
79    pub fn config(&self) -> &S3Config {
80        &self.config
81    }
82
83    /// Reset all state (buckets, objects, multipart uploads, CORS rules).
84    ///
85    /// Primarily useful for testing and the `/_localstack/health` reset endpoint.
86    pub fn reset(&self) {
87        self.state.reset();
88        self.storage.reset();
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_should_create_provider_with_defaults() {
98        let provider = RustackS3::new(S3Config::default());
99        assert_eq!(provider.config().gateway_listen, "0.0.0.0:4566");
100        assert!(provider.state().list_buckets().is_empty());
101    }
102
103    #[test]
104    fn test_should_debug_format_provider() {
105        let provider = RustackS3::new(S3Config::default());
106        let debug_str = format!("{provider:?}");
107        assert!(debug_str.contains("RustackS3"));
108    }
109
110    #[test]
111    fn test_should_share_via_arc() {
112        let provider = Arc::new(RustackS3::new(S3Config::default()));
113        let clone = Arc::clone(&provider);
114        assert_eq!(
115            provider.config().default_region,
116            clone.config().default_region
117        );
118    }
119
120    #[test]
121    fn test_should_clone_provider() {
122        let provider = RustackS3::new(S3Config::default());
123        let cloned = provider.clone();
124        assert_eq!(
125            provider.config().default_region,
126            cloned.config().default_region
127        );
128    }
129
130    #[test]
131    fn test_should_reset_state() {
132        let provider = RustackS3::new(S3Config::default());
133        provider
134            .state()
135            .create_bucket(
136                "test".to_owned(),
137                "us-east-1".to_owned(),
138                crate::state::object::Owner::default(),
139            )
140            .unwrap_or_else(|e| panic!("create failed: {e}"));
141        assert!(provider.state().bucket_exists("test"));
142
143        provider.reset();
144        assert!(!provider.state().bucket_exists("test"));
145    }
146}