1use serde::{Deserialize, Serialize};
7use std::fmt;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ReplicationConfiguration {
14 #[serde(default)]
16 pub role: String,
17
18 pub rules: Vec<ReplicationRule>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24#[serde(rename_all = "camelCase")]
25pub struct ReplicationRule {
26 pub id: String,
28
29 pub priority: i32,
31
32 pub status: ReplicationRuleStatus,
34
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub prefix: Option<String>,
38
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub tags: Option<std::collections::HashMap<String, String>>,
42
43 pub destination: ReplicationDestination,
45
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub delete_marker_replication: Option<bool>,
49
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub existing_object_replication: Option<bool>,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub delete_replication: Option<bool>,
57}
58
59#[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#[derive(Debug, Clone, Serialize, Deserialize)]
89#[serde(rename_all = "camelCase")]
90pub struct ReplicationDestination {
91 pub bucket_arn: String,
93
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub storage_class: Option<String>,
97}
98
99#[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(
120 rename = "skipTlsVerify",
121 default,
122 skip_serializing_if = "Option::is_none"
123 )]
124 pub skip_tls_verify: Option<bool>,
125
126 #[serde(rename = "caCertPem", default, skip_serializing_if = "Option::is_none")]
127 pub ca_cert_pem: Option<String>,
128
129 #[serde(default)]
130 pub path: String,
131
132 #[serde(default)]
133 pub api: String,
134
135 #[serde(default)]
136 pub arn: String,
137
138 #[serde(rename = "type", default)]
139 pub target_type: String,
140
141 #[serde(default)]
142 pub region: String,
143
144 #[serde(alias = "bandwidth", default)]
145 pub bandwidth_limit: i64,
146
147 #[serde(rename = "replicationSync", default)]
148 pub replication_sync: bool,
149
150 #[serde(default)]
151 pub storage_class: String,
152
153 #[serde(rename = "healthCheckDuration", default)]
154 pub health_check_duration: u64,
155
156 #[serde(rename = "disableProxy", default)]
157 pub disable_proxy: bool,
158
159 #[serde(rename = "isOnline", default)]
160 pub online: bool,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize, Default)]
165pub struct BucketTargetCredentials {
166 #[serde(rename = "accessKey")]
167 pub access_key: String,
168 #[serde(rename = "secretKey")]
169 pub secret_key: String,
170}
171
172impl fmt::Display for ReplicationRule {
173 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174 write!(
175 f,
176 "{} (priority={}, {})",
177 self.id, self.priority, self.status
178 )
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn test_replication_rule_status_display() {
188 assert_eq!(ReplicationRuleStatus::Enabled.to_string(), "Enabled");
189 assert_eq!(ReplicationRuleStatus::Disabled.to_string(), "Disabled");
190 }
191
192 #[test]
193 fn test_replication_rule_status_from_str() {
194 assert_eq!(
195 "enabled".parse::<ReplicationRuleStatus>().unwrap(),
196 ReplicationRuleStatus::Enabled
197 );
198 assert!("invalid".parse::<ReplicationRuleStatus>().is_err());
199 }
200
201 #[test]
202 fn test_replication_configuration_serialization() {
203 let config = ReplicationConfiguration {
204 role: "arn:aws:iam::123456789:role/replication".to_string(),
205 rules: vec![ReplicationRule {
206 id: "rule-1".to_string(),
207 priority: 1,
208 status: ReplicationRuleStatus::Enabled,
209 prefix: Some("data/".to_string()),
210 tags: None,
211 destination: ReplicationDestination {
212 bucket_arn: "arn:aws:s3:::dest-bucket".to_string(),
213 storage_class: None,
214 },
215 delete_marker_replication: Some(true),
216 existing_object_replication: Some(true),
217 delete_replication: None,
218 }],
219 };
220
221 let json = serde_json::to_string_pretty(&config).unwrap();
222 let decoded: ReplicationConfiguration = serde_json::from_str(&json).unwrap();
223 assert_eq!(decoded.rules.len(), 1);
224 assert_eq!(decoded.rules[0].id, "rule-1");
225 assert_eq!(decoded.rules[0].priority, 1);
226 }
227
228 #[test]
229 fn test_bucket_target_serialization() {
230 let target = BucketTarget {
231 source_bucket: "my-bucket".to_string(),
232 endpoint: "http://remote:9000".to_string(),
233 credentials: Some(BucketTargetCredentials {
234 access_key: "admin".to_string(),
235 secret_key: "secret".to_string(),
236 }),
237 target_bucket: "dest-bucket".to_string(),
238 secure: false,
239 skip_tls_verify: Some(true),
240 target_type: "replication".to_string(),
241 region: "us-east-1".to_string(),
242 replication_sync: true,
243 ..Default::default()
244 };
245
246 let json = serde_json::to_string(&target).unwrap();
247 assert!(json.contains("sourcebucket"));
248 assert!(json.contains("targetbucket"));
249 assert!(json.contains("replicationSync"));
250 assert!(json.contains("skipTlsVerify"));
251
252 let decoded: BucketTarget = serde_json::from_str(&json).unwrap();
253 assert_eq!(decoded.source_bucket, "my-bucket");
254 assert_eq!(decoded.target_bucket, "dest-bucket");
255 assert!(decoded.replication_sync);
256 assert_eq!(decoded.skip_tls_verify, Some(true));
257 }
258
259 #[test]
260 fn test_bucket_target_deserialization_from_backend() {
261 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}"#;
262 let target: BucketTarget = serde_json::from_str(json).unwrap();
263 assert_eq!(target.source_bucket, "src");
264 assert_eq!(target.target_bucket, "dst");
265 assert!(target.online);
266 assert_eq!(target.target_type, "replication");
267 }
268
269 #[test]
270 fn test_bucket_target_serialization_includes_ca_cert_pem_content() {
271 let pem = "-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n";
272 let target = BucketTarget {
273 source_bucket: "my-bucket".to_string(),
274 endpoint: "remote:9000".to_string(),
275 target_bucket: "dest-bucket".to_string(),
276 secure: true,
277 skip_tls_verify: Some(false),
278 ca_cert_pem: Some(pem.to_string()),
279 target_type: "replication".to_string(),
280 ..Default::default()
281 };
282
283 let json = serde_json::to_string(&target).unwrap();
284 assert!(json.contains("\"skipTlsVerify\":false"));
285 assert!(json.contains("caCertPem"));
286 assert!(!json.contains("ca.pem"));
287 }
288}