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, PartialEq, Eq, Serialize, Deserialize)]
292pub struct ManualTransitionRunRequest {
293 pub bucket: String,
294 pub prefix: String,
295 pub tier: Option<String>,
296 pub dry_run: bool,
297 pub max_objects: u64,
298 pub max_duration_seconds: Option<u64>,
299}
300
301#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
303pub struct ManualTransitionRunResponse {
304 pub state: String,
305 pub mode: String,
306 pub job_id: Option<String>,
307 pub status_endpoint: Option<String>,
308 pub report: ManualTransitionRunReport,
309}
310
311#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
313pub struct ManualTransitionRunReport {
314 pub bucket: String,
315 pub prefix: String,
316 pub tier: Option<String>,
317 pub dry_run: bool,
318 pub lifecycle_config_found: bool,
319 pub scanned: u64,
320 pub eligible: u64,
321 pub enqueued: u64,
322 pub dry_run_eligible: u64,
323 pub skipped_not_transition: u64,
324 pub skipped_tier: u64,
325 pub skipped_delete_marker: u64,
326 pub skipped_directory: u64,
327 pub skipped_replication: u64,
328 #[serde(default)]
329 pub skipped_already_transitioned: u64,
330 pub skipped_already_in_flight: u64,
331 pub skipped_queue_full: u64,
332 pub skipped_queue_closed: u64,
333 pub skipped_queue_timeout: u64,
334 pub truncated_by_limit: bool,
335 #[serde(default)]
336 pub truncated_by_duration: bool,
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize, Default)]
343#[serde(default)]
344pub struct TierS3 {
345 pub name: String,
346 pub endpoint: String,
347 #[serde(rename = "accessKey")]
348 pub access_key: String,
349 #[serde(rename = "secretKey")]
350 pub secret_key: String,
351 pub bucket: String,
352 pub prefix: String,
353 pub region: String,
354 #[serde(rename = "storageClass")]
355 pub storage_class: String,
356}
357
358#[derive(Debug, Clone, Serialize, Deserialize, Default)]
359#[serde(default)]
360pub struct TierRustFS {
361 pub name: String,
362 pub endpoint: String,
363 #[serde(rename = "accessKey")]
364 pub access_key: String,
365 #[serde(rename = "secretKey")]
366 pub secret_key: String,
367 pub bucket: String,
368 pub prefix: String,
369 pub region: String,
370 #[serde(rename = "storageClass")]
371 pub storage_class: String,
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize, Default)]
375#[serde(default)]
376pub struct TierMinIO {
377 pub name: String,
378 pub endpoint: String,
379 #[serde(rename = "accessKey")]
380 pub access_key: String,
381 #[serde(rename = "secretKey")]
382 pub secret_key: String,
383 pub bucket: String,
384 pub prefix: String,
385 pub region: String,
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize, Default)]
389#[serde(default)]
390pub struct TierAliyun {
391 pub name: String,
392 pub endpoint: String,
393 #[serde(rename = "accessKey")]
394 pub access_key: String,
395 #[serde(rename = "secretKey")]
396 pub secret_key: String,
397 pub bucket: String,
398 pub prefix: String,
399 pub region: String,
400}
401
402#[derive(Debug, Clone, Serialize, Deserialize, Default)]
403#[serde(default)]
404pub struct TierTencent {
405 pub name: String,
406 pub endpoint: String,
407 #[serde(rename = "accessKey")]
408 pub access_key: String,
409 #[serde(rename = "secretKey")]
410 pub secret_key: String,
411 pub bucket: String,
412 pub prefix: String,
413 pub region: String,
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize, Default)]
417#[serde(default)]
418pub struct TierHuaweicloud {
419 pub name: String,
420 pub endpoint: String,
421 #[serde(rename = "accessKey")]
422 pub access_key: String,
423 #[serde(rename = "secretKey")]
424 pub secret_key: String,
425 pub bucket: String,
426 pub prefix: String,
427 pub region: String,
428}
429
430#[derive(Debug, Clone, Serialize, Deserialize, Default)]
431#[serde(default)]
432pub struct TierAzure {
433 pub name: String,
434 pub endpoint: String,
435 #[serde(rename = "accessKey")]
436 pub access_key: String,
437 #[serde(rename = "secretKey")]
438 pub secret_key: String,
439 pub bucket: String,
440 pub prefix: String,
441 pub region: String,
442 #[serde(rename = "storageClass")]
443 pub storage_class: String,
444}
445
446#[derive(Debug, Clone, Serialize, Deserialize, Default)]
447#[serde(default)]
448pub struct TierGCS {
449 pub name: String,
450 pub endpoint: String,
451 #[serde(rename = "creds")]
452 pub creds: String,
453 pub bucket: String,
454 pub prefix: String,
455 pub region: String,
456 #[serde(rename = "storageClass")]
457 pub storage_class: String,
458}
459
460#[derive(Debug, Clone, Serialize, Deserialize, Default)]
461#[serde(default)]
462pub struct TierR2 {
463 pub name: String,
464 pub endpoint: String,
465 #[serde(rename = "accessKey")]
466 pub access_key: String,
467 #[serde(rename = "secretKey")]
468 pub secret_key: String,
469 pub bucket: String,
470 pub prefix: String,
471 pub region: String,
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477
478 #[test]
479 fn test_tier_type_display() {
480 assert_eq!(TierType::S3.to_string(), "S3");
481 assert_eq!(TierType::RustFS.to_string(), "RustFS");
482 assert_eq!(TierType::MinIO.to_string(), "MinIO");
483 assert_eq!(TierType::Azure.to_string(), "Azure");
484 assert_eq!(TierType::GCS.to_string(), "GCS");
485 assert_eq!(TierType::R2.to_string(), "R2");
486 }
487
488 #[test]
489 fn test_tier_type_from_str() {
490 assert_eq!("s3".parse::<TierType>().unwrap(), TierType::S3);
491 assert_eq!("rustfs".parse::<TierType>().unwrap(), TierType::RustFS);
492 assert_eq!("MINIO".parse::<TierType>().unwrap(), TierType::MinIO);
493 assert_eq!("Azure".parse::<TierType>().unwrap(), TierType::Azure);
494 assert!("invalid".parse::<TierType>().is_err());
495 }
496
497 #[test]
498 fn test_tier_config_serialization_s3() {
499 let config = TierConfig {
500 tier_type: TierType::S3,
501 name: "WARM".to_string(),
502 s3: Some(TierS3 {
503 name: "WARM".to_string(),
504 endpoint: "https://s3.amazonaws.com".to_string(),
505 access_key: "AKID".to_string(),
506 secret_key: "REDACTED".to_string(),
507 bucket: "warm-bucket".to_string(),
508 prefix: "tier/".to_string(),
509 region: "us-east-1".to_string(),
510 storage_class: "STANDARD_IA".to_string(),
511 }),
512 ..Default::default()
513 };
514
515 let json = serde_json::to_string(&config).unwrap();
516 assert!(json.contains(r#""type":"s3""#));
517 assert!(json.contains("warm-bucket"));
518
519 let decoded: TierConfig = serde_json::from_str(&json).unwrap();
520 assert_eq!(decoded.tier_type, TierType::S3);
521 assert_eq!(decoded.tier_name(), "WARM");
522 assert_eq!(decoded.bucket(), "warm-bucket");
523 }
524
525 #[test]
526 fn test_tier_config_deserialization_from_backend() {
527 let json = r#"{"type":"rustfs","rustfs":{"name":"ARCHIVE","endpoint":"http://remote:9000","accessKey":"admin","secretKey":"REDACTED","bucket":"archive","prefix":"","region":""}}"#;
529 let config: TierConfig = serde_json::from_str(json).unwrap();
530 assert_eq!(config.tier_type, TierType::RustFS);
531 assert_eq!(config.tier_name(), "ARCHIVE");
532 assert_eq!(config.endpoint(), "http://remote:9000");
533 assert_eq!(config.bucket(), "archive");
534 }
535
536 #[test]
537 fn test_tier_creds_serialization() {
538 let creds = TierCreds {
539 access_key: "newkey".to_string(),
540 secret_key: "newsecret".to_string(),
541 };
542
543 let json = serde_json::to_string(&creds).unwrap();
544 assert!(json.contains("accessKey"));
545 assert!(json.contains("secretKey"));
546
547 let decoded: TierCreds = serde_json::from_str(&json).unwrap();
548 assert_eq!(decoded.access_key, "newkey");
549 }
550}