Skip to main content

aliyun_oss/operations/
bucket_worm.rs

1//! WORM (Write Once Read Many) operations.
2
3use std::sync::Arc;
4
5use serde::{Deserialize, Serialize};
6
7use crate::client::{BucketOperations, OSSClientInner};
8use crate::error::{ErrorContext, OssError, OssErrorKind, Result};
9use crate::http::client::HttpRequest;
10use crate::types::bucket::BucketName;
11
12#[derive(Debug, Clone, Serialize)]
13#[serde(rename = "InitiateWormConfiguration")]
14struct InitiateWormConfiguration {
15    #[serde(rename = "RetentionPeriodInDays")]
16    retention_period_days: i32,
17}
18
19#[derive(Debug, Clone, Serialize)]
20#[serde(rename = "ExtendWormConfiguration")]
21struct ExtendWormConfiguration {
22    #[serde(rename = "RetentionPeriodInDays")]
23    retention_period_days: i32,
24}
25
26#[derive(Debug, Clone, Deserialize)]
27#[serde(rename = "WormConfiguration")]
28struct WormConfigurationResponse {
29    #[serde(rename = "WormId")]
30    worm_id: String,
31    #[serde(rename = "State")]
32    state: String,
33    #[serde(rename = "CreationDate")]
34    creation_date: String,
35    #[serde(rename = "RetentionPeriodInDays")]
36    retention_period_days: i32,
37}
38
39pub struct InitiateBucketWormBuilder {
40    client: Arc<OSSClientInner>,
41    bucket: BucketName,
42    retention_days: i32,
43}
44
45impl InitiateBucketWormBuilder {
46    pub(crate) fn new(
47        client: Arc<OSSClientInner>,
48        bucket: BucketName,
49        retention_days: i32,
50    ) -> Self {
51        Self {
52            client,
53            bucket,
54            retention_days,
55        }
56    }
57
58    pub async fn send(self) -> Result<InitiateBucketWormOutput> {
59        let endpoint = self.client.endpoint.clone();
60        let uri = format!("https://{}.{}?worm", self.bucket.as_str(), endpoint);
61        let query_params: Vec<(String, String)> = vec![("worm".into(), String::new())];
62
63        let config = InitiateWormConfiguration {
64            retention_period_days: self.retention_days,
65        };
66        let body_xml = crate::util::xml::to_xml(&config)?;
67
68        let request = HttpRequest::builder()
69            .method(http::Method::POST)
70            .uri(&uri)
71            .body(bytes::Bytes::from(body_xml))
72            .build();
73
74        let response = self
75            .client
76            .send_signed(request, Some(&self.bucket), query_params)
77            .await
78            .map_err(|e| OssError {
79                kind: OssErrorKind::TransportError,
80                context: Box::new(ErrorContext {
81                    operation: Some("InitiateBucketWorm".into()),
82                    bucket: Some(self.bucket.to_string()),
83                    endpoint: Some(endpoint),
84                    ..Default::default()
85                }),
86                source: Some(Box::new(e)),
87            })?;
88
89        if response.status().is_success() {
90            Ok(InitiateBucketWormOutput {
91                request_id: response
92                    .headers
93                    .get("x-oss-request-id")
94                    .and_then(|v| v.to_str().ok())
95                    .unwrap_or("")
96                    .to_string(),
97                worm_id: response
98                    .headers
99                    .get("x-oss-worm-id")
100                    .and_then(|v| v.to_str().ok())
101                    .unwrap_or("")
102                    .to_string(),
103            })
104        } else {
105            Err(OssError {
106                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
107                    status_code: response.status().as_u16(),
108                    code: String::new(),
109                    message: String::new(),
110                    request_id: String::new(),
111                    host_id: String::new(),
112                    resource: Some(self.bucket.to_string()),
113                    string_to_sign: None,
114                })),
115                context: Box::new(ErrorContext {
116                    operation: Some("InitiateBucketWorm".into()),
117                    bucket: Some(self.bucket.to_string()),
118                    ..Default::default()
119                }),
120                source: None,
121            })
122        }
123    }
124}
125
126#[derive(Debug, Clone)]
127pub struct InitiateBucketWormOutput {
128    pub request_id: String,
129    pub worm_id: String,
130}
131
132pub struct AbortBucketWormBuilder {
133    client: Arc<OSSClientInner>,
134    bucket: BucketName,
135}
136
137impl AbortBucketWormBuilder {
138    pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName) -> Self {
139        Self { client, bucket }
140    }
141
142    pub async fn send(self) -> Result<AbortBucketWormOutput> {
143        let endpoint = self.client.endpoint.clone();
144        let uri = format!("https://{}.{}?worm", self.bucket.as_str(), endpoint);
145        let query_params: Vec<(String, String)> = vec![("worm".into(), String::new())];
146
147        let request = HttpRequest::builder()
148            .method(http::Method::DELETE)
149            .uri(&uri)
150            .build();
151
152        let response = self
153            .client
154            .send_signed(request, Some(&self.bucket), query_params)
155            .await
156            .map_err(|e| OssError {
157                kind: OssErrorKind::TransportError,
158                context: Box::new(ErrorContext {
159                    operation: Some("AbortBucketWorm".into()),
160                    bucket: Some(self.bucket.to_string()),
161                    endpoint: Some(endpoint),
162                    ..Default::default()
163                }),
164                source: Some(Box::new(e)),
165            })?;
166
167        if response.status().is_success() {
168            Ok(AbortBucketWormOutput {
169                request_id: response
170                    .headers
171                    .get("x-oss-request-id")
172                    .and_then(|v| v.to_str().ok())
173                    .unwrap_or("")
174                    .to_string(),
175            })
176        } else {
177            Err(OssError {
178                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
179                    status_code: response.status().as_u16(),
180                    code: String::new(),
181                    message: String::new(),
182                    request_id: String::new(),
183                    host_id: String::new(),
184                    resource: Some(self.bucket.to_string()),
185                    string_to_sign: None,
186                })),
187                context: Box::new(ErrorContext {
188                    operation: Some("AbortBucketWorm".into()),
189                    bucket: Some(self.bucket.to_string()),
190                    ..Default::default()
191                }),
192                source: None,
193            })
194        }
195    }
196}
197
198#[derive(Debug, Clone)]
199pub struct AbortBucketWormOutput {
200    pub request_id: String,
201}
202
203pub struct CompleteBucketWormBuilder {
204    client: Arc<OSSClientInner>,
205    bucket: BucketName,
206    worm_id: String,
207}
208
209impl CompleteBucketWormBuilder {
210    pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName, worm_id: String) -> Self {
211        Self {
212            client,
213            bucket,
214            worm_id,
215        }
216    }
217
218    pub async fn send(self) -> Result<CompleteBucketWormOutput> {
219        let endpoint = self.client.endpoint.clone();
220        let uri = format!(
221            "https://{}.{}?wormId={}",
222            self.bucket.as_str(),
223            endpoint,
224            self.worm_id
225        );
226        let query_params: Vec<(String, String)> = vec![("wormId".into(), self.worm_id)];
227
228        let request = HttpRequest::builder()
229            .method(http::Method::PUT)
230            .uri(&uri)
231            .build();
232
233        let response = self
234            .client
235            .send_signed(request, Some(&self.bucket), query_params)
236            .await
237            .map_err(|e| OssError {
238                kind: OssErrorKind::TransportError,
239                context: Box::new(ErrorContext {
240                    operation: Some("CompleteBucketWorm".into()),
241                    bucket: Some(self.bucket.to_string()),
242                    endpoint: Some(endpoint),
243                    ..Default::default()
244                }),
245                source: Some(Box::new(e)),
246            })?;
247
248        if response.status().is_success() {
249            Ok(CompleteBucketWormOutput {
250                request_id: response
251                    .headers
252                    .get("x-oss-request-id")
253                    .and_then(|v| v.to_str().ok())
254                    .unwrap_or("")
255                    .to_string(),
256            })
257        } else {
258            Err(OssError {
259                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
260                    status_code: response.status().as_u16(),
261                    code: String::new(),
262                    message: String::new(),
263                    request_id: String::new(),
264                    host_id: String::new(),
265                    resource: Some(self.bucket.to_string()),
266                    string_to_sign: None,
267                })),
268                context: Box::new(ErrorContext {
269                    operation: Some("CompleteBucketWorm".into()),
270                    bucket: Some(self.bucket.to_string()),
271                    ..Default::default()
272                }),
273                source: None,
274            })
275        }
276    }
277}
278
279#[derive(Debug, Clone)]
280pub struct CompleteBucketWormOutput {
281    pub request_id: String,
282}
283
284pub struct ExtendBucketWormBuilder {
285    client: Arc<OSSClientInner>,
286    bucket: BucketName,
287    worm_id: String,
288    extension_days: i32,
289}
290
291impl ExtendBucketWormBuilder {
292    pub(crate) fn new(
293        client: Arc<OSSClientInner>,
294        bucket: BucketName,
295        worm_id: String,
296        extension_days: i32,
297    ) -> Self {
298        Self {
299            client,
300            bucket,
301            worm_id,
302            extension_days,
303        }
304    }
305
306    pub async fn send(self) -> Result<ExtendBucketWormOutput> {
307        let endpoint = self.client.endpoint.clone();
308        let uri = format!(
309            "https://{}.{}?wormId={}&wormExtend",
310            self.bucket.as_str(),
311            endpoint,
312            self.worm_id
313        );
314        let query_params: Vec<(String, String)> = vec![
315            ("wormId".into(), self.worm_id),
316            ("wormExtend".into(), String::new()),
317        ];
318
319        let config = ExtendWormConfiguration {
320            retention_period_days: self.extension_days,
321        };
322        let body_xml = crate::util::xml::to_xml(&config)?;
323
324        let request = HttpRequest::builder()
325            .method(http::Method::POST)
326            .uri(&uri)
327            .body(bytes::Bytes::from(body_xml))
328            .build();
329
330        let response = self
331            .client
332            .send_signed(request, Some(&self.bucket), query_params)
333            .await
334            .map_err(|e| OssError {
335                kind: OssErrorKind::TransportError,
336                context: Box::new(ErrorContext {
337                    operation: Some("ExtendBucketWorm".into()),
338                    bucket: Some(self.bucket.to_string()),
339                    endpoint: Some(endpoint),
340                    ..Default::default()
341                }),
342                source: Some(Box::new(e)),
343            })?;
344
345        if response.status().is_success() {
346            Ok(ExtendBucketWormOutput {
347                request_id: response
348                    .headers
349                    .get("x-oss-request-id")
350                    .and_then(|v| v.to_str().ok())
351                    .unwrap_or("")
352                    .to_string(),
353            })
354        } else {
355            Err(OssError {
356                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
357                    status_code: response.status().as_u16(),
358                    code: String::new(),
359                    message: String::new(),
360                    request_id: String::new(),
361                    host_id: String::new(),
362                    resource: Some(self.bucket.to_string()),
363                    string_to_sign: None,
364                })),
365                context: Box::new(ErrorContext {
366                    operation: Some("ExtendBucketWorm".into()),
367                    bucket: Some(self.bucket.to_string()),
368                    ..Default::default()
369                }),
370                source: None,
371            })
372        }
373    }
374}
375
376#[derive(Debug, Clone)]
377pub struct ExtendBucketWormOutput {
378    pub request_id: String,
379}
380
381pub struct GetBucketWormBuilder {
382    client: Arc<OSSClientInner>,
383    bucket: BucketName,
384}
385
386impl GetBucketWormBuilder {
387    pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName) -> Self {
388        Self { client, bucket }
389    }
390
391    pub async fn send(self) -> Result<GetBucketWormOutput> {
392        let endpoint = self.client.endpoint.clone();
393        let uri = format!("https://{}.{}?worm", self.bucket.as_str(), endpoint);
394        let query_params: Vec<(String, String)> = vec![("worm".into(), String::new())];
395
396        let request = HttpRequest::builder()
397            .method(http::Method::GET)
398            .uri(&uri)
399            .build();
400
401        let response = self
402            .client
403            .send_signed(request, Some(&self.bucket), query_params)
404            .await
405            .map_err(|e| OssError {
406                kind: OssErrorKind::TransportError,
407                context: Box::new(ErrorContext {
408                    operation: Some("GetBucketWorm".into()),
409                    bucket: Some(self.bucket.to_string()),
410                    endpoint: Some(endpoint),
411                    ..Default::default()
412                }),
413                source: Some(Box::new(e)),
414            })?;
415
416        if response.is_success() {
417            let body_str = response.body_as_str().unwrap_or("");
418            let config: WormConfigurationResponse =
419                crate::util::xml::from_xml(body_str).map_err(|e| OssError {
420                    kind: OssErrorKind::DeserializationError,
421                    context: Box::new(ErrorContext {
422                        operation: Some("GetBucketWorm: parse XML".into()),
423                        bucket: Some(self.bucket.to_string()),
424                        ..Default::default()
425                    }),
426                    source: Some(Box::new(e)),
427                })?;
428
429            Ok(GetBucketWormOutput {
430                worm_id: config.worm_id,
431                state: config.state,
432                creation_date: config.creation_date,
433                retention_period_days: config.retention_period_days,
434            })
435        } else {
436            Err(OssError {
437                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
438                    status_code: response.status().as_u16(),
439                    code: String::new(),
440                    message: String::new(),
441                    request_id: String::new(),
442                    host_id: String::new(),
443                    resource: Some(self.bucket.to_string()),
444                    string_to_sign: None,
445                })),
446                context: Box::new(ErrorContext {
447                    operation: Some("GetBucketWorm".into()),
448                    bucket: Some(self.bucket.to_string()),
449                    ..Default::default()
450                }),
451                source: None,
452            })
453        }
454    }
455}
456
457#[derive(Debug, Clone)]
458pub struct GetBucketWormOutput {
459    pub worm_id: String,
460    pub state: String,
461    pub creation_date: String,
462    pub retention_period_days: i32,
463}
464
465impl BucketOperations {
466    pub fn initiate_worm(&self, retention_days: i32) -> InitiateBucketWormBuilder {
467        InitiateBucketWormBuilder::new(
468            self.client_inner().clone(),
469            self.bucket_name().clone(),
470            retention_days,
471        )
472    }
473
474    pub fn abort_worm(&self) -> AbortBucketWormBuilder {
475        AbortBucketWormBuilder::new(self.client_inner().clone(), self.bucket_name().clone())
476    }
477
478    pub fn complete_worm(&self, worm_id: String) -> CompleteBucketWormBuilder {
479        CompleteBucketWormBuilder::new(
480            self.client_inner().clone(),
481            self.bucket_name().clone(),
482            worm_id,
483        )
484    }
485
486    pub fn extend_worm(&self, worm_id: String, extension_days: i32) -> ExtendBucketWormBuilder {
487        ExtendBucketWormBuilder::new(
488            self.client_inner().clone(),
489            self.bucket_name().clone(),
490            worm_id,
491            extension_days,
492        )
493    }
494
495    pub fn get_worm(&self) -> GetBucketWormBuilder {
496        GetBucketWormBuilder::new(self.client_inner().clone(), self.bucket_name().clone())
497    }
498}
499
500#[cfg(test)]
501mod tests {
502    use std::sync::Mutex;
503
504    use crate::client::OSSClientInner;
505    use crate::config::credentials::Credentials;
506    use crate::http::client::{HttpClient, HttpRequest, HttpResponse};
507    use crate::types::region::Region;
508
509    use super::*;
510
511    struct RecordingHttpClient {
512        requests: Arc<Mutex<Vec<HttpRequest>>>,
513        status_code: http::StatusCode,
514        response_body: bytes::Bytes,
515    }
516
517    #[async_trait::async_trait]
518    impl HttpClient for RecordingHttpClient {
519        async fn send(&self, request: HttpRequest) -> crate::error::Result<HttpResponse> {
520            self.requests.lock().unwrap().push(request);
521            let mut headers = http::HeaderMap::new();
522            headers.insert(
523                "x-oss-request-id",
524                http::HeaderValue::from_static("rid-worm"),
525            );
526            Ok(HttpResponse {
527                status: self.status_code,
528                headers,
529                body: self.response_body.clone(),
530            })
531        }
532    }
533
534    fn create_test_inner(
535        body: bytes::Bytes,
536    ) -> (Arc<OSSClientInner>, Arc<Mutex<Vec<HttpRequest>>>) {
537        let requests = Arc::new(Mutex::new(Vec::new()));
538        let http = Arc::new(RecordingHttpClient {
539            requests: requests.clone(),
540            status_code: http::StatusCode::OK,
541            response_body: body,
542        });
543        let credentials = Arc::new(crate::config::credentials::StaticCredentialsProvider::new(
544            Credentials::builder()
545                .access_key_id("test-ak")
546                .access_key_secret("test-sk")
547                .build()
548                .unwrap(),
549        ));
550        let inner = Arc::new(OSSClientInner {
551            http,
552            credentials,
553            signer: Arc::from(crate::signer::create_signer(crate::signer::SignVersion::V4)),
554            region: Region::CnHangzhou,
555            endpoint: "oss-cn-hangzhou.aliyuncs.com".into(),
556        });
557        (inner, requests)
558    }
559
560    #[test]
561    fn worm_initiate_xml() {
562        let config = InitiateWormConfiguration {
563            retention_period_days: 365,
564        };
565        let xml = crate::util::xml::to_xml(&config).unwrap();
566        assert!(xml.contains("<RetentionPeriodInDays>365</RetentionPeriodInDays>"));
567    }
568
569    #[tokio::test]
570    async fn get_worm_parses_response() {
571        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
572<WormConfiguration>
573  <WormId>worm-123</WormId>
574  <State>Locked</State>
575  <CreationDate>2024-01-01T00:00:00.000Z</CreationDate>
576  <RetentionPeriodInDays>365</RetentionPeriodInDays>
577</WormConfiguration>"#;
578        let (inner, _) = create_test_inner(bytes::Bytes::from(xml));
579        let builder = GetBucketWormBuilder::new(inner, BucketName::new("test-bucket").unwrap());
580        let output = builder.send().await.unwrap();
581        assert_eq!(output.worm_id, "worm-123");
582        assert_eq!(output.state, "Locked");
583    }
584}