Skip to main content

aliyun_oss/operations/
bucket_versioning.rs

1//! Bucket versioning 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 = "VersioningConfiguration")]
14struct VersioningConfiguration {
15    #[serde(rename = "Status")]
16    status: String,
17}
18
19#[derive(Debug, Clone, Deserialize)]
20#[serde(rename = "VersioningConfiguration")]
21struct VersioningConfigurationResponse {
22    #[serde(rename = "Status")]
23    status: String,
24}
25
26pub struct PutBucketVersioningBuilder {
27    client: Arc<OSSClientInner>,
28    bucket: BucketName,
29    status: String,
30}
31
32impl PutBucketVersioningBuilder {
33    pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName, status: String) -> Self {
34        Self {
35            client,
36            bucket,
37            status,
38        }
39    }
40
41    pub async fn send(self) -> Result<PutBucketVersioningOutput> {
42        let endpoint = self.client.endpoint.clone();
43        let uri = format!("https://{}.{}?versioning", self.bucket.as_str(), endpoint);
44
45        let query_params: Vec<(String, String)> = vec![("versioning".into(), String::new())];
46
47        let config = VersioningConfiguration {
48            status: self.status,
49        };
50        let body_xml = crate::util::xml::to_xml(&config)?;
51
52        let request = HttpRequest::builder()
53            .method(http::Method::PUT)
54            .uri(&uri)
55            .body(bytes::Bytes::from(body_xml))
56            .build();
57
58        let response = self
59            .client
60            .send_signed(request, Some(&self.bucket), query_params)
61            .await
62            .map_err(|e| OssError {
63                kind: OssErrorKind::TransportError,
64                context: Box::new(ErrorContext {
65                    operation: Some("PutBucketVersioning".into()),
66                    bucket: Some(self.bucket.to_string()),
67                    endpoint: Some(endpoint),
68                    ..Default::default()
69                }),
70                source: Some(Box::new(e)),
71            })?;
72
73        if response.status().is_success() {
74            Ok(PutBucketVersioningOutput {
75                request_id: response
76                    .headers
77                    .get("x-oss-request-id")
78                    .and_then(|v| v.to_str().ok())
79                    .unwrap_or("")
80                    .to_string(),
81            })
82        } else {
83            Err(OssError {
84                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
85                    status_code: response.status().as_u16(),
86                    code: String::new(),
87                    message: String::new(),
88                    request_id: String::new(),
89                    host_id: String::new(),
90                    resource: Some(self.bucket.to_string()),
91                    string_to_sign: None,
92                })),
93                context: Box::new(ErrorContext {
94                    operation: Some("PutBucketVersioning".into()),
95                    bucket: Some(self.bucket.to_string()),
96                    ..Default::default()
97                }),
98                source: None,
99            })
100        }
101    }
102}
103
104#[derive(Debug, Clone)]
105pub struct PutBucketVersioningOutput {
106    pub request_id: String,
107}
108
109pub struct GetBucketVersioningBuilder {
110    client: Arc<OSSClientInner>,
111    bucket: BucketName,
112}
113
114impl GetBucketVersioningBuilder {
115    pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName) -> Self {
116        Self { client, bucket }
117    }
118
119    pub async fn send(self) -> Result<GetBucketVersioningOutput> {
120        let endpoint = self.client.endpoint.clone();
121        let uri = format!("https://{}.{}?versioning", self.bucket.as_str(), endpoint);
122
123        let query_params: Vec<(String, String)> = vec![("versioning".into(), String::new())];
124
125        let request = HttpRequest::builder()
126            .method(http::Method::GET)
127            .uri(&uri)
128            .build();
129
130        let response = self
131            .client
132            .send_signed(request, Some(&self.bucket), query_params)
133            .await
134            .map_err(|e| OssError {
135                kind: OssErrorKind::TransportError,
136                context: Box::new(ErrorContext {
137                    operation: Some("GetBucketVersioning".into()),
138                    bucket: Some(self.bucket.to_string()),
139                    endpoint: Some(endpoint),
140                    ..Default::default()
141                }),
142                source: Some(Box::new(e)),
143            })?;
144
145        if response.is_success() {
146            let body_str = response.body_as_str().unwrap_or("");
147            let config: VersioningConfigurationResponse = crate::util::xml::from_xml(body_str)
148                .map_err(|e| OssError {
149                    kind: OssErrorKind::DeserializationError,
150                    context: Box::new(ErrorContext {
151                        operation: Some("GetBucketVersioning: parse XML".into()),
152                        bucket: Some(self.bucket.to_string()),
153                        ..Default::default()
154                    }),
155                    source: Some(Box::new(e)),
156                })?;
157
158            Ok(GetBucketVersioningOutput {
159                status: config.status,
160            })
161        } else {
162            Err(OssError {
163                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
164                    status_code: response.status().as_u16(),
165                    code: String::new(),
166                    message: String::new(),
167                    request_id: String::new(),
168                    host_id: String::new(),
169                    resource: Some(self.bucket.to_string()),
170                    string_to_sign: None,
171                })),
172                context: Box::new(ErrorContext {
173                    operation: Some("GetBucketVersioning".into()),
174                    bucket: Some(self.bucket.to_string()),
175                    ..Default::default()
176                }),
177                source: None,
178            })
179        }
180    }
181}
182
183#[derive(Debug, Clone)]
184pub struct GetBucketVersioningOutput {
185    pub status: String,
186}
187
188impl BucketOperations {
189    pub fn put_versioning(&self, status: impl Into<String>) -> PutBucketVersioningBuilder {
190        PutBucketVersioningBuilder::new(
191            self.client_inner().clone(),
192            self.bucket_name().clone(),
193            status.into(),
194        )
195    }
196
197    pub fn get_versioning(&self) -> GetBucketVersioningBuilder {
198        GetBucketVersioningBuilder::new(self.client_inner().clone(), self.bucket_name().clone())
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use std::str::FromStr;
205    use std::sync::Mutex;
206
207    use crate::client::OSSClientInner;
208    use crate::config::credentials::Credentials;
209    use crate::http::client::{HttpClient, HttpRequest, HttpResponse};
210    use crate::types::region::Region;
211
212    use super::*;
213
214    struct RecordingHttpClient {
215        requests: Arc<Mutex<Vec<HttpRequest>>>,
216        status_code: http::StatusCode,
217        response_body: bytes::Bytes,
218    }
219
220    #[async_trait::async_trait]
221    impl HttpClient for RecordingHttpClient {
222        async fn send(&self, request: HttpRequest) -> crate::error::Result<HttpResponse> {
223            self.requests.lock().unwrap().push(request);
224            let mut headers = http::HeaderMap::new();
225            headers.insert(
226                "x-oss-request-id",
227                http::HeaderValue::from_static("rid-versioning"),
228            );
229            Ok(HttpResponse {
230                status: self.status_code,
231                headers,
232                body: self.response_body.clone(),
233            })
234        }
235    }
236
237    fn create_test_inner_with_body(
238        status: http::StatusCode,
239        body: bytes::Bytes,
240    ) -> (Arc<OSSClientInner>, Arc<Mutex<Vec<HttpRequest>>>) {
241        let requests = Arc::new(Mutex::new(Vec::new()));
242        let http = Arc::new(RecordingHttpClient {
243            requests: requests.clone(),
244            status_code: status,
245            response_body: body,
246        });
247        let credentials = Arc::new(crate::config::credentials::StaticCredentialsProvider::new(
248            Credentials::builder()
249                .access_key_id("test-ak")
250                .access_key_secret("test-sk")
251                .build()
252                .unwrap(),
253        ));
254        let inner = Arc::new(OSSClientInner {
255            http,
256            credentials,
257            signer: Arc::from(crate::signer::create_signer(crate::signer::SignVersion::V4)),
258            region: Region::CnHangzhou,
259            endpoint: "oss-cn-hangzhou.aliyuncs.com".into(),
260        });
261        (inner, requests)
262    }
263
264    #[test]
265    fn put_versioning_xml_enabled() {
266        let config = VersioningConfiguration {
267            status: "Enabled".into(),
268        };
269        let xml = crate::util::xml::to_xml(&config).unwrap();
270        assert!(xml.contains("<Status>Enabled</Status>"));
271    }
272
273    #[test]
274    fn put_versioning_xml_suspended() {
275        let config = VersioningConfiguration {
276            status: "Suspended".into(),
277        };
278        let xml = crate::util::xml::to_xml(&config).unwrap();
279        assert!(xml.contains("<Status>Suspended</Status>"));
280    }
281
282    #[tokio::test]
283    async fn put_versioning_sends_request() {
284        let (inner, requests) =
285            create_test_inner_with_body(http::StatusCode::OK, bytes::Bytes::new());
286        let builder = PutBucketVersioningBuilder::new(
287            inner,
288            BucketName::new("test-bucket").unwrap(),
289            "Enabled".into(),
290        );
291        builder.send().await.unwrap();
292        let captured = requests.lock().unwrap();
293        assert_eq!(captured[0].method, http::Method::PUT);
294        assert!(captured[0].uri.contains("?versioning"));
295    }
296
297    #[tokio::test]
298    async fn get_versioning_parses_response() {
299        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
300<VersioningConfiguration>
301  <Status>Enabled</Status>
302</VersioningConfiguration>"#;
303        let (inner, _) = create_test_inner_with_body(http::StatusCode::OK, bytes::Bytes::from(xml));
304        let builder =
305            GetBucketVersioningBuilder::new(inner, BucketName::new("test-bucket").unwrap());
306        let output = builder.send().await.unwrap();
307        assert_eq!(output.status, "Enabled");
308    }
309
310    #[tokio::test]
311    #[ignore = "requires valid OSS credentials"]
312    async fn e2e_bucket_versioning() {
313        let ak = std::env::var("OSS_ACCESS_KEY_ID").expect("OSS_ACCESS_KEY_ID not set");
314        let sk = std::env::var("OSS_ACCESS_KEY_SECRET").expect("OSS_ACCESS_KEY_SECRET not set");
315        let region_str = std::env::var("OSS_REGION").unwrap_or_else(|_| "cn-wulanchabu".into());
316        let bucket_str = std::env::var("OSS_BUCKET").expect("OSS_BUCKET not set");
317
318        let region = Region::from_str(&region_str).unwrap_or_else(|_| Region::Custom {
319            endpoint: format!("oss-{}.aliyuncs.com", region_str),
320            region_id: region_str.clone(),
321        });
322
323        let client = crate::client::OSSClient::builder()
324            .region(region)
325            .credentials(ak, sk)
326            .build()
327            .unwrap();
328
329        let bucket = client.bucket(&bucket_str).unwrap();
330        bucket.put_versioning("Enabled").send().await.unwrap();
331        let output = bucket.get_versioning().send().await.unwrap();
332        assert!(!output.status.is_empty());
333        eprintln!("GetBucketVersioning: status={}", output.status);
334    }
335}