Skip to main content

aliyun_oss/operations/
bucket_cors.rs

1//! Bucket CORS configuration 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)]
13pub struct CorsRule {
14    pub allowed_origins: Vec<String>,
15    pub allowed_methods: Vec<String>,
16    pub allowed_headers: Vec<String>,
17    pub expose_headers: Vec<String>,
18    pub max_age_seconds: Option<i32>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22#[serde(rename = "CORSConfiguration")]
23struct CORSConfiguration {
24    #[serde(rename = "CORSRule")]
25    rules: Vec<CorsRuleData>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29struct CorsRuleData {
30    #[serde(rename = "AllowedOrigin")]
31    allowed_origin: Vec<String>,
32    #[serde(rename = "AllowedMethod")]
33    allowed_method: Vec<String>,
34    #[serde(
35        rename = "AllowedHeader",
36        default,
37        skip_serializing_if = "Vec::is_empty"
38    )]
39    allowed_header: Vec<String>,
40    #[serde(
41        rename = "ExposeHeader",
42        default,
43        skip_serializing_if = "Vec::is_empty"
44    )]
45    expose_header: Vec<String>,
46    #[serde(
47        rename = "MaxAgeSeconds",
48        default,
49        skip_serializing_if = "Option::is_none"
50    )]
51    max_age_seconds: Option<i32>,
52}
53
54pub struct PutBucketCorsBuilder {
55    client: Arc<OSSClientInner>,
56    bucket: BucketName,
57    rules: Vec<CorsRule>,
58}
59
60impl PutBucketCorsBuilder {
61    pub(crate) fn new(
62        client: Arc<OSSClientInner>,
63        bucket: BucketName,
64        rules: Vec<CorsRule>,
65    ) -> Self {
66        Self {
67            client,
68            bucket,
69            rules,
70        }
71    }
72
73    pub async fn send(self) -> Result<PutBucketCorsOutput> {
74        let endpoint = self.client.endpoint.clone();
75        let uri = format!("https://{}.{}?cors", self.bucket.as_str(), endpoint);
76
77        let query_params: Vec<(String, String)> = vec![("cors".into(), String::new())];
78
79        let config = CORSConfiguration {
80            rules: self
81                .rules
82                .into_iter()
83                .map(|r| CorsRuleData {
84                    allowed_origin: r.allowed_origins,
85                    allowed_method: r.allowed_methods,
86                    allowed_header: r.allowed_headers,
87                    expose_header: r.expose_headers,
88                    max_age_seconds: r.max_age_seconds,
89                })
90                .collect(),
91        };
92
93        let body_xml = crate::util::xml::to_xml(&config)?;
94
95        let request = HttpRequest::builder()
96            .method(http::Method::PUT)
97            .uri(&uri)
98            .body(bytes::Bytes::from(body_xml))
99            .build();
100
101        let response = self
102            .client
103            .send_signed(request, Some(&self.bucket), query_params)
104            .await
105            .map_err(|e| OssError {
106                kind: OssErrorKind::TransportError,
107                context: Box::new(ErrorContext {
108                    operation: Some("PutBucketCors".into()),
109                    bucket: Some(self.bucket.to_string()),
110                    endpoint: Some(endpoint),
111                    ..Default::default()
112                }),
113                source: Some(Box::new(e)),
114            })?;
115
116        if response.status().is_success() {
117            Ok(PutBucketCorsOutput {
118                request_id: response
119                    .headers
120                    .get("x-oss-request-id")
121                    .and_then(|v| v.to_str().ok())
122                    .unwrap_or("")
123                    .to_string(),
124            })
125        } else {
126            Err(OssError {
127                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
128                    status_code: response.status().as_u16(),
129                    code: String::new(),
130                    message: String::new(),
131                    request_id: String::new(),
132                    host_id: String::new(),
133                    resource: Some(self.bucket.to_string()),
134                    string_to_sign: None,
135                })),
136                context: Box::new(ErrorContext {
137                    operation: Some("PutBucketCors".into()),
138                    bucket: Some(self.bucket.to_string()),
139                    ..Default::default()
140                }),
141                source: None,
142            })
143        }
144    }
145}
146
147#[derive(Debug, Clone)]
148pub struct PutBucketCorsOutput {
149    pub request_id: String,
150}
151
152pub struct GetBucketCorsBuilder {
153    client: Arc<OSSClientInner>,
154    bucket: BucketName,
155}
156
157impl GetBucketCorsBuilder {
158    pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName) -> Self {
159        Self { client, bucket }
160    }
161
162    pub async fn send(self) -> Result<GetBucketCorsOutput> {
163        let endpoint = self.client.endpoint.clone();
164        let uri = format!("https://{}.{}?cors", self.bucket.as_str(), endpoint);
165
166        let query_params: Vec<(String, String)> = vec![("cors".into(), String::new())];
167
168        let request = HttpRequest::builder()
169            .method(http::Method::GET)
170            .uri(&uri)
171            .build();
172
173        let response = self
174            .client
175            .send_signed(request, Some(&self.bucket), query_params)
176            .await
177            .map_err(|e| OssError {
178                kind: OssErrorKind::TransportError,
179                context: Box::new(ErrorContext {
180                    operation: Some("GetBucketCors".into()),
181                    bucket: Some(self.bucket.to_string()),
182                    endpoint: Some(endpoint),
183                    ..Default::default()
184                }),
185                source: Some(Box::new(e)),
186            })?;
187
188        if response.is_success() {
189            let body_str = response.body_as_str().unwrap_or("");
190            let config: CORSConfiguration =
191                crate::util::xml::from_xml(body_str).map_err(|e| OssError {
192                    kind: OssErrorKind::DeserializationError,
193                    context: Box::new(ErrorContext {
194                        operation: Some("GetBucketCors: parse XML".into()),
195                        bucket: Some(self.bucket.to_string()),
196                        ..Default::default()
197                    }),
198                    source: Some(Box::new(e)),
199                })?;
200
201            Ok(GetBucketCorsOutput {
202                rules: config
203                    .rules
204                    .into_iter()
205                    .map(|r| CorsRule {
206                        allowed_origins: r.allowed_origin,
207                        allowed_methods: r.allowed_method,
208                        allowed_headers: r.allowed_header,
209                        expose_headers: r.expose_header,
210                        max_age_seconds: r.max_age_seconds,
211                    })
212                    .collect(),
213            })
214        } else {
215            Err(OssError {
216                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
217                    status_code: response.status().as_u16(),
218                    code: String::new(),
219                    message: String::new(),
220                    request_id: String::new(),
221                    host_id: String::new(),
222                    resource: Some(self.bucket.to_string()),
223                    string_to_sign: None,
224                })),
225                context: Box::new(ErrorContext {
226                    operation: Some("GetBucketCors".into()),
227                    bucket: Some(self.bucket.to_string()),
228                    ..Default::default()
229                }),
230                source: None,
231            })
232        }
233    }
234}
235
236#[derive(Debug, Clone)]
237pub struct GetBucketCorsOutput {
238    pub rules: Vec<CorsRule>,
239}
240
241pub struct DeleteBucketCorsBuilder {
242    client: Arc<OSSClientInner>,
243    bucket: BucketName,
244}
245
246impl DeleteBucketCorsBuilder {
247    pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName) -> Self {
248        Self { client, bucket }
249    }
250
251    pub async fn send(self) -> Result<DeleteBucketCorsOutput> {
252        let endpoint = self.client.endpoint.clone();
253        let uri = format!("https://{}.{}?cors", self.bucket.as_str(), endpoint);
254
255        let query_params: Vec<(String, String)> = vec![("cors".into(), String::new())];
256
257        let request = HttpRequest::builder()
258            .method(http::Method::DELETE)
259            .uri(&uri)
260            .build();
261
262        let response = self
263            .client
264            .send_signed(request, Some(&self.bucket), query_params)
265            .await
266            .map_err(|e| OssError {
267                kind: OssErrorKind::TransportError,
268                context: Box::new(ErrorContext {
269                    operation: Some("DeleteBucketCors".into()),
270                    bucket: Some(self.bucket.to_string()),
271                    endpoint: Some(endpoint),
272                    ..Default::default()
273                }),
274                source: Some(Box::new(e)),
275            })?;
276
277        if response.status().is_success() {
278            Ok(DeleteBucketCorsOutput {
279                request_id: response
280                    .headers
281                    .get("x-oss-request-id")
282                    .and_then(|v| v.to_str().ok())
283                    .unwrap_or("")
284                    .to_string(),
285            })
286        } else {
287            Err(OssError {
288                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
289                    status_code: response.status().as_u16(),
290                    code: String::new(),
291                    message: String::new(),
292                    request_id: String::new(),
293                    host_id: String::new(),
294                    resource: Some(self.bucket.to_string()),
295                    string_to_sign: None,
296                })),
297                context: Box::new(ErrorContext {
298                    operation: Some("DeleteBucketCors".into()),
299                    bucket: Some(self.bucket.to_string()),
300                    ..Default::default()
301                }),
302                source: None,
303            })
304        }
305    }
306}
307
308#[derive(Debug, Clone)]
309pub struct DeleteBucketCorsOutput {
310    pub request_id: String,
311}
312
313impl BucketOperations {
314    pub fn put_cors(&self, rules: Vec<CorsRule>) -> PutBucketCorsBuilder {
315        PutBucketCorsBuilder::new(
316            self.client_inner().clone(),
317            self.bucket_name().clone(),
318            rules,
319        )
320    }
321
322    pub fn get_cors(&self) -> GetBucketCorsBuilder {
323        GetBucketCorsBuilder::new(self.client_inner().clone(), self.bucket_name().clone())
324    }
325
326    pub fn delete_cors(&self) -> DeleteBucketCorsBuilder {
327        DeleteBucketCorsBuilder::new(self.client_inner().clone(), self.bucket_name().clone())
328    }
329}
330
331#[cfg(test)]
332mod tests {
333    use std::sync::Mutex;
334
335    use crate::client::OSSClientInner;
336    use crate::config::credentials::Credentials;
337    use crate::http::client::{HttpClient, HttpRequest, HttpResponse};
338    use crate::types::region::Region;
339
340    use super::*;
341
342    struct RecordingHttpClient {
343        requests: Arc<Mutex<Vec<HttpRequest>>>,
344        status_code: http::StatusCode,
345        response_body: bytes::Bytes,
346    }
347
348    #[async_trait::async_trait]
349    impl HttpClient for RecordingHttpClient {
350        async fn send(&self, request: HttpRequest) -> crate::error::Result<HttpResponse> {
351            self.requests.lock().unwrap().push(request);
352            let mut headers = http::HeaderMap::new();
353            headers.insert(
354                "x-oss-request-id",
355                http::HeaderValue::from_static("rid-cors"),
356            );
357            Ok(HttpResponse {
358                status: self.status_code,
359                headers,
360                body: self.response_body.clone(),
361            })
362        }
363    }
364
365    fn create_test_inner_with_body(
366        status: http::StatusCode,
367        body: bytes::Bytes,
368    ) -> (Arc<OSSClientInner>, Arc<Mutex<Vec<HttpRequest>>>) {
369        let requests = Arc::new(Mutex::new(Vec::new()));
370        let http = Arc::new(RecordingHttpClient {
371            requests: requests.clone(),
372            status_code: status,
373            response_body: body,
374        });
375        let credentials = Arc::new(crate::config::credentials::StaticCredentialsProvider::new(
376            Credentials::builder()
377                .access_key_id("test-ak")
378                .access_key_secret("test-sk")
379                .build()
380                .unwrap(),
381        ));
382        let inner = Arc::new(OSSClientInner {
383            http,
384            credentials,
385            signer: Arc::from(crate::signer::create_signer(crate::signer::SignVersion::V4)),
386            region: Region::CnHangzhou,
387            endpoint: "oss-cn-hangzhou.aliyuncs.com".into(),
388        });
389        (inner, requests)
390    }
391
392    #[test]
393    fn cors_xml_generation() {
394        let config = CORSConfiguration {
395            rules: vec![CorsRuleData {
396                allowed_origin: vec!["*".into()],
397                allowed_method: vec!["GET".into(), "PUT".into()],
398                allowed_header: vec!["*".into()],
399                expose_header: vec![],
400                max_age_seconds: Some(3600),
401            }],
402        };
403        let xml = crate::util::xml::to_xml(&config).unwrap();
404        assert!(xml.contains("<AllowedOrigin>*</AllowedOrigin>"));
405        assert!(xml.contains("<AllowedMethod>GET</AllowedMethod>"));
406        assert!(xml.contains("<MaxAgeSeconds>3600</MaxAgeSeconds>"));
407    }
408
409    #[tokio::test]
410    async fn get_bucket_cors_parses_xml() {
411        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
412<CORSConfiguration>
413  <CORSRule>
414    <AllowedOrigin>*</AllowedOrigin>
415    <AllowedMethod>GET</AllowedMethod>
416  </CORSRule>
417</CORSConfiguration>"#;
418        let (inner, _) = create_test_inner_with_body(http::StatusCode::OK, bytes::Bytes::from(xml));
419        let builder = GetBucketCorsBuilder::new(inner, BucketName::new("test-bucket").unwrap());
420        let output = builder.send().await.unwrap();
421        assert_eq!(output.rules.len(), 1);
422    }
423
424    #[tokio::test]
425    async fn delete_bucket_cors_sends_delete_request() {
426        let (inner, requests) =
427            create_test_inner_with_body(http::StatusCode::NO_CONTENT, bytes::Bytes::new());
428        let builder = DeleteBucketCorsBuilder::new(inner, BucketName::new("test-bucket").unwrap());
429        builder.send().await.unwrap();
430        let captured = requests.lock().unwrap();
431        assert_eq!(captured[0].method, http::Method::DELETE);
432    }
433}