Skip to main content

rc_core/
alias.rs

1//! Alias management
2//!
3//! Aliases are named references to S3-compatible storage endpoints,
4//! including connection details and credentials.
5
6use serde::{Deserialize, Serialize};
7
8use crate::config::ConfigManager;
9use crate::error::{Error, Result};
10
11/// Retry configuration for an alias
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct RetryConfig {
14    /// Maximum number of retry attempts
15    #[serde(default = "default_max_attempts")]
16    pub max_attempts: u32,
17
18    /// Initial backoff duration in milliseconds
19    #[serde(default = "default_initial_backoff")]
20    pub initial_backoff_ms: u64,
21
22    /// Maximum backoff duration in milliseconds
23    #[serde(default = "default_max_backoff")]
24    pub max_backoff_ms: u64,
25}
26
27fn default_max_attempts() -> u32 {
28    3
29}
30
31fn default_initial_backoff() -> u64 {
32    100
33}
34
35fn default_max_backoff() -> u64 {
36    10000
37}
38
39impl Default for RetryConfig {
40    fn default() -> Self {
41        Self {
42            max_attempts: default_max_attempts(),
43            initial_backoff_ms: default_initial_backoff(),
44            max_backoff_ms: default_max_backoff(),
45        }
46    }
47}
48
49/// Timeout configuration for an alias
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct TimeoutConfig {
52    /// Connection timeout in milliseconds
53    #[serde(default = "default_connect_timeout")]
54    pub connect_ms: u64,
55
56    /// Read timeout in milliseconds
57    #[serde(default = "default_read_timeout")]
58    pub read_ms: u64,
59}
60
61fn default_connect_timeout() -> u64 {
62    5000
63}
64
65fn default_read_timeout() -> u64 {
66    30000
67}
68
69impl Default for TimeoutConfig {
70    fn default() -> Self {
71        Self {
72            connect_ms: default_connect_timeout(),
73            read_ms: default_read_timeout(),
74        }
75    }
76}
77
78/// An alias represents a named S3-compatible storage endpoint
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct Alias {
81    /// Unique name for this alias
82    pub name: String,
83
84    /// S3 endpoint URL
85    pub endpoint: String,
86
87    /// Access key ID
88    pub access_key: String,
89
90    /// Secret access key
91    pub secret_key: String,
92
93    /// AWS region
94    #[serde(default = "default_region")]
95    pub region: String,
96
97    /// Signature version: "v4" or "v2"
98    #[serde(default = "default_signature")]
99    pub signature: String,
100
101    /// Bucket lookup style: "auto", "path", or "dns"
102    #[serde(default = "default_bucket_lookup")]
103    pub bucket_lookup: String,
104
105    /// Allow insecure TLS connections
106    #[serde(default)]
107    pub insecure: bool,
108
109    /// Path to custom CA bundle
110    #[serde(default, skip_serializing_if = "Option::is_none")]
111    pub ca_bundle: Option<String>,
112
113    /// Retry configuration
114    #[serde(default, skip_serializing_if = "Option::is_none")]
115    pub retry: Option<RetryConfig>,
116
117    /// Timeout configuration
118    #[serde(default, skip_serializing_if = "Option::is_none")]
119    pub timeout: Option<TimeoutConfig>,
120}
121
122fn default_region() -> String {
123    "us-east-1".to_string()
124}
125
126fn default_signature() -> String {
127    "v4".to_string()
128}
129
130fn default_bucket_lookup() -> String {
131    "auto".to_string()
132}
133
134impl Alias {
135    /// Create a new alias with required fields
136    pub fn new(
137        name: impl Into<String>,
138        endpoint: impl Into<String>,
139        access_key: impl Into<String>,
140        secret_key: impl Into<String>,
141    ) -> Self {
142        Self {
143            name: name.into(),
144            endpoint: endpoint.into(),
145            access_key: access_key.into(),
146            secret_key: secret_key.into(),
147            region: default_region(),
148            signature: default_signature(),
149            bucket_lookup: default_bucket_lookup(),
150            insecure: false,
151            ca_bundle: None,
152            retry: None,
153            timeout: None,
154        }
155    }
156
157    /// Get the effective retry configuration
158    pub fn retry_config(&self) -> RetryConfig {
159        self.retry.clone().unwrap_or_default()
160    }
161
162    /// Get the effective timeout configuration
163    pub fn timeout_config(&self) -> TimeoutConfig {
164        self.timeout.clone().unwrap_or_default()
165    }
166}
167
168/// Manager for alias operations
169pub struct AliasManager {
170    config_manager: ConfigManager,
171}
172
173impl AliasManager {
174    /// Create a new AliasManager with a specific ConfigManager
175    pub fn with_config_manager(config_manager: ConfigManager) -> Self {
176        Self { config_manager }
177    }
178
179    /// Create a new AliasManager using the default config location
180    pub fn new() -> Result<Self> {
181        let config_manager = ConfigManager::new()?;
182        Ok(Self { config_manager })
183    }
184
185    /// List all configured aliases
186    pub fn list(&self) -> Result<Vec<Alias>> {
187        let config = self.config_manager.load()?;
188        Ok(config.aliases)
189    }
190
191    /// Get an alias by name
192    pub fn get(&self, name: &str) -> Result<Alias> {
193        let config = self.config_manager.load()?;
194        config
195            .aliases
196            .into_iter()
197            .find(|a| a.name == name)
198            .ok_or_else(|| Error::AliasNotFound(name.to_string()))
199    }
200
201    /// Add or update an alias
202    pub fn set(&self, alias: Alias) -> Result<()> {
203        let mut config = self.config_manager.load()?;
204
205        // Remove existing alias with same name
206        config.aliases.retain(|a| a.name != alias.name);
207        config.aliases.push(alias);
208
209        self.config_manager.save(&config)
210    }
211
212    /// Remove an alias
213    pub fn remove(&self, name: &str) -> Result<()> {
214        let mut config = self.config_manager.load()?;
215        let original_len = config.aliases.len();
216
217        config.aliases.retain(|a| a.name != name);
218
219        if config.aliases.len() == original_len {
220            return Err(Error::AliasNotFound(name.to_string()));
221        }
222
223        self.config_manager.save(&config)
224    }
225
226    /// Check if an alias exists
227    pub fn exists(&self, name: &str) -> Result<bool> {
228        let config = self.config_manager.load()?;
229        Ok(config.aliases.iter().any(|a| a.name == name))
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use tempfile::TempDir;
237
238    fn temp_alias_manager() -> (AliasManager, TempDir) {
239        let temp_dir = TempDir::new().unwrap();
240        let config_path = temp_dir.path().join("config.toml");
241        let config_manager = ConfigManager::with_path(config_path);
242        let alias_manager = AliasManager::with_config_manager(config_manager);
243        (alias_manager, temp_dir)
244    }
245
246    #[test]
247    fn test_alias_new() {
248        let alias = Alias::new("test", "http://localhost:9000", "access", "secret");
249        assert_eq!(alias.name, "test");
250        assert_eq!(alias.endpoint, "http://localhost:9000");
251        assert_eq!(alias.region, "us-east-1");
252        assert_eq!(alias.signature, "v4");
253        assert_eq!(alias.bucket_lookup, "auto");
254        assert!(!alias.insecure);
255    }
256
257    #[test]
258    fn test_alias_manager_set_and_get() {
259        let (manager, _temp_dir) = temp_alias_manager();
260
261        let alias = Alias::new("local", "http://localhost:9000", "accesskey", "secretkey");
262        manager.set(alias).unwrap();
263
264        let retrieved = manager.get("local").unwrap();
265        assert_eq!(retrieved.name, "local");
266        assert_eq!(retrieved.endpoint, "http://localhost:9000");
267    }
268
269    #[test]
270    fn test_alias_manager_list() {
271        let (manager, _temp_dir) = temp_alias_manager();
272
273        manager
274            .set(Alias::new("a", "http://a:9000", "a", "a"))
275            .unwrap();
276        manager
277            .set(Alias::new("b", "http://b:9000", "b", "b"))
278            .unwrap();
279
280        let aliases = manager.list().unwrap();
281        assert_eq!(aliases.len(), 2);
282    }
283
284    #[test]
285    fn test_alias_manager_remove() {
286        let (manager, _temp_dir) = temp_alias_manager();
287
288        manager
289            .set(Alias::new("test", "http://localhost:9000", "a", "b"))
290            .unwrap();
291        assert!(manager.exists("test").unwrap());
292
293        manager.remove("test").unwrap();
294        assert!(!manager.exists("test").unwrap());
295    }
296
297    #[test]
298    fn test_alias_manager_remove_not_found() {
299        let (manager, _temp_dir) = temp_alias_manager();
300
301        let result = manager.remove("nonexistent");
302        assert!(result.is_err());
303        assert!(matches!(result.unwrap_err(), Error::AliasNotFound(_)));
304    }
305
306    #[test]
307    fn test_alias_manager_get_not_found() {
308        let (manager, _temp_dir) = temp_alias_manager();
309
310        let result = manager.get("nonexistent");
311        assert!(result.is_err());
312        assert!(matches!(result.unwrap_err(), Error::AliasNotFound(_)));
313    }
314
315    #[test]
316    fn test_alias_update_existing() {
317        let (manager, _temp_dir) = temp_alias_manager();
318
319        manager
320            .set(Alias::new("test", "http://old:9000", "a", "b"))
321            .unwrap();
322        manager
323            .set(Alias::new("test", "http://new:9000", "c", "d"))
324            .unwrap();
325
326        let aliases = manager.list().unwrap();
327        assert_eq!(aliases.len(), 1);
328        assert_eq!(aliases[0].endpoint, "http://new:9000");
329    }
330}