1use std::sync::Arc;
4
5use serde::Serialize;
6
7use crate::client::{BucketOperations, OSSClientInner};
8use crate::error::{ErrorContext, OssError, OssErrorKind, Result};
9use crate::http::client::HttpRequest;
10use crate::types::acl::BucketAcl;
11use crate::types::bucket::BucketName;
12use crate::types::region::Region;
13use crate::types::storage::{DataRedundancyType, StorageClass};
14use crate::util::xml::to_xml;
15
16#[derive(Debug, Clone, Serialize)]
17#[serde(rename = "CreateBucketConfiguration")]
18struct CreateBucketConfiguration {
19 #[serde(rename = "StorageClass", skip_serializing_if = "Option::is_none")]
20 storage_class: Option<String>,
21 #[serde(rename = "DataRedundancyType", skip_serializing_if = "Option::is_none")]
22 data_redundancy_type: Option<String>,
23}
24
25pub struct PutBucketBuilder {
26 client: Arc<OSSClientInner>,
27 bucket: BucketName,
28 acl: Option<BucketAcl>,
29 storage_class: Option<StorageClass>,
30 data_redundancy: Option<DataRedundancyType>,
31}
32
33impl PutBucketBuilder {
34 pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName) -> Self {
35 Self {
36 client,
37 bucket,
38 acl: None,
39 storage_class: None,
40 data_redundancy: None,
41 }
42 }
43
44 pub fn acl(mut self, acl: BucketAcl) -> Self {
45 self.acl = Some(acl);
46 self
47 }
48
49 pub fn storage_class(mut self, sc: StorageClass) -> Self {
50 self.storage_class = Some(sc);
51 self
52 }
53
54 pub fn data_redundancy(mut self, dr: DataRedundancyType) -> Self {
55 self.data_redundancy = Some(dr);
56 self
57 }
58
59 pub async fn send(self) -> Result<PutBucketOutput> {
60 let region: Region = self.client.region.clone();
61 let endpoint = self.client.endpoint.clone();
62 let uri = format!("https://{}/{}/", endpoint, self.bucket.as_str());
63
64 let mut req = HttpRequest::builder().method(http::Method::PUT).uri(&uri);
65
66 if let Some(acl) = &self.acl {
67 req = req.header(
68 http::HeaderName::from_static("x-oss-acl"),
69 http::HeaderValue::from_str(acl.as_str()).map_err(|e| OssError {
70 kind: OssErrorKind::ValidationError,
71 context: Box::new(ErrorContext {
72 operation: Some("set x-oss-acl header".into()),
73 bucket: Some(self.bucket.to_string()),
74 ..Default::default()
75 }),
76 source: Some(Box::new(e)),
77 })?,
78 );
79 }
80
81 let config = CreateBucketConfiguration {
82 storage_class: self.storage_class.map(|sc| sc.as_str().to_string()),
83 data_redundancy_type: self.data_redundancy.map(|dr| dr.as_str().to_string()),
84 };
85
86 if config.storage_class.is_some() || config.data_redundancy_type.is_some() {
87 let body_xml = to_xml(&config)?;
88 req = req.body(bytes::Bytes::from(body_xml));
89 }
90
91 let request = req.build();
92
93 let response = self.client.http.send(request).await.map_err(|e| OssError {
94 kind: OssErrorKind::TransportError,
95 context: Box::new(ErrorContext {
96 operation: Some("PutBucket".into()),
97 bucket: Some(self.bucket.to_string()),
98 endpoint: Some(endpoint),
99 ..Default::default()
100 }),
101 source: Some(Box::new(e)),
102 })?;
103
104 if response.is_success() {
105 Ok(PutBucketOutput {
106 request_id: response
107 .headers
108 .get("x-oss-request-id")
109 .and_then(|v| v.to_str().ok())
110 .unwrap_or("")
111 .to_string(),
112 region,
113 })
114 } else {
115 Err(OssError {
116 kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
117 status_code: response.status().as_u16(),
118 code: String::new(),
119 message: String::new(),
120 request_id: String::new(),
121 host_id: String::new(),
122 resource: Some(self.bucket.to_string()),
123 string_to_sign: None,
124 })),
125 context: Box::new(ErrorContext {
126 operation: Some("PutBucket".into()),
127 bucket: Some(self.bucket.to_string()),
128 ..Default::default()
129 }),
130 source: None,
131 })
132 }
133 }
134}
135
136#[derive(Debug, Clone)]
137pub struct PutBucketOutput {
138 pub request_id: String,
139 pub region: Region,
140}
141
142impl BucketOperations {
143 pub fn create(&self) -> PutBucketBuilder {
144 PutBucketBuilder::new(self.client_inner().clone(), self.bucket_name().clone())
145 }
146
147 pub fn delete(&self) -> DeleteBucketBuilder {
148 DeleteBucketBuilder::new(self.client_inner().clone(), self.bucket_name().clone())
149 }
150
151 pub fn get_info(&self) -> GetBucketInfoBuilder {
152 GetBucketInfoBuilder::new(self.client_inner().clone(), self.bucket_name().clone())
153 }
154
155 pub fn get_stat(&self) -> GetBucketStatBuilder {
156 GetBucketStatBuilder::new(self.client_inner().clone(), self.bucket_name().clone())
157 }
158}
159
160pub struct DeleteBucketBuilder {
161 client: Arc<OSSClientInner>,
162 bucket: BucketName,
163}
164
165impl DeleteBucketBuilder {
166 pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName) -> Self {
167 Self { client, bucket }
168 }
169
170 pub async fn send(self) -> Result<DeleteBucketOutput> {
171 let endpoint = self.client.endpoint.clone();
172 let uri = format!("https://{}.{}", self.bucket.as_str(), endpoint);
173
174 let request = HttpRequest::builder()
175 .method(http::Method::DELETE)
176 .uri(&uri)
177 .build();
178
179 let response = self
180 .client
181 .send_signed(request, Some(&self.bucket), Vec::new())
182 .await
183 .map_err(|e| OssError {
184 kind: OssErrorKind::TransportError,
185 context: Box::new(ErrorContext {
186 operation: Some("DeleteBucket".into()),
187 bucket: Some(self.bucket.to_string()),
188 endpoint: Some(endpoint),
189 ..Default::default()
190 }),
191 source: Some(Box::new(e)),
192 })?;
193
194 if response.status().is_success() {
195 Ok(DeleteBucketOutput {
196 request_id: response
197 .headers
198 .get("x-oss-request-id")
199 .and_then(|v| v.to_str().ok())
200 .unwrap_or("")
201 .to_string(),
202 })
203 } else {
204 Err(OssError {
205 kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
206 status_code: response.status().as_u16(),
207 code: String::new(),
208 message: String::new(),
209 request_id: String::new(),
210 host_id: String::new(),
211 resource: Some(self.bucket.to_string()),
212 string_to_sign: None,
213 })),
214 context: Box::new(ErrorContext {
215 operation: Some("DeleteBucket".into()),
216 bucket: Some(self.bucket.to_string()),
217 ..Default::default()
218 }),
219 source: None,
220 })
221 }
222 }
223}
224
225#[derive(Debug, Clone)]
226pub struct DeleteBucketOutput {
227 pub request_id: String,
228}
229
230pub struct GetBucketInfoBuilder {
231 client: Arc<OSSClientInner>,
232 bucket: BucketName,
233}
234
235impl GetBucketInfoBuilder {
236 pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName) -> Self {
237 Self { client, bucket }
238 }
239
240 pub async fn send(self) -> Result<crate::types::response::GetBucketInfoOutput> {
241 let endpoint = self.client.endpoint.clone();
242 let uri = format!("https://{}.{}?bucketInfo", self.bucket.as_str(), endpoint);
243
244 let request = HttpRequest::builder()
245 .method(http::Method::GET)
246 .uri(&uri)
247 .build();
248
249 let query_params = vec![("bucketInfo".into(), "".into())];
250
251 let response = self
252 .client
253 .send_signed(request, Some(&self.bucket), query_params)
254 .await
255 .map_err(|e| OssError {
256 kind: OssErrorKind::TransportError,
257 context: Box::new(ErrorContext {
258 operation: Some("GetBucketInfo".into()),
259 bucket: Some(self.bucket.to_string()),
260 endpoint: Some(endpoint),
261 ..Default::default()
262 }),
263 source: Some(Box::new(e)),
264 })?;
265
266 if response.is_success() {
267 let body_str = response.body_as_str().unwrap_or("");
268 Ok(crate::util::xml::from_xml(body_str)?)
269 } else {
270 Err(OssError {
271 kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
272 status_code: response.status().as_u16(),
273 code: String::new(),
274 message: String::new(),
275 request_id: String::new(),
276 host_id: String::new(),
277 resource: Some(self.bucket.to_string()),
278 string_to_sign: None,
279 })),
280 context: Box::new(ErrorContext {
281 operation: Some("GetBucketInfo".into()),
282 bucket: Some(self.bucket.to_string()),
283 ..Default::default()
284 }),
285 source: None,
286 })
287 }
288 }
289}
290
291pub struct GetBucketStatBuilder {
292 client: Arc<OSSClientInner>,
293 bucket: BucketName,
294}
295
296impl GetBucketStatBuilder {
297 pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName) -> Self {
298 Self { client, bucket }
299 }
300
301 pub async fn send(self) -> Result<crate::types::response::GetBucketStatOutput> {
302 let endpoint = self.client.endpoint.clone();
303 let uri = format!("https://{}.{}?stat", self.bucket.as_str(), endpoint);
304 let query_params: Vec<(String, String)> = vec![("stat".into(), String::new())];
305
306 let request = HttpRequest::builder()
307 .method(http::Method::GET)
308 .uri(&uri)
309 .build();
310
311 let response = self
312 .client
313 .send_signed(request, Some(&self.bucket), query_params)
314 .await
315 .map_err(|e| OssError {
316 kind: OssErrorKind::TransportError,
317 context: Box::new(ErrorContext {
318 operation: Some("GetBucketStat".into()),
319 bucket: Some(self.bucket.to_string()),
320 endpoint: Some(endpoint),
321 ..Default::default()
322 }),
323 source: Some(Box::new(e)),
324 })?;
325
326 if response.is_success() {
327 let body_str = response.body_as_str().unwrap_or("");
328 Ok(crate::util::xml::from_xml(body_str)?)
329 } else {
330 Err(OssError {
331 kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
332 status_code: response.status().as_u16(),
333 code: String::new(),
334 message: String::new(),
335 request_id: String::new(),
336 host_id: String::new(),
337 resource: Some(self.bucket.to_string()),
338 string_to_sign: None,
339 })),
340 context: Box::new(ErrorContext {
341 operation: Some("GetBucketStat".into()),
342 bucket: Some(self.bucket.to_string()),
343 ..Default::default()
344 }),
345 source: None,
346 })
347 }
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use std::str::FromStr;
354 use std::sync::Mutex;
355
356 use crate::client::OSSClientInner;
357 use crate::config::credentials::Credentials;
358 use crate::http::client::{HttpClient, HttpRequest, HttpResponse};
359
360 use super::*;
361 use crate::util::xml::to_xml;
362
363 struct RecordingHttpClient {
364 requests: Arc<Mutex<Vec<HttpRequest>>>,
365 response_status: http::StatusCode,
366 response_body: bytes::Bytes,
367 }
368
369 #[async_trait::async_trait]
370 impl HttpClient for RecordingHttpClient {
371 async fn send(&self, request: HttpRequest) -> crate::error::Result<HttpResponse> {
372 self.requests.lock().unwrap().push(request);
373 let mut headers = http::HeaderMap::new();
374 headers.insert(
375 "x-oss-request-id",
376 http::HeaderValue::from_static("rid-bucket"),
377 );
378 Ok(HttpResponse {
379 status: self.response_status,
380 headers,
381 body: self.response_body.clone(),
382 })
383 }
384 }
385
386 fn create_test_inner(
387 status: http::StatusCode,
388 body: bytes::Bytes,
389 ) -> (Arc<OSSClientInner>, Arc<Mutex<Vec<HttpRequest>>>) {
390 let requests = Arc::new(Mutex::new(Vec::new()));
391 let http = Arc::new(RecordingHttpClient {
392 requests: requests.clone(),
393 response_status: status,
394 response_body: body,
395 });
396 let credentials = Arc::new(crate::config::credentials::StaticCredentialsProvider::new(
397 Credentials::builder()
398 .access_key_id("test-ak")
399 .access_key_secret("test-sk")
400 .build()
401 .unwrap(),
402 ));
403 let inner = Arc::new(OSSClientInner {
404 http,
405 credentials,
406 signer: Arc::from(crate::signer::create_signer(crate::signer::SignVersion::V4)),
407 region: crate::types::region::Region::CnHangzhou,
408 endpoint: "oss-cn-hangzhou.aliyuncs.com".into(),
409 });
410 (inner, requests)
411 }
412
413 #[test]
414 fn put_bucket_builder_generates_correct_xml_body() {
415 let config = CreateBucketConfiguration {
416 storage_class: Some("Standard".into()),
417 data_redundancy_type: Some("LRS".into()),
418 };
419 let xml = to_xml(&config).unwrap();
420 assert!(xml.contains("<StorageClass>Standard</StorageClass>"));
421 assert!(xml.contains("<DataRedundancyType>LRS</DataRedundancyType>"));
422 }
423
424 #[test]
425 fn put_bucket_builder_xml_omits_none_fields() {
426 let config = CreateBucketConfiguration {
427 storage_class: Some("IA".into()),
428 data_redundancy_type: None,
429 };
430 let xml = to_xml(&config).unwrap();
431 assert!(xml.contains("<StorageClass>IA</StorageClass>"));
432 assert!(!xml.contains("DataRedundancyType"));
433 }
434
435 #[test]
436 fn put_bucket_builder_empty_config() {
437 let config = CreateBucketConfiguration {
438 storage_class: None,
439 data_redundancy_type: None,
440 };
441 let xml = to_xml(&config).unwrap();
442 assert!(xml.contains("CreateBucketConfiguration"));
443 }
444
445 #[tokio::test]
446 async fn delete_bucket_sends_delete_request() {
447 let (inner, requests) =
448 create_test_inner(http::StatusCode::NO_CONTENT, bytes::Bytes::new());
449 let builder = DeleteBucketBuilder::new(inner, BucketName::new("test-bucket").unwrap());
450
451 builder.send().await.unwrap();
452
453 let captured = requests.lock().unwrap();
454 assert_eq!(captured[0].method, http::Method::DELETE);
455 }
456
457 #[tokio::test]
458 async fn get_bucket_info_parses_response() {
459 let info_xml = r#"<?xml version="1.0" encoding="UTF-8"?>
460<BucketInfo>
461 <Bucket>
462 <Name>test-bucket</Name>
463 <CreationDate>2024-01-01T00:00:00.000Z</CreationDate>
464 <Location>oss-cn-hangzhou</Location>
465 <StorageClass>Standard</StorageClass>
466 <ExtranetEndpoint>test-bucket.oss-cn-hangzhou.aliyuncs.com</ExtranetEndpoint>
467 <IntranetEndpoint>test-bucket.oss-cn-hangzhou-internal.aliyuncs.com</IntranetEndpoint>
468 <Owner>
469 <ID>owner-id</ID>
470 </Owner>
471 </Bucket>
472</BucketInfo>"#;
473
474 let (inner, requests) =
475 create_test_inner(http::StatusCode::OK, bytes::Bytes::from(info_xml));
476 let builder = GetBucketInfoBuilder::new(inner, BucketName::new("test-bucket").unwrap());
477
478 let output = builder.send().await.unwrap();
479 assert_eq!(output.bucket.name, "test-bucket");
480
481 let captured = requests.lock().unwrap();
482 assert!(captured[0].uri.contains("bucketInfo"));
483 }
484
485 #[tokio::test]
486 #[ignore = "requires valid OSS credentials"]
487 async fn e2e_get_bucket_info() {
488 let ak = std::env::var("OSS_ACCESS_KEY_ID").expect("OSS_ACCESS_KEY_ID not set");
489 let sk = std::env::var("OSS_ACCESS_KEY_SECRET").expect("OSS_ACCESS_KEY_SECRET not set");
490 let region_str = std::env::var("OSS_REGION").unwrap_or_else(|_| "cn-wulanchabu".into());
491 let bucket_str = std::env::var("OSS_BUCKET").expect("OSS_BUCKET not set");
492
493 let region = crate::types::region::Region::from_str(®ion_str).unwrap_or_else(|_| {
494 crate::types::region::Region::Custom {
495 endpoint: format!("oss-{}.aliyuncs.com", region_str),
496 region_id: region_str.clone(),
497 }
498 });
499
500 let client = crate::client::OSSClient::builder()
501 .region(region)
502 .credentials(ak, sk)
503 .build()
504 .unwrap();
505
506 let output = client
507 .bucket(&bucket_str)
508 .unwrap()
509 .get_info()
510 .send()
511 .await
512 .unwrap();
513
514 assert_eq!(output.bucket.name, bucket_str);
515 eprintln!(
516 "GetBucketInfo: name={}, location={}, storage={}",
517 output.bucket.name, output.bucket.location, output.bucket.storage_class
518 );
519 }
520}