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)]
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}