1use serde::{Deserialize, Serialize};
8use std::fmt;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub enum TierType {
13 #[serde(rename = "s3")]
14 S3,
15 #[serde(rename = "rustfs")]
16 RustFS,
17 #[serde(rename = "minio")]
18 MinIO,
19 #[serde(rename = "aliyun")]
20 Aliyun,
21 #[serde(rename = "tencent")]
22 Tencent,
23 #[serde(rename = "huaweicloud")]
24 Huaweicloud,
25 #[serde(rename = "azure")]
26 Azure,
27 #[serde(rename = "gcs")]
28 GCS,
29 #[serde(rename = "r2")]
30 R2,
31}
32
33impl fmt::Display for TierType {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 match self {
36 TierType::S3 => write!(f, "S3"),
37 TierType::RustFS => write!(f, "RustFS"),
38 TierType::MinIO => write!(f, "MinIO"),
39 TierType::Aliyun => write!(f, "Aliyun"),
40 TierType::Tencent => write!(f, "Tencent"),
41 TierType::Huaweicloud => write!(f, "Huaweicloud"),
42 TierType::Azure => write!(f, "Azure"),
43 TierType::GCS => write!(f, "GCS"),
44 TierType::R2 => write!(f, "R2"),
45 }
46 }
47}
48
49impl std::str::FromStr for TierType {
50 type Err = String;
51
52 fn from_str(s: &str) -> Result<Self, Self::Err> {
53 match s.to_lowercase().as_str() {
54 "s3" => Ok(TierType::S3),
55 "rustfs" => Ok(TierType::RustFS),
56 "minio" => Ok(TierType::MinIO),
57 "aliyun" => Ok(TierType::Aliyun),
58 "tencent" => Ok(TierType::Tencent),
59 "huaweicloud" => Ok(TierType::Huaweicloud),
60 "azure" => Ok(TierType::Azure),
61 "gcs" => Ok(TierType::GCS),
62 "r2" => Ok(TierType::R2),
63 _ => Err(format!(
64 "Invalid tier type: {s}. Valid types: s3, rustfs, minio, aliyun, tencent, huaweicloud, azure, gcs, r2"
65 )),
66 }
67 }
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
76#[serde(default)]
77pub struct TierConfig {
78 #[serde(rename = "type")]
79 pub tier_type: TierType,
80
81 #[serde(skip)]
84 pub name: String,
85
86 #[serde(rename = "s3", skip_serializing_if = "Option::is_none")]
87 pub s3: Option<TierS3>,
88 #[serde(rename = "rustfs", skip_serializing_if = "Option::is_none")]
89 pub rustfs: Option<TierRustFS>,
90 #[serde(rename = "minio", skip_serializing_if = "Option::is_none")]
91 pub minio: Option<TierMinIO>,
92 #[serde(rename = "aliyun", skip_serializing_if = "Option::is_none")]
93 pub aliyun: Option<TierAliyun>,
94 #[serde(rename = "tencent", skip_serializing_if = "Option::is_none")]
95 pub tencent: Option<TierTencent>,
96 #[serde(rename = "huaweicloud", skip_serializing_if = "Option::is_none")]
97 pub huaweicloud: Option<TierHuaweicloud>,
98 #[serde(rename = "azure", skip_serializing_if = "Option::is_none")]
99 pub azure: Option<TierAzure>,
100 #[serde(rename = "gcs", skip_serializing_if = "Option::is_none")]
101 pub gcs: Option<TierGCS>,
102 #[serde(rename = "r2", skip_serializing_if = "Option::is_none")]
103 pub r2: Option<TierR2>,
104}
105
106impl Default for TierConfig {
107 fn default() -> Self {
108 Self {
109 tier_type: TierType::S3,
110 name: String::new(),
111 s3: None,
112 rustfs: None,
113 minio: None,
114 aliyun: None,
115 tencent: None,
116 huaweicloud: None,
117 azure: None,
118 gcs: None,
119 r2: None,
120 }
121 }
122}
123
124impl TierConfig {
125 pub fn tier_name(&self) -> &str {
127 if !self.name.is_empty() {
128 return &self.name;
129 }
130 match self.tier_type {
131 TierType::S3 => self.s3.as_ref().map(|c| c.name.as_str()).unwrap_or(""),
132 TierType::RustFS => self.rustfs.as_ref().map(|c| c.name.as_str()).unwrap_or(""),
133 TierType::MinIO => self.minio.as_ref().map(|c| c.name.as_str()).unwrap_or(""),
134 TierType::Aliyun => self.aliyun.as_ref().map(|c| c.name.as_str()).unwrap_or(""),
135 TierType::Tencent => self.tencent.as_ref().map(|c| c.name.as_str()).unwrap_or(""),
136 TierType::Huaweicloud => self
137 .huaweicloud
138 .as_ref()
139 .map(|c| c.name.as_str())
140 .unwrap_or(""),
141 TierType::Azure => self.azure.as_ref().map(|c| c.name.as_str()).unwrap_or(""),
142 TierType::GCS => self.gcs.as_ref().map(|c| c.name.as_str()).unwrap_or(""),
143 TierType::R2 => self.r2.as_ref().map(|c| c.name.as_str()).unwrap_or(""),
144 }
145 }
146
147 pub fn endpoint(&self) -> &str {
149 match self.tier_type {
150 TierType::S3 => self.s3.as_ref().map(|c| c.endpoint.as_str()).unwrap_or(""),
151 TierType::RustFS => self
152 .rustfs
153 .as_ref()
154 .map(|c| c.endpoint.as_str())
155 .unwrap_or(""),
156 TierType::MinIO => self
157 .minio
158 .as_ref()
159 .map(|c| c.endpoint.as_str())
160 .unwrap_or(""),
161 TierType::Aliyun => self
162 .aliyun
163 .as_ref()
164 .map(|c| c.endpoint.as_str())
165 .unwrap_or(""),
166 TierType::Tencent => self
167 .tencent
168 .as_ref()
169 .map(|c| c.endpoint.as_str())
170 .unwrap_or(""),
171 TierType::Huaweicloud => self
172 .huaweicloud
173 .as_ref()
174 .map(|c| c.endpoint.as_str())
175 .unwrap_or(""),
176 TierType::Azure => self
177 .azure
178 .as_ref()
179 .map(|c| c.endpoint.as_str())
180 .unwrap_or(""),
181 TierType::GCS => self.gcs.as_ref().map(|c| c.endpoint.as_str()).unwrap_or(""),
182 TierType::R2 => self.r2.as_ref().map(|c| c.endpoint.as_str()).unwrap_or(""),
183 }
184 }
185
186 pub fn bucket(&self) -> &str {
188 match self.tier_type {
189 TierType::S3 => self.s3.as_ref().map(|c| c.bucket.as_str()).unwrap_or(""),
190 TierType::RustFS => self
191 .rustfs
192 .as_ref()
193 .map(|c| c.bucket.as_str())
194 .unwrap_or(""),
195 TierType::MinIO => self.minio.as_ref().map(|c| c.bucket.as_str()).unwrap_or(""),
196 TierType::Aliyun => self
197 .aliyun
198 .as_ref()
199 .map(|c| c.bucket.as_str())
200 .unwrap_or(""),
201 TierType::Tencent => self
202 .tencent
203 .as_ref()
204 .map(|c| c.bucket.as_str())
205 .unwrap_or(""),
206 TierType::Huaweicloud => self
207 .huaweicloud
208 .as_ref()
209 .map(|c| c.bucket.as_str())
210 .unwrap_or(""),
211 TierType::Azure => self.azure.as_ref().map(|c| c.bucket.as_str()).unwrap_or(""),
212 TierType::GCS => self.gcs.as_ref().map(|c| c.bucket.as_str()).unwrap_or(""),
213 TierType::R2 => self.r2.as_ref().map(|c| c.bucket.as_str()).unwrap_or(""),
214 }
215 }
216
217 pub fn prefix(&self) -> &str {
219 match self.tier_type {
220 TierType::S3 => self.s3.as_ref().map(|c| c.prefix.as_str()).unwrap_or(""),
221 TierType::RustFS => self
222 .rustfs
223 .as_ref()
224 .map(|c| c.prefix.as_str())
225 .unwrap_or(""),
226 TierType::MinIO => self.minio.as_ref().map(|c| c.prefix.as_str()).unwrap_or(""),
227 TierType::Aliyun => self
228 .aliyun
229 .as_ref()
230 .map(|c| c.prefix.as_str())
231 .unwrap_or(""),
232 TierType::Tencent => self
233 .tencent
234 .as_ref()
235 .map(|c| c.prefix.as_str())
236 .unwrap_or(""),
237 TierType::Huaweicloud => self
238 .huaweicloud
239 .as_ref()
240 .map(|c| c.prefix.as_str())
241 .unwrap_or(""),
242 TierType::Azure => self.azure.as_ref().map(|c| c.prefix.as_str()).unwrap_or(""),
243 TierType::GCS => self.gcs.as_ref().map(|c| c.prefix.as_str()).unwrap_or(""),
244 TierType::R2 => self.r2.as_ref().map(|c| c.prefix.as_str()).unwrap_or(""),
245 }
246 }
247
248 pub fn region(&self) -> &str {
250 match self.tier_type {
251 TierType::S3 => self.s3.as_ref().map(|c| c.region.as_str()).unwrap_or(""),
252 TierType::RustFS => self
253 .rustfs
254 .as_ref()
255 .map(|c| c.region.as_str())
256 .unwrap_or(""),
257 TierType::MinIO => self.minio.as_ref().map(|c| c.region.as_str()).unwrap_or(""),
258 TierType::Aliyun => self
259 .aliyun
260 .as_ref()
261 .map(|c| c.region.as_str())
262 .unwrap_or(""),
263 TierType::Tencent => self
264 .tencent
265 .as_ref()
266 .map(|c| c.region.as_str())
267 .unwrap_or(""),
268 TierType::Huaweicloud => self
269 .huaweicloud
270 .as_ref()
271 .map(|c| c.region.as_str())
272 .unwrap_or(""),
273 TierType::Azure => self.azure.as_ref().map(|c| c.region.as_str()).unwrap_or(""),
274 TierType::GCS => self.gcs.as_ref().map(|c| c.region.as_str()).unwrap_or(""),
275 TierType::R2 => self.r2.as_ref().map(|c| c.region.as_str()).unwrap_or(""),
276 }
277 }
278}
279
280#[derive(Debug, Clone, Default, Serialize, Deserialize)]
282#[serde(default)]
283pub struct TierCreds {
284 #[serde(rename = "accessKey")]
285 pub access_key: String,
286 #[serde(rename = "secretKey")]
287 pub secret_key: String,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize, Default)]
294#[serde(default)]
295pub struct TierS3 {
296 pub name: String,
297 pub endpoint: String,
298 #[serde(rename = "accessKey")]
299 pub access_key: String,
300 #[serde(rename = "secretKey")]
301 pub secret_key: String,
302 pub bucket: String,
303 pub prefix: String,
304 pub region: String,
305 #[serde(rename = "storageClass")]
306 pub storage_class: String,
307}
308
309#[derive(Debug, Clone, Serialize, Deserialize, Default)]
310#[serde(default)]
311pub struct TierRustFS {
312 pub name: String,
313 pub endpoint: String,
314 #[serde(rename = "accessKey")]
315 pub access_key: String,
316 #[serde(rename = "secretKey")]
317 pub secret_key: String,
318 pub bucket: String,
319 pub prefix: String,
320 pub region: String,
321 #[serde(rename = "storageClass")]
322 pub storage_class: String,
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize, Default)]
326#[serde(default)]
327pub struct TierMinIO {
328 pub name: String,
329 pub endpoint: String,
330 #[serde(rename = "accessKey")]
331 pub access_key: String,
332 #[serde(rename = "secretKey")]
333 pub secret_key: String,
334 pub bucket: String,
335 pub prefix: String,
336 pub region: String,
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize, Default)]
340#[serde(default)]
341pub struct TierAliyun {
342 pub name: String,
343 pub endpoint: String,
344 #[serde(rename = "accessKey")]
345 pub access_key: String,
346 #[serde(rename = "secretKey")]
347 pub secret_key: String,
348 pub bucket: String,
349 pub prefix: String,
350 pub region: String,
351}
352
353#[derive(Debug, Clone, Serialize, Deserialize, Default)]
354#[serde(default)]
355pub struct TierTencent {
356 pub name: String,
357 pub endpoint: String,
358 #[serde(rename = "accessKey")]
359 pub access_key: String,
360 #[serde(rename = "secretKey")]
361 pub secret_key: String,
362 pub bucket: String,
363 pub prefix: String,
364 pub region: String,
365}
366
367#[derive(Debug, Clone, Serialize, Deserialize, Default)]
368#[serde(default)]
369pub struct TierHuaweicloud {
370 pub name: String,
371 pub endpoint: String,
372 #[serde(rename = "accessKey")]
373 pub access_key: String,
374 #[serde(rename = "secretKey")]
375 pub secret_key: String,
376 pub bucket: String,
377 pub prefix: String,
378 pub region: String,
379}
380
381#[derive(Debug, Clone, Serialize, Deserialize, Default)]
382#[serde(default)]
383pub struct TierAzure {
384 pub name: String,
385 pub endpoint: String,
386 #[serde(rename = "accessKey")]
387 pub access_key: String,
388 #[serde(rename = "secretKey")]
389 pub secret_key: String,
390 pub bucket: String,
391 pub prefix: String,
392 pub region: String,
393 #[serde(rename = "storageClass")]
394 pub storage_class: String,
395}
396
397#[derive(Debug, Clone, Serialize, Deserialize, Default)]
398#[serde(default)]
399pub struct TierGCS {
400 pub name: String,
401 pub endpoint: String,
402 #[serde(rename = "creds")]
403 pub creds: String,
404 pub bucket: String,
405 pub prefix: String,
406 pub region: String,
407 #[serde(rename = "storageClass")]
408 pub storage_class: String,
409}
410
411#[derive(Debug, Clone, Serialize, Deserialize, Default)]
412#[serde(default)]
413pub struct TierR2 {
414 pub name: String,
415 pub endpoint: String,
416 #[serde(rename = "accessKey")]
417 pub access_key: String,
418 #[serde(rename = "secretKey")]
419 pub secret_key: String,
420 pub bucket: String,
421 pub prefix: String,
422 pub region: String,
423}
424
425#[cfg(test)]
426mod tests {
427 use super::*;
428
429 #[test]
430 fn test_tier_type_display() {
431 assert_eq!(TierType::S3.to_string(), "S3");
432 assert_eq!(TierType::RustFS.to_string(), "RustFS");
433 assert_eq!(TierType::MinIO.to_string(), "MinIO");
434 assert_eq!(TierType::Azure.to_string(), "Azure");
435 assert_eq!(TierType::GCS.to_string(), "GCS");
436 assert_eq!(TierType::R2.to_string(), "R2");
437 }
438
439 #[test]
440 fn test_tier_type_from_str() {
441 assert_eq!("s3".parse::<TierType>().unwrap(), TierType::S3);
442 assert_eq!("rustfs".parse::<TierType>().unwrap(), TierType::RustFS);
443 assert_eq!("MINIO".parse::<TierType>().unwrap(), TierType::MinIO);
444 assert_eq!("Azure".parse::<TierType>().unwrap(), TierType::Azure);
445 assert!("invalid".parse::<TierType>().is_err());
446 }
447
448 #[test]
449 fn test_tier_config_serialization_s3() {
450 let config = TierConfig {
451 tier_type: TierType::S3,
452 name: "WARM".to_string(),
453 s3: Some(TierS3 {
454 name: "WARM".to_string(),
455 endpoint: "https://s3.amazonaws.com".to_string(),
456 access_key: "AKID".to_string(),
457 secret_key: "REDACTED".to_string(),
458 bucket: "warm-bucket".to_string(),
459 prefix: "tier/".to_string(),
460 region: "us-east-1".to_string(),
461 storage_class: "STANDARD_IA".to_string(),
462 }),
463 ..Default::default()
464 };
465
466 let json = serde_json::to_string(&config).unwrap();
467 assert!(json.contains(r#""type":"s3""#));
468 assert!(json.contains("warm-bucket"));
469
470 let decoded: TierConfig = serde_json::from_str(&json).unwrap();
471 assert_eq!(decoded.tier_type, TierType::S3);
472 assert_eq!(decoded.tier_name(), "WARM");
473 assert_eq!(decoded.bucket(), "warm-bucket");
474 }
475
476 #[test]
477 fn test_tier_config_deserialization_from_backend() {
478 let json = r#"{"type":"rustfs","rustfs":{"name":"ARCHIVE","endpoint":"http://remote:9000","accessKey":"admin","secretKey":"REDACTED","bucket":"archive","prefix":"","region":""}}"#;
480 let config: TierConfig = serde_json::from_str(json).unwrap();
481 assert_eq!(config.tier_type, TierType::RustFS);
482 assert_eq!(config.tier_name(), "ARCHIVE");
483 assert_eq!(config.endpoint(), "http://remote:9000");
484 assert_eq!(config.bucket(), "archive");
485 }
486
487 #[test]
488 fn test_tier_creds_serialization() {
489 let creds = TierCreds {
490 access_key: "newkey".to_string(),
491 secret_key: "newsecret".to_string(),
492 };
493
494 let json = serde_json::to_string(&creds).unwrap();
495 assert!(json.contains("accessKey"));
496 assert!(json.contains("secretKey"));
497
498 let decoded: TierCreds = serde_json::from_str(&json).unwrap();
499 assert_eq!(decoded.access_key, "newkey");
500 }
501}