Skip to main content

rc_core/
replication.rs

1//! Bucket replication configuration types
2//!
3//! Domain types for S3 bucket replication configuration and
4//! RustFS admin API remote target management.
5
6use serde::{Deserialize, Serialize};
7use std::fmt;
8
9// ==================== S3 Replication Config Types ====================
10
11/// Full replication configuration for a bucket (S3 API)
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ReplicationConfiguration {
14    /// Role ARN or empty for per-rule destination ARNs
15    #[serde(default)]
16    pub role: String,
17
18    /// Replication rules
19    pub rules: Vec<ReplicationRule>,
20}
21
22/// A single replication rule
23#[derive(Debug, Clone, Serialize, Deserialize)]
24#[serde(rename_all = "camelCase")]
25pub struct ReplicationRule {
26    /// Rule identifier
27    pub id: String,
28
29    /// Rule priority (higher = more important)
30    pub priority: i32,
31
32    /// Whether the rule is enabled or disabled
33    pub status: ReplicationRuleStatus,
34
35    /// Key prefix filter
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub prefix: Option<String>,
38
39    /// Tag-based filter
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub tags: Option<std::collections::HashMap<String, String>>,
42
43    /// Destination bucket ARN and optional storage class
44    pub destination: ReplicationDestination,
45
46    /// Whether to replicate delete markers
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub delete_marker_replication: Option<bool>,
49
50    /// Whether to replicate existing objects
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub existing_object_replication: Option<bool>,
53
54    /// Whether to replicate version deletes
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub delete_replication: Option<bool>,
57}
58
59/// Rule status
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
61pub enum ReplicationRuleStatus {
62    Enabled,
63    Disabled,
64}
65
66impl fmt::Display for ReplicationRuleStatus {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        match self {
69            ReplicationRuleStatus::Enabled => write!(f, "Enabled"),
70            ReplicationRuleStatus::Disabled => write!(f, "Disabled"),
71        }
72    }
73}
74
75impl std::str::FromStr for ReplicationRuleStatus {
76    type Err = String;
77
78    fn from_str(s: &str) -> Result<Self, Self::Err> {
79        match s.to_lowercase().as_str() {
80            "enabled" => Ok(ReplicationRuleStatus::Enabled),
81            "disabled" => Ok(ReplicationRuleStatus::Disabled),
82            _ => Err(format!("Invalid replication rule status: {s}")),
83        }
84    }
85}
86
87/// Replication destination
88#[derive(Debug, Clone, Serialize, Deserialize)]
89#[serde(rename_all = "camelCase")]
90pub struct ReplicationDestination {
91    /// Destination bucket ARN
92    pub bucket_arn: String,
93
94    /// Optional storage class override at destination
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub storage_class: Option<String>,
97}
98
99// ==================== Admin API Remote Target Types ====================
100
101/// Remote bucket target for replication (matches RustFS admin API JSON format)
102#[derive(Debug, Clone, Serialize, Deserialize, Default)]
103pub struct BucketTarget {
104    #[serde(rename = "sourcebucket", default)]
105    pub source_bucket: String,
106
107    #[serde(default)]
108    pub endpoint: String,
109
110    #[serde(default)]
111    pub credentials: Option<BucketTargetCredentials>,
112
113    #[serde(rename = "targetbucket", default)]
114    pub target_bucket: String,
115
116    #[serde(default)]
117    pub secure: bool,
118
119    #[serde(default)]
120    pub path: String,
121
122    #[serde(default)]
123    pub api: String,
124
125    #[serde(default)]
126    pub arn: String,
127
128    #[serde(rename = "type", default)]
129    pub target_type: String,
130
131    #[serde(default)]
132    pub region: String,
133
134    #[serde(alias = "bandwidth", default)]
135    pub bandwidth_limit: i64,
136
137    #[serde(rename = "replicationSync", default)]
138    pub replication_sync: bool,
139
140    #[serde(default)]
141    pub storage_class: String,
142
143    #[serde(rename = "healthCheckDuration", default)]
144    pub health_check_duration: u64,
145
146    #[serde(rename = "disableProxy", default)]
147    pub disable_proxy: bool,
148
149    #[serde(rename = "isOnline", default)]
150    pub online: bool,
151}
152
153/// Credentials for a remote bucket target
154#[derive(Debug, Clone, Serialize, Deserialize, Default)]
155pub struct BucketTargetCredentials {
156    #[serde(rename = "accessKey")]
157    pub access_key: String,
158    #[serde(rename = "secretKey")]
159    pub secret_key: String,
160}
161
162impl fmt::Display for ReplicationRule {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        write!(
165            f,
166            "{} (priority={}, {})",
167            self.id, self.priority, self.status
168        )
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn test_replication_rule_status_display() {
178        assert_eq!(ReplicationRuleStatus::Enabled.to_string(), "Enabled");
179        assert_eq!(ReplicationRuleStatus::Disabled.to_string(), "Disabled");
180    }
181
182    #[test]
183    fn test_replication_rule_status_from_str() {
184        assert_eq!(
185            "enabled".parse::<ReplicationRuleStatus>().unwrap(),
186            ReplicationRuleStatus::Enabled
187        );
188        assert!("invalid".parse::<ReplicationRuleStatus>().is_err());
189    }
190
191    #[test]
192    fn test_replication_configuration_serialization() {
193        let config = ReplicationConfiguration {
194            role: "arn:aws:iam::123456789:role/replication".to_string(),
195            rules: vec![ReplicationRule {
196                id: "rule-1".to_string(),
197                priority: 1,
198                status: ReplicationRuleStatus::Enabled,
199                prefix: Some("data/".to_string()),
200                tags: None,
201                destination: ReplicationDestination {
202                    bucket_arn: "arn:aws:s3:::dest-bucket".to_string(),
203                    storage_class: None,
204                },
205                delete_marker_replication: Some(true),
206                existing_object_replication: Some(true),
207                delete_replication: None,
208            }],
209        };
210
211        let json = serde_json::to_string_pretty(&config).unwrap();
212        let decoded: ReplicationConfiguration = serde_json::from_str(&json).unwrap();
213        assert_eq!(decoded.rules.len(), 1);
214        assert_eq!(decoded.rules[0].id, "rule-1");
215        assert_eq!(decoded.rules[0].priority, 1);
216    }
217
218    #[test]
219    fn test_bucket_target_serialization() {
220        let target = BucketTarget {
221            source_bucket: "my-bucket".to_string(),
222            endpoint: "http://remote:9000".to_string(),
223            credentials: Some(BucketTargetCredentials {
224                access_key: "admin".to_string(),
225                secret_key: "secret".to_string(),
226            }),
227            target_bucket: "dest-bucket".to_string(),
228            secure: false,
229            target_type: "replication".to_string(),
230            region: "us-east-1".to_string(),
231            replication_sync: true,
232            ..Default::default()
233        };
234
235        let json = serde_json::to_string(&target).unwrap();
236        assert!(json.contains("sourcebucket"));
237        assert!(json.contains("targetbucket"));
238        assert!(json.contains("replicationSync"));
239
240        let decoded: BucketTarget = serde_json::from_str(&json).unwrap();
241        assert_eq!(decoded.source_bucket, "my-bucket");
242        assert_eq!(decoded.target_bucket, "dest-bucket");
243        assert!(decoded.replication_sync);
244    }
245
246    #[test]
247    fn test_bucket_target_deserialization_from_backend() {
248        let json = r#"{"sourcebucket":"src","endpoint":"http://host:9000","credentials":{"accessKey":"ak","secretKey":"sk"},"targetbucket":"dst","secure":false,"path":"","api":"","arn":"arn:rustfs:replication::id:dst","type":"replication","region":"","bandwidth":0,"replicationSync":false,"storage_class":"","healthCheckDuration":0,"disableProxy":false,"isOnline":true}"#;
249        let target: BucketTarget = serde_json::from_str(json).unwrap();
250        assert_eq!(target.source_bucket, "src");
251        assert_eq!(target.target_bucket, "dst");
252        assert!(target.online);
253        assert_eq!(target.target_type, "replication");
254    }
255}