Skip to main content

rustack_s3_core/
config.rs

1//! S3-specific configuration.
2//!
3//! Provides [`S3Config`] for configuring the Rustack S3 service.
4//! Configuration values are loaded from environment variables, matching
5//! LocalStack conventions for S3-specific settings.
6
7use serde::{Deserialize, Serialize};
8use typed_builder::TypedBuilder;
9
10/// S3 service configuration.
11///
12/// All fields have sensible defaults matching LocalStack behavior. Configuration
13/// can be loaded from environment variables via [`S3Config::from_env`].
14///
15/// # Examples
16///
17/// ```
18/// use rustack_s3_core::config::S3Config;
19///
20/// let config = S3Config::default();
21/// assert_eq!(config.gateway_listen, "0.0.0.0:4566");
22/// assert!(config.s3_virtual_hosting);
23/// ```
24#[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)]
25#[serde(rename_all = "camelCase")]
26pub struct S3Config {
27    /// Bind address for the gateway (e.g. `"0.0.0.0:4566"`).
28    #[builder(default = String::from("0.0.0.0:4566"))]
29    pub gateway_listen: String,
30
31    /// Whether S3 virtual-hosted-style addressing is enabled.
32    #[builder(default = true)]
33    pub s3_virtual_hosting: bool,
34
35    /// Domain for S3 virtual hosting resolution.
36    #[builder(default = String::from("s3.localhost.localstack.cloud"))]
37    pub s3_domain: String,
38
39    /// Whether to skip signature validation on incoming requests.
40    #[builder(default = true)]
41    pub s3_skip_signature_validation: bool,
42
43    /// Maximum object size (in bytes) kept entirely in memory before spilling to disk.
44    #[builder(default = 524_288)]
45    pub s3_max_memory_object_size: usize,
46
47    /// Default AWS region for this S3 service instance.
48    #[builder(default = String::from("us-east-1"))]
49    pub default_region: String,
50
51    /// Log level filter string (e.g. `"info"`, `"debug"`).
52    #[builder(default = String::from("info"))]
53    pub log_level: String,
54
55    /// Whether persistence (durable storage) is enabled.
56    #[builder(default = false)]
57    pub persistence: bool,
58
59    /// Data directory used when persistence is enabled.
60    #[builder(default = String::from("/var/lib/localstack"))]
61    pub data_dir: String,
62}
63
64impl Default for S3Config {
65    fn default() -> Self {
66        Self {
67            gateway_listen: String::from("0.0.0.0:4566"),
68            s3_virtual_hosting: true,
69            s3_domain: String::from("s3.localhost.localstack.cloud"),
70            s3_skip_signature_validation: true,
71            s3_max_memory_object_size: 524_288,
72            default_region: String::from("us-east-1"),
73            log_level: String::from("info"),
74            persistence: false,
75            data_dir: String::from("/var/lib/localstack"),
76        }
77    }
78}
79
80impl S3Config {
81    /// Load configuration from environment variables.
82    ///
83    /// Reads the following environment variables (falling back to defaults):
84    ///
85    /// | Variable | Default |
86    /// |----------|---------|
87    /// | `GATEWAY_LISTEN` | `0.0.0.0:4566` |
88    /// | `S3_VIRTUAL_HOSTING` | `true` |
89    /// | `S3_DOMAIN` | `s3.localhost.localstack.cloud` |
90    /// | `S3_SKIP_SIGNATURE_VALIDATION` | `true` |
91    /// | `S3_MAX_MEMORY_OBJECT_SIZE` | `524288` |
92    /// | `DEFAULT_REGION` | `us-east-1` |
93    /// | `LOG_LEVEL` | `info` |
94    /// | `PERSISTENCE` | `false` |
95    /// | `DATA_DIR` | `/var/lib/localstack` |
96    ///
97    /// # Examples
98    ///
99    /// ```
100    /// use rustack_s3_core::config::S3Config;
101    ///
102    /// let config = S3Config::from_env();
103    /// assert!(!config.gateway_listen.is_empty());
104    /// ```
105    #[must_use]
106    pub fn from_env() -> Self {
107        let mut config = Self::default();
108
109        if let Ok(v) = std::env::var("GATEWAY_LISTEN") {
110            config.gateway_listen = v;
111        }
112        if let Ok(v) = std::env::var("S3_VIRTUAL_HOSTING") {
113            config.s3_virtual_hosting = parse_bool(&v);
114        }
115        if let Ok(v) = std::env::var("S3_DOMAIN") {
116            config.s3_domain = v;
117        }
118        if let Ok(v) = std::env::var("S3_SKIP_SIGNATURE_VALIDATION") {
119            config.s3_skip_signature_validation = parse_bool(&v);
120        }
121        if let Ok(v) = std::env::var("S3_MAX_MEMORY_OBJECT_SIZE") {
122            if let Ok(n) = v.parse::<usize>() {
123                config.s3_max_memory_object_size = n;
124            }
125        }
126        if let Ok(v) = std::env::var("DEFAULT_REGION") {
127            config.default_region = v;
128        }
129        if let Ok(v) = std::env::var("LOG_LEVEL") {
130            config.log_level = v;
131        }
132        if let Ok(v) = std::env::var("PERSISTENCE") {
133            config.persistence = parse_bool(&v);
134        }
135        if let Ok(v) = std::env::var("DATA_DIR") {
136            config.data_dir = v;
137        }
138
139        config
140    }
141}
142
143/// Parse a string as a boolean, accepting `"1"` and `"true"` (case-insensitive).
144fn parse_bool(value: &str) -> bool {
145    value == "1" || value.eq_ignore_ascii_case("true")
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_should_create_default_config() {
154        let config = S3Config::default();
155        assert_eq!(config.gateway_listen, "0.0.0.0:4566");
156        assert!(config.s3_virtual_hosting);
157        assert_eq!(config.s3_domain, "s3.localhost.localstack.cloud");
158        assert!(config.s3_skip_signature_validation);
159        assert_eq!(config.s3_max_memory_object_size, 524_288);
160        assert_eq!(config.default_region, "us-east-1");
161        assert_eq!(config.log_level, "info");
162        assert!(!config.persistence);
163        assert_eq!(config.data_dir, "/var/lib/localstack");
164    }
165
166    #[test]
167    fn test_should_load_from_env() {
168        let config = S3Config::from_env();
169        assert!(!config.gateway_listen.is_empty());
170    }
171
172    #[test]
173    fn test_should_build_with_typed_builder() {
174        let config = S3Config::builder()
175            .gateway_listen("127.0.0.1:9999".into())
176            .s3_virtual_hosting(false)
177            .s3_domain("custom.domain".into())
178            .s3_skip_signature_validation(false)
179            .s3_max_memory_object_size(1024)
180            .default_region("eu-west-1".into())
181            .log_level("debug".into())
182            .persistence(true)
183            .data_dir("/tmp/data".into())
184            .build();
185
186        assert_eq!(config.gateway_listen, "127.0.0.1:9999");
187        assert!(!config.s3_virtual_hosting);
188        assert_eq!(config.s3_domain, "custom.domain");
189        assert!(!config.s3_skip_signature_validation);
190        assert_eq!(config.s3_max_memory_object_size, 1024);
191        assert_eq!(config.default_region, "eu-west-1");
192        assert_eq!(config.log_level, "debug");
193        assert!(config.persistence);
194        assert_eq!(config.data_dir, "/tmp/data");
195    }
196
197    #[test]
198    fn test_should_serialize_to_camel_case_json() {
199        let config = S3Config::default();
200        let json = serde_json::to_string(&config).expect("test serialization");
201        assert!(json.contains("gatewayListen"));
202        assert!(json.contains("s3VirtualHosting"));
203    }
204
205    #[test]
206    fn test_should_parse_bool_values() {
207        assert!(parse_bool("1"));
208        assert!(parse_bool("true"));
209        assert!(parse_bool("TRUE"));
210        assert!(parse_bool("True"));
211        assert!(!parse_bool("0"));
212        assert!(!parse_bool("false"));
213        assert!(!parse_bool(""));
214    }
215}