1use 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}