1use std::sync::Arc;
4
5use crate::client::{BucketOperations, OSSClientInner};
6use crate::error::{ErrorContext, OssError, OssErrorKind, Result};
7use crate::http::client::{HttpRequest, HttpResponse};
8use crate::types::acl::ObjectAcl;
9use crate::types::bucket::BucketName;
10use crate::types::object::ObjectKey;
11use crate::types::storage::{ServerSideEncryption, StorageClass};
12use crate::util::uri::oss_endpoint_url;
13
14pub struct PutObjectBuilder {
15 client: Arc<OSSClientInner>,
16 bucket: BucketName,
17 key: ObjectKey,
18 body: Option<bytes::Bytes>,
19 content_type: Option<String>,
20 content_md5: Option<String>,
21 cache_control: Option<String>,
22 content_disposition: Option<String>,
23 content_encoding: Option<String>,
24 expires: Option<String>,
25 acl: Option<ObjectAcl>,
26 storage_class: Option<StorageClass>,
27 sse: Option<ServerSideEncryption>,
28 sse_key_id: Option<String>,
29 tagging: Option<String>,
30 metadata: Vec<(String, String)>,
31}
32
33impl PutObjectBuilder {
34 pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName, key: ObjectKey) -> Self {
35 Self {
36 client,
37 bucket,
38 key,
39 body: None,
40 content_type: None,
41 content_md5: None,
42 cache_control: None,
43 content_disposition: None,
44 content_encoding: None,
45 expires: None,
46 acl: None,
47 storage_class: None,
48 sse: None,
49 sse_key_id: None,
50 tagging: None,
51 metadata: Vec::new(),
52 }
53 }
54
55 pub fn body(mut self, body: impl Into<bytes::Bytes>) -> Self {
56 self.body = Some(body.into());
57 self
58 }
59
60 pub fn content_type(mut self, ct: impl Into<String>) -> Self {
61 self.content_type = Some(ct.into());
62 self
63 }
64
65 pub fn content_md5(mut self, md5: impl Into<String>) -> Self {
66 self.content_md5 = Some(md5.into());
67 self
68 }
69
70 pub fn cache_control(mut self, cc: impl Into<String>) -> Self {
71 self.cache_control = Some(cc.into());
72 self
73 }
74
75 pub fn content_disposition(mut self, cd: impl Into<String>) -> Self {
76 self.content_disposition = Some(cd.into());
77 self
78 }
79
80 pub fn content_encoding(mut self, ce: impl Into<String>) -> Self {
81 self.content_encoding = Some(ce.into());
82 self
83 }
84
85 pub fn expires(mut self, exp: impl Into<String>) -> Self {
86 self.expires = Some(exp.into());
87 self
88 }
89
90 pub fn acl(mut self, acl: ObjectAcl) -> Self {
91 self.acl = Some(acl);
92 self
93 }
94
95 pub fn storage_class(mut self, sc: StorageClass) -> Self {
96 self.storage_class = Some(sc);
97 self
98 }
99
100 pub fn server_side_encryption(mut self, sse: impl Into<String>) -> Self {
101 match sse.into().as_str() {
102 "AES256" => self.sse = Some(ServerSideEncryption::AES256),
103 "KMS" => self.sse = Some(ServerSideEncryption::KMS),
104 _ => {}
105 }
106 self
107 }
108
109 pub fn sse_key_id(mut self, key_id: impl Into<String>) -> Self {
110 self.sse_key_id = Some(key_id.into());
111 self
112 }
113
114 pub fn tagging(mut self, tag: impl Into<String>) -> Self {
115 self.tagging = Some(tag.into());
116 self
117 }
118
119 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
120 self.metadata.push((key.into(), value.into()));
121 self
122 }
123
124 pub async fn send(self) -> Result<PutObjectOutput> {
125 let body = self.body.ok_or_else(|| OssError {
126 kind: OssErrorKind::ValidationError,
127 context: Box::new(ErrorContext {
128 operation: Some("PutObject: body is required".into()),
129 bucket: Some(self.bucket.to_string()),
130 object_key: Some(self.key.to_string()),
131 ..Default::default()
132 }),
133 source: None,
134 })?;
135
136 let endpoint = self.client.endpoint.clone();
137 let uri = oss_endpoint_url(
138 &endpoint,
139 Some(self.bucket.as_str()),
140 Some(self.key.as_str()),
141 );
142
143 let full_uri = uri;
144
145 let mut req = HttpRequest::builder()
146 .method(http::Method::PUT)
147 .uri(&full_uri);
148
149 if let Some(ref ct) = self.content_type {
150 req = req.header(
151 http::HeaderName::from_static("content-type"),
152 http::HeaderValue::from_str(ct).map_err(|e| OssError {
153 kind: OssErrorKind::ValidationError,
154 context: Box::new(ErrorContext {
155 operation: Some("set content-type header".into()),
156 bucket: Some(self.bucket.to_string()),
157 object_key: Some(self.key.to_string()),
158 ..Default::default()
159 }),
160 source: Some(Box::new(e)),
161 })?,
162 );
163 }
164
165 if let Some(ref md5) = self.content_md5 {
166 req = req.header(
167 http::HeaderName::from_static("content-md5"),
168 http::HeaderValue::from_str(md5).map_err(|e| OssError {
169 kind: OssErrorKind::ValidationError,
170 context: Box::new(ErrorContext {
171 operation: Some("set content-md5 header".into()),
172 bucket: Some(self.bucket.to_string()),
173 object_key: Some(self.key.to_string()),
174 ..Default::default()
175 }),
176 source: Some(Box::new(e)),
177 })?,
178 );
179 }
180
181 if let Some(ref cc) = self.cache_control {
182 req = req.header(
183 http::HeaderName::from_static("cache-control"),
184 http::HeaderValue::from_str(cc).map_err(|e| OssError {
185 kind: OssErrorKind::ValidationError,
186 context: Box::new(ErrorContext {
187 operation: Some("set cache-control header".into()),
188 bucket: Some(self.bucket.to_string()),
189 object_key: Some(self.key.to_string()),
190 ..Default::default()
191 }),
192 source: Some(Box::new(e)),
193 })?,
194 );
195 }
196
197 if let Some(ref cd) = self.content_disposition {
198 req = req.header(
199 http::HeaderName::from_static("content-disposition"),
200 http::HeaderValue::from_str(cd).map_err(|e| OssError {
201 kind: OssErrorKind::ValidationError,
202 context: Box::new(ErrorContext {
203 operation: Some("set content-disposition header".into()),
204 bucket: Some(self.bucket.to_string()),
205 object_key: Some(self.key.to_string()),
206 ..Default::default()
207 }),
208 source: Some(Box::new(e)),
209 })?,
210 );
211 }
212
213 if let Some(ref ce) = self.content_encoding {
214 req = req.header(
215 http::HeaderName::from_static("content-encoding"),
216 http::HeaderValue::from_str(ce).map_err(|e| OssError {
217 kind: OssErrorKind::ValidationError,
218 context: Box::new(ErrorContext {
219 operation: Some("set content-encoding header".into()),
220 bucket: Some(self.bucket.to_string()),
221 object_key: Some(self.key.to_string()),
222 ..Default::default()
223 }),
224 source: Some(Box::new(e)),
225 })?,
226 );
227 }
228
229 if let Some(ref exp) = self.expires {
230 req = req.header(
231 http::HeaderName::from_static("expires"),
232 http::HeaderValue::from_str(exp).map_err(|e| OssError {
233 kind: OssErrorKind::ValidationError,
234 context: Box::new(ErrorContext {
235 operation: Some("set expires header".into()),
236 bucket: Some(self.bucket.to_string()),
237 object_key: Some(self.key.to_string()),
238 ..Default::default()
239 }),
240 source: Some(Box::new(e)),
241 })?,
242 );
243 }
244
245 if let Some(acl) = self.acl {
246 req = req.header(
247 http::HeaderName::from_static("x-oss-object-acl"),
248 http::HeaderValue::from_str(acl.as_str()).map_err(|e| OssError {
249 kind: OssErrorKind::ValidationError,
250 context: Box::new(ErrorContext {
251 operation: Some("set x-oss-object-acl header".into()),
252 bucket: Some(self.bucket.to_string()),
253 object_key: Some(self.key.to_string()),
254 ..Default::default()
255 }),
256 source: Some(Box::new(e)),
257 })?,
258 );
259 }
260
261 if let Some(sc) = self.storage_class {
262 req = req.header(
263 http::HeaderName::from_static("x-oss-storage-class"),
264 http::HeaderValue::from_str(sc.as_str()).map_err(|e| OssError {
265 kind: OssErrorKind::ValidationError,
266 context: Box::new(ErrorContext {
267 operation: Some("set x-oss-storage-class header".into()),
268 bucket: Some(self.bucket.to_string()),
269 object_key: Some(self.key.to_string()),
270 ..Default::default()
271 }),
272 source: Some(Box::new(e)),
273 })?,
274 );
275 }
276
277 if let Some(ref sse) = self.sse {
278 req = req.header(
279 http::HeaderName::from_static("x-oss-server-side-encryption"),
280 http::HeaderValue::from_str(sse.as_str()).map_err(|e| OssError {
281 kind: OssErrorKind::ValidationError,
282 context: Box::new(ErrorContext {
283 operation: Some("set x-oss-server-side-encryption header".into()),
284 bucket: Some(self.bucket.to_string()),
285 object_key: Some(self.key.to_string()),
286 ..Default::default()
287 }),
288 source: Some(Box::new(e)),
289 })?,
290 );
291
292 if let Some(key_id) = sse.key_id() {
293 req = req.header(
294 http::HeaderName::from_static("x-oss-server-side-encryption-key-id"),
295 http::HeaderValue::from_str(key_id).map_err(|e| OssError {
296 kind: OssErrorKind::ValidationError,
297 context: Box::new(ErrorContext {
298 operation: Some(
299 "set x-oss-server-side-encryption-key-id header".into(),
300 ),
301 bucket: Some(self.bucket.to_string()),
302 object_key: Some(self.key.to_string()),
303 ..Default::default()
304 }),
305 source: Some(Box::new(e)),
306 })?,
307 );
308 }
309 } else if let Some(ref key_id) = self.sse_key_id {
310 req = req.header(
311 http::HeaderName::from_static("x-oss-server-side-encryption"),
312 http::HeaderValue::from_str("KMS").map_err(|e| OssError {
313 kind: OssErrorKind::ValidationError,
314 context: Box::new(ErrorContext {
315 operation: Some("set x-oss-server-side-encryption header".into()),
316 bucket: Some(self.bucket.to_string()),
317 object_key: Some(self.key.to_string()),
318 ..Default::default()
319 }),
320 source: Some(Box::new(e)),
321 })?,
322 );
323 req = req.header(
324 http::HeaderName::from_static("x-oss-server-side-encryption-key-id"),
325 http::HeaderValue::from_str(key_id).map_err(|e| OssError {
326 kind: OssErrorKind::ValidationError,
327 context: Box::new(ErrorContext {
328 operation: Some("set x-oss-server-side-encryption-key-id header".into()),
329 bucket: Some(self.bucket.to_string()),
330 object_key: Some(self.key.to_string()),
331 ..Default::default()
332 }),
333 source: Some(Box::new(e)),
334 })?,
335 );
336 }
337
338 if let Some(ref tag) = self.tagging {
339 req = req.header(
340 http::HeaderName::from_static("x-oss-tagging"),
341 http::HeaderValue::from_str(tag).map_err(|e| OssError {
342 kind: OssErrorKind::ValidationError,
343 context: Box::new(ErrorContext {
344 operation: Some("set x-oss-tagging header".into()),
345 bucket: Some(self.bucket.to_string()),
346 object_key: Some(self.key.to_string()),
347 ..Default::default()
348 }),
349 source: Some(Box::new(e)),
350 })?,
351 );
352 }
353
354 for (k, v) in &self.metadata {
355 let header_name = http::HeaderName::from_bytes(k.as_bytes()).map_err(|e| OssError {
356 kind: OssErrorKind::ValidationError,
357 context: Box::new(ErrorContext {
358 operation: Some(format!("set metadata header '{}'", k)),
359 bucket: Some(self.bucket.to_string()),
360 object_key: Some(self.key.to_string()),
361 ..Default::default()
362 }),
363 source: Some(Box::new(e)),
364 })?;
365 req = req.header(
366 header_name,
367 http::HeaderValue::from_str(v).map_err(|e| OssError {
368 kind: OssErrorKind::ValidationError,
369 context: Box::new(ErrorContext {
370 operation: Some(format!("set metadata header value '{}'", k)),
371 bucket: Some(self.bucket.to_string()),
372 object_key: Some(self.key.to_string()),
373 ..Default::default()
374 }),
375 source: Some(Box::new(e)),
376 })?,
377 );
378 }
379
380 let request = req.body(body).build();
381
382 let response = self
383 .client
384 .send_signed(request, Some(&self.bucket), Vec::new())
385 .await
386 .map_err(|e| OssError {
387 kind: OssErrorKind::TransportError,
388 context: Box::new(ErrorContext {
389 operation: Some("PutObject".into()),
390 bucket: Some(self.bucket.to_string()),
391 object_key: Some(self.key.to_string()),
392 endpoint: Some(endpoint),
393 ..Default::default()
394 }),
395 source: Some(Box::new(e)),
396 })?;
397
398 if response.is_success() {
399 let request_id = response
400 .headers
401 .get("x-oss-request-id")
402 .and_then(|v| v.to_str().ok())
403 .unwrap_or("")
404 .to_string();
405
406 let etag = response
407 .headers
408 .get("ETag")
409 .or_else(|| response.headers.get("etag"))
410 .and_then(|v| v.to_str().ok())
411 .map(|s| s.trim_matches('"').to_string())
412 .unwrap_or_default();
413
414 let version_id = response
415 .headers
416 .get("x-oss-version-id")
417 .and_then(|v| v.to_str().ok())
418 .map(|s| s.to_string());
419
420 let hash_crc64 = response
421 .headers
422 .get("x-oss-hash-crc64ecma")
423 .and_then(|v| v.to_str().ok())
424 .map(|s| s.to_string());
425
426 let result_sse = response
427 .headers
428 .get("x-oss-server-side-encryption")
429 .and_then(|v| v.to_str().ok())
430 .map(|s| s.to_string());
431
432 Ok(PutObjectOutput {
433 request_id,
434 etag,
435 version_id,
436 hash_crc64,
437 sse: result_sse,
438 })
439 } else {
440 Err(OssError {
441 kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
442 status_code: response.status().as_u16(),
443 code: String::new(),
444 message: String::new(),
445 request_id: String::new(),
446 host_id: String::new(),
447 resource: Some(self.key.to_string()),
448 string_to_sign: None,
449 })),
450 context: Box::new(ErrorContext {
451 operation: Some("PutObject".into()),
452 bucket: Some(self.bucket.to_string()),
453 object_key: Some(self.key.to_string()),
454 ..Default::default()
455 }),
456 source: None,
457 })
458 }
459 }
460}
461
462#[derive(Debug, Clone)]
463pub struct PutObjectOutput {
464 pub request_id: String,
465 pub etag: String,
466 pub version_id: Option<String>,
467 pub hash_crc64: Option<String>,
468 pub sse: Option<String>,
469}
470
471impl BucketOperations {
472 pub fn put_object(&self, key: impl Into<String>) -> Result<PutObjectBuilder> {
473 let object_key = ObjectKey::new(key.into())?;
474 Ok(PutObjectBuilder::new(
475 self.client_inner().clone(),
476 self.bucket_name().clone(),
477 object_key,
478 ))
479 }
480
481 pub fn get_object(&self, key: impl Into<String>) -> Result<GetObjectBuilder> {
482 let object_key = ObjectKey::new(key.into())?;
483 Ok(GetObjectBuilder::new(
484 self.client_inner().clone(),
485 self.bucket_name().clone(),
486 object_key,
487 ))
488 }
489
490 pub fn head_object(&self, key: impl Into<String>) -> Result<HeadObjectBuilder> {
491 let object_key = ObjectKey::new(key.into())?;
492 Ok(HeadObjectBuilder::new(
493 self.client_inner().clone(),
494 self.bucket_name().clone(),
495 object_key,
496 ))
497 }
498
499 pub fn delete_object(&self, key: impl Into<String>) -> Result<DeleteObjectBuilder> {
500 let object_key = ObjectKey::new(key.into())?;
501 Ok(DeleteObjectBuilder::new(
502 self.client_inner().clone(),
503 self.bucket_name().clone(),
504 object_key,
505 ))
506 }
507
508 pub fn get_object_meta(&self, key: impl Into<String>) -> Result<GetObjectMetaBuilder> {
509 let object_key = ObjectKey::new(key.into())?;
510 Ok(GetObjectMetaBuilder::new(
511 self.client_inner().clone(),
512 self.bucket_name().clone(),
513 object_key,
514 ))
515 }
516
517 pub fn delete_multiple_objects(&self, keys: Vec<String>) -> DeleteMultipleObjectsBuilder {
518 DeleteMultipleObjectsBuilder::new(
519 self.client_inner().clone(),
520 self.bucket_name().clone(),
521 keys,
522 )
523 }
524
525 pub fn process_object(
526 &self,
527 key: impl Into<String>,
528 style: impl Into<String>,
529 ) -> Result<GetObjectBuilder> {
530 let object_key = ObjectKey::new(key.into())?;
531 Ok(GetObjectBuilder::new(
532 self.client_inner().clone(),
533 self.bucket_name().clone(),
534 object_key,
535 )
536 .process(style))
537 }
538}
539
540pub struct GetObjectBuilder {
541 client: Arc<OSSClientInner>,
542 bucket: BucketName,
543 key: ObjectKey,
544 range: Option<String>,
545 if_match: Option<String>,
546 if_none_match: Option<String>,
547 if_modified_since: Option<String>,
548 if_unmodified_since: Option<String>,
549 response_content_type: Option<String>,
550 response_content_encoding: Option<String>,
551 response_cache_control: Option<String>,
552 response_content_disposition: Option<String>,
553 response_content_language: Option<String>,
554 response_expires: Option<String>,
555 version_id: Option<String>,
556 process: Option<String>,
557}
558
559impl GetObjectBuilder {
560 pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName, key: ObjectKey) -> Self {
561 Self {
562 client,
563 bucket,
564 key,
565 range: None,
566 if_match: None,
567 if_none_match: None,
568 if_modified_since: None,
569 if_unmodified_since: None,
570 response_content_type: None,
571 response_content_encoding: None,
572 response_cache_control: None,
573 response_content_disposition: None,
574 response_content_language: None,
575 response_expires: None,
576 version_id: None,
577 process: None,
578 }
579 }
580
581 pub fn range(mut self, range: impl Into<String>) -> Self {
582 self.range = Some(range.into());
583 self
584 }
585
586 pub fn if_match(mut self, etag: impl Into<String>) -> Self {
587 self.if_match = Some(etag.into());
588 self
589 }
590
591 pub fn if_none_match(mut self, etag: impl Into<String>) -> Self {
592 self.if_none_match = Some(etag.into());
593 self
594 }
595
596 pub fn if_modified_since(mut self, time: impl Into<String>) -> Self {
597 self.if_modified_since = Some(time.into());
598 self
599 }
600
601 pub fn if_unmodified_since(mut self, time: impl Into<String>) -> Self {
602 self.if_unmodified_since = Some(time.into());
603 self
604 }
605
606 pub fn response_content_type(mut self, ct: impl Into<String>) -> Self {
607 self.response_content_type = Some(ct.into());
608 self
609 }
610
611 pub fn response_content_encoding(mut self, ce: impl Into<String>) -> Self {
612 self.response_content_encoding = Some(ce.into());
613 self
614 }
615
616 pub fn response_cache_control(mut self, cc: impl Into<String>) -> Self {
617 self.response_cache_control = Some(cc.into());
618 self
619 }
620
621 pub fn response_content_disposition(mut self, cd: impl Into<String>) -> Self {
622 self.response_content_disposition = Some(cd.into());
623 self
624 }
625
626 pub fn response_content_language(mut self, cl: impl Into<String>) -> Self {
627 self.response_content_language = Some(cl.into());
628 self
629 }
630
631 pub fn response_expires(mut self, exp: impl Into<String>) -> Self {
632 self.response_expires = Some(exp.into());
633 self
634 }
635
636 pub fn process(mut self, process: impl Into<String>) -> Self {
637 self.process = Some(process.into());
638 self
639 }
640
641 pub fn version_id(mut self, id: impl Into<String>) -> Self {
642 self.version_id = Some(id.into());
643 self
644 }
645
646 pub async fn send(self) -> Result<GetObjectOutput> {
647 let endpoint = self.client.endpoint.clone();
648 let uri = oss_endpoint_url(
649 &endpoint,
650 Some(self.bucket.as_str()),
651 Some(self.key.as_str()),
652 );
653
654 let mut query_pairs: Vec<(String, String)> = Vec::new();
655
656 if let Some(ref ct) = self.response_content_type {
657 query_pairs.push(("response-content-type".into(), ct.clone()));
658 }
659 if let Some(ref ce) = self.response_content_encoding {
660 query_pairs.push(("response-content-encoding".into(), ce.clone()));
661 }
662 if let Some(ref cc) = self.response_cache_control {
663 query_pairs.push(("response-cache-control".into(), cc.clone()));
664 }
665 if let Some(ref cd) = self.response_content_disposition {
666 query_pairs.push(("response-content-disposition".into(), cd.clone()));
667 }
668 if let Some(ref cl) = self.response_content_language {
669 query_pairs.push(("response-content-language".into(), cl.clone()));
670 }
671 if let Some(ref exp) = self.response_expires {
672 query_pairs.push(("response-expires".into(), exp.clone()));
673 }
674 if let Some(ref vid) = self.version_id {
675 query_pairs.push(("versionId".into(), vid.clone()));
676 }
677 if let Some(ref proc) = self.process {
678 query_pairs.push(("x-oss-process".into(), proc.clone()));
679 }
680
681 let query_string = if query_pairs.is_empty() {
682 String::new()
683 } else {
684 let parts: Vec<String> = query_pairs
685 .iter()
686 .map(|(k, v)| {
687 format!(
688 "{}={}",
689 crate::util::uri::uri_encode(k),
690 crate::util::uri::uri_encode(v)
691 )
692 })
693 .collect();
694 format!("?{}", parts.join("&"))
695 };
696
697 let full_uri = format!("{}{}", uri, query_string);
698
699 let mut req = HttpRequest::builder()
700 .method(http::Method::GET)
701 .uri(&full_uri);
702
703 if let Some(ref range) = self.range {
704 req = req.header(
705 http::HeaderName::from_static("range"),
706 http::HeaderValue::from_str(range).map_err(|e| OssError {
707 kind: OssErrorKind::ValidationError,
708 context: Box::new(ErrorContext {
709 operation: Some("set range header".into()),
710 bucket: Some(self.bucket.to_string()),
711 object_key: Some(self.key.to_string()),
712 ..Default::default()
713 }),
714 source: Some(Box::new(e)),
715 })?,
716 );
717 }
718
719 if let Some(ref im) = self.if_match {
720 req = req.header(
721 http::HeaderName::from_static("if-match"),
722 http::HeaderValue::from_str(im).map_err(|e| OssError {
723 kind: OssErrorKind::ValidationError,
724 context: Box::new(ErrorContext {
725 operation: Some("set if-match header".into()),
726 bucket: Some(self.bucket.to_string()),
727 object_key: Some(self.key.to_string()),
728 ..Default::default()
729 }),
730 source: Some(Box::new(e)),
731 })?,
732 );
733 }
734
735 if let Some(ref inm) = self.if_none_match {
736 req = req.header(
737 http::HeaderName::from_static("if-none-match"),
738 http::HeaderValue::from_str(inm).map_err(|e| OssError {
739 kind: OssErrorKind::ValidationError,
740 context: Box::new(ErrorContext {
741 operation: Some("set if-none-match header".into()),
742 bucket: Some(self.bucket.to_string()),
743 object_key: Some(self.key.to_string()),
744 ..Default::default()
745 }),
746 source: Some(Box::new(e)),
747 })?,
748 );
749 }
750
751 if let Some(ref ims) = self.if_modified_since {
752 req = req.header(
753 http::HeaderName::from_static("if-modified-since"),
754 http::HeaderValue::from_str(ims).map_err(|e| OssError {
755 kind: OssErrorKind::ValidationError,
756 context: Box::new(ErrorContext {
757 operation: Some("set if-modified-since header".into()),
758 bucket: Some(self.bucket.to_string()),
759 object_key: Some(self.key.to_string()),
760 ..Default::default()
761 }),
762 source: Some(Box::new(e)),
763 })?,
764 );
765 }
766
767 if let Some(ref ius) = self.if_unmodified_since {
768 req = req.header(
769 http::HeaderName::from_static("if-unmodified-since"),
770 http::HeaderValue::from_str(ius).map_err(|e| OssError {
771 kind: OssErrorKind::ValidationError,
772 context: Box::new(ErrorContext {
773 operation: Some("set if-unmodified-since header".into()),
774 bucket: Some(self.bucket.to_string()),
775 object_key: Some(self.key.to_string()),
776 ..Default::default()
777 }),
778 source: Some(Box::new(e)),
779 })?,
780 );
781 }
782
783 let request = req.build();
784
785 let response = self
786 .client
787 .send_signed(request, Some(&self.bucket), query_pairs)
788 .await
789 .map_err(|e| OssError {
790 kind: OssErrorKind::TransportError,
791 context: Box::new(ErrorContext {
792 operation: Some("GetObject".into()),
793 bucket: Some(self.bucket.to_string()),
794 object_key: Some(self.key.to_string()),
795 endpoint: Some(endpoint),
796 ..Default::default()
797 }),
798 source: Some(Box::new(e)),
799 })?;
800
801 if response.is_success() {
802 let request_id = response
803 .headers
804 .get("x-oss-request-id")
805 .and_then(|v| v.to_str().ok())
806 .unwrap_or("")
807 .to_string();
808
809 let content_type = response
810 .headers
811 .get("content-type")
812 .and_then(|v| v.to_str().ok())
813 .map(|s| s.to_string());
814
815 let content_length = response
816 .headers
817 .get("content-length")
818 .and_then(|v| v.to_str().ok())
819 .and_then(|s| s.parse::<u64>().ok());
820
821 let etag = response
822 .headers
823 .get("ETag")
824 .or_else(|| response.headers.get("etag"))
825 .and_then(|v| v.to_str().ok())
826 .map(|s| s.trim_matches('"').to_string());
827
828 let last_modified = response
829 .headers
830 .get("last-modified")
831 .and_then(|v| v.to_str().ok())
832 .map(|s| s.to_string());
833
834 let storage_class = response
835 .headers
836 .get("x-oss-storage-class")
837 .and_then(|v| v.to_str().ok())
838 .map(|s| s.to_string());
839
840 let object_type = response
841 .headers
842 .get("x-oss-object-type")
843 .and_then(|v| v.to_str().ok())
844 .map(|s| s.to_string());
845
846 let mut metadata: Vec<(String, String)> = Vec::new();
847 for (name, value) in response.headers.iter() {
848 let name_str = name.as_str().to_lowercase();
849 if name_str.starts_with("x-oss-meta-")
850 && let Ok(v) = value.to_str()
851 {
852 metadata.push((name.as_str().to_string(), v.to_string()));
853 }
854 }
855
856 Ok(GetObjectOutput {
857 request_id,
858 body: response.body,
859 content_type,
860 content_length,
861 etag,
862 last_modified,
863 metadata,
864 storage_class,
865 object_type,
866 })
867 } else {
868 Err(OssError {
869 kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
870 status_code: response.status().as_u16(),
871 code: String::new(),
872 message: String::new(),
873 request_id: String::new(),
874 host_id: String::new(),
875 resource: Some(self.key.to_string()),
876 string_to_sign: None,
877 })),
878 context: Box::new(ErrorContext {
879 operation: Some("GetObject".into()),
880 bucket: Some(self.bucket.to_string()),
881 object_key: Some(self.key.to_string()),
882 ..Default::default()
883 }),
884 source: None,
885 })
886 }
887 }
888}
889
890#[derive(Debug, Clone)]
891pub struct GetObjectOutput {
892 pub request_id: String,
893 pub body: bytes::Bytes,
894 pub content_type: Option<String>,
895 pub content_length: Option<u64>,
896 pub etag: Option<String>,
897 pub last_modified: Option<String>,
898 pub metadata: Vec<(String, String)>,
899 pub storage_class: Option<String>,
900 pub object_type: Option<String>,
901}
902
903pub struct HeadObjectBuilder {
904 client: Arc<OSSClientInner>,
905 bucket: BucketName,
906 key: ObjectKey,
907 if_match: Option<String>,
908 if_none_match: Option<String>,
909 if_modified_since: Option<String>,
910 if_unmodified_since: Option<String>,
911 version_id: Option<String>,
912}
913
914impl HeadObjectBuilder {
915 pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName, key: ObjectKey) -> Self {
916 Self {
917 client,
918 bucket,
919 key,
920 if_match: None,
921 if_none_match: None,
922 if_modified_since: None,
923 if_unmodified_since: None,
924 version_id: None,
925 }
926 }
927
928 pub fn if_match(mut self, etag: impl Into<String>) -> Self {
929 self.if_match = Some(etag.into());
930 self
931 }
932
933 pub fn if_none_match(mut self, etag: impl Into<String>) -> Self {
934 self.if_none_match = Some(etag.into());
935 self
936 }
937
938 pub fn if_modified_since(mut self, time: impl Into<String>) -> Self {
939 self.if_modified_since = Some(time.into());
940 self
941 }
942
943 pub fn if_unmodified_since(mut self, time: impl Into<String>) -> Self {
944 self.if_unmodified_since = Some(time.into());
945 self
946 }
947
948 pub fn version_id(mut self, id: impl Into<String>) -> Self {
949 self.version_id = Some(id.into());
950 self
951 }
952
953 pub async fn send(self) -> Result<HeadObjectOutput> {
954 let endpoint = self.client.endpoint.clone();
955 let uri = oss_endpoint_url(
956 &endpoint,
957 Some(self.bucket.as_str()),
958 Some(self.key.as_str()),
959 );
960
961 let mut query_pairs: Vec<(String, String)> = Vec::new();
962 if let Some(ref vid) = self.version_id {
963 query_pairs.push(("versionId".into(), vid.clone()));
964 }
965
966 let query_string = if query_pairs.is_empty() {
967 String::new()
968 } else {
969 let parts: Vec<String> = query_pairs
970 .iter()
971 .map(|(k, v)| {
972 format!(
973 "{}={}",
974 crate::util::uri::uri_encode(k),
975 crate::util::uri::uri_encode(v)
976 )
977 })
978 .collect();
979 format!("?{}", parts.join("&"))
980 };
981 let full_uri = format!("{}{}", uri, query_string);
982
983 let mut req = HttpRequest::builder()
984 .method(http::Method::HEAD)
985 .uri(&full_uri);
986
987 if let Some(ref im) = self.if_match {
988 req = req.header(
989 http::HeaderName::from_static("if-match"),
990 http::HeaderValue::from_str(im).map_err(|e| OssError {
991 kind: OssErrorKind::ValidationError,
992 context: Box::new(ErrorContext {
993 operation: Some("set if-match header".into()),
994 bucket: Some(self.bucket.to_string()),
995 object_key: Some(self.key.to_string()),
996 ..Default::default()
997 }),
998 source: Some(Box::new(e)),
999 })?,
1000 );
1001 }
1002
1003 if let Some(ref inm) = self.if_none_match {
1004 req = req.header(
1005 http::HeaderName::from_static("if-none-match"),
1006 http::HeaderValue::from_str(inm).map_err(|e| OssError {
1007 kind: OssErrorKind::ValidationError,
1008 context: Box::new(ErrorContext {
1009 operation: Some("set if-none-match header".into()),
1010 bucket: Some(self.bucket.to_string()),
1011 object_key: Some(self.key.to_string()),
1012 ..Default::default()
1013 }),
1014 source: Some(Box::new(e)),
1015 })?,
1016 );
1017 }
1018
1019 if let Some(ref ims) = self.if_modified_since {
1020 req = req.header(
1021 http::HeaderName::from_static("if-modified-since"),
1022 http::HeaderValue::from_str(ims).map_err(|e| OssError {
1023 kind: OssErrorKind::ValidationError,
1024 context: Box::new(ErrorContext {
1025 operation: Some("set if-modified-since header".into()),
1026 bucket: Some(self.bucket.to_string()),
1027 object_key: Some(self.key.to_string()),
1028 ..Default::default()
1029 }),
1030 source: Some(Box::new(e)),
1031 })?,
1032 );
1033 }
1034
1035 if let Some(ref ius) = self.if_unmodified_since {
1036 req = req.header(
1037 http::HeaderName::from_static("if-unmodified-since"),
1038 http::HeaderValue::from_str(ius).map_err(|e| OssError {
1039 kind: OssErrorKind::ValidationError,
1040 context: Box::new(ErrorContext {
1041 operation: Some("set if-unmodified-since header".into()),
1042 bucket: Some(self.bucket.to_string()),
1043 object_key: Some(self.key.to_string()),
1044 ..Default::default()
1045 }),
1046 source: Some(Box::new(e)),
1047 })?,
1048 );
1049 }
1050
1051 let request = req.build();
1052
1053 let response = self
1054 .client
1055 .send_signed(request, Some(&self.bucket), query_pairs)
1056 .await
1057 .map_err(|e| OssError {
1058 kind: OssErrorKind::TransportError,
1059 context: Box::new(ErrorContext {
1060 operation: Some("HeadObject".into()),
1061 bucket: Some(self.bucket.to_string()),
1062 object_key: Some(self.key.to_string()),
1063 endpoint: Some(endpoint),
1064 ..Default::default()
1065 }),
1066 source: Some(Box::new(e)),
1067 })?;
1068
1069 if response.is_success() {
1070 Ok(HeadObjectOutput::from_response(&response))
1071 } else {
1072 Err(OssError {
1073 kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
1074 status_code: response.status().as_u16(),
1075 code: String::new(),
1076 message: String::new(),
1077 request_id: String::new(),
1078 host_id: String::new(),
1079 resource: Some(self.key.to_string()),
1080 string_to_sign: None,
1081 })),
1082 context: Box::new(ErrorContext {
1083 operation: Some("HeadObject".into()),
1084 bucket: Some(self.bucket.to_string()),
1085 object_key: Some(self.key.to_string()),
1086 ..Default::default()
1087 }),
1088 source: None,
1089 })
1090 }
1091 }
1092}
1093
1094#[derive(Debug, Clone)]
1095pub struct HeadObjectOutput {
1096 pub request_id: String,
1097 pub content_type: Option<String>,
1098 pub content_length: Option<u64>,
1099 pub etag: Option<String>,
1100 pub last_modified: Option<String>,
1101 pub metadata: Vec<(String, String)>,
1102 pub storage_class: Option<String>,
1103 pub object_type: Option<String>,
1104}
1105
1106impl HeadObjectOutput {
1107 fn from_response(response: &HttpResponse) -> Self {
1108 let request_id = response
1109 .headers
1110 .get("x-oss-request-id")
1111 .and_then(|v| v.to_str().ok())
1112 .unwrap_or("")
1113 .to_string();
1114
1115 let content_type = response
1116 .headers
1117 .get("content-type")
1118 .and_then(|v| v.to_str().ok())
1119 .map(|s| s.to_string());
1120
1121 let content_length = response
1122 .headers
1123 .get("content-length")
1124 .and_then(|v| v.to_str().ok())
1125 .and_then(|s| s.parse::<u64>().ok());
1126
1127 let etag = response
1128 .headers
1129 .get("ETag")
1130 .or_else(|| response.headers.get("etag"))
1131 .and_then(|v| v.to_str().ok())
1132 .map(|s| s.trim_matches('"').to_string());
1133
1134 let last_modified = response
1135 .headers
1136 .get("last-modified")
1137 .and_then(|v| v.to_str().ok())
1138 .map(|s| s.to_string());
1139
1140 let storage_class = response
1141 .headers
1142 .get("x-oss-storage-class")
1143 .and_then(|v| v.to_str().ok())
1144 .map(|s| s.to_string());
1145
1146 let object_type = response
1147 .headers
1148 .get("x-oss-object-type")
1149 .and_then(|v| v.to_str().ok())
1150 .map(|s| s.to_string());
1151
1152 let mut metadata: Vec<(String, String)> = Vec::new();
1153 for (name, value) in response.headers.iter() {
1154 let name_str = name.as_str().to_lowercase();
1155 if name_str.starts_with("x-oss-meta-")
1156 && let Ok(v) = value.to_str()
1157 {
1158 metadata.push((name.as_str().to_string(), v.to_string()));
1159 }
1160 }
1161
1162 Self {
1163 request_id,
1164 content_type,
1165 content_length,
1166 etag,
1167 last_modified,
1168 metadata,
1169 storage_class,
1170 object_type,
1171 }
1172 }
1173}
1174
1175pub struct DeleteObjectBuilder {
1176 client: Arc<OSSClientInner>,
1177 bucket: BucketName,
1178 key: ObjectKey,
1179 version_id: Option<String>,
1180}
1181
1182impl DeleteObjectBuilder {
1183 pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName, key: ObjectKey) -> Self {
1184 Self {
1185 client,
1186 bucket,
1187 key,
1188 version_id: None,
1189 }
1190 }
1191
1192 pub fn version_id(mut self, id: impl Into<String>) -> Self {
1193 self.version_id = Some(id.into());
1194 self
1195 }
1196
1197 pub async fn send(self) -> Result<DeleteObjectOutput> {
1198 let endpoint = self.client.endpoint.clone();
1199 let uri = oss_endpoint_url(
1200 &endpoint,
1201 Some(self.bucket.as_str()),
1202 Some(self.key.as_str()),
1203 );
1204
1205 let mut query_pairs: Vec<(String, String)> = Vec::new();
1206 if let Some(ref vid) = self.version_id {
1207 query_pairs.push(("versionId".into(), vid.clone()));
1208 }
1209
1210 let query_string = if query_pairs.is_empty() {
1211 String::new()
1212 } else {
1213 let parts: Vec<String> = query_pairs
1214 .iter()
1215 .map(|(k, v)| {
1216 format!(
1217 "{}={}",
1218 crate::util::uri::uri_encode(k),
1219 crate::util::uri::uri_encode(v)
1220 )
1221 })
1222 .collect();
1223 format!("?{}", parts.join("&"))
1224 };
1225 let full_uri = format!("{}{}", uri, query_string);
1226
1227 let request = HttpRequest::builder()
1228 .method(http::Method::DELETE)
1229 .uri(&full_uri)
1230 .build();
1231
1232 let response = self
1233 .client
1234 .send_signed(request, Some(&self.bucket), query_pairs)
1235 .await
1236 .map_err(|e| OssError {
1237 kind: OssErrorKind::TransportError,
1238 context: Box::new(ErrorContext {
1239 operation: Some("DeleteObject".into()),
1240 bucket: Some(self.bucket.to_string()),
1241 object_key: Some(self.key.to_string()),
1242 endpoint: Some(endpoint),
1243 ..Default::default()
1244 }),
1245 source: Some(Box::new(e)),
1246 })?;
1247
1248 if response.status().is_success() {
1249 let request_id = response
1250 .headers
1251 .get("x-oss-request-id")
1252 .and_then(|v| v.to_str().ok())
1253 .unwrap_or("")
1254 .to_string();
1255
1256 Ok(DeleteObjectOutput { request_id })
1257 } else {
1258 Err(OssError {
1259 kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
1260 status_code: response.status().as_u16(),
1261 code: String::new(),
1262 message: String::new(),
1263 request_id: String::new(),
1264 host_id: String::new(),
1265 resource: Some(self.key.to_string()),
1266 string_to_sign: None,
1267 })),
1268 context: Box::new(ErrorContext {
1269 operation: Some("DeleteObject".into()),
1270 bucket: Some(self.bucket.to_string()),
1271 object_key: Some(self.key.to_string()),
1272 ..Default::default()
1273 }),
1274 source: None,
1275 })
1276 }
1277 }
1278}
1279
1280#[derive(Debug, Clone)]
1281pub struct DeleteObjectOutput {
1282 pub request_id: String,
1283}
1284
1285pub struct GetObjectMetaBuilder {
1286 client: Arc<OSSClientInner>,
1287 bucket: BucketName,
1288 key: ObjectKey,
1289 version_id: Option<String>,
1290}
1291
1292impl GetObjectMetaBuilder {
1293 pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName, key: ObjectKey) -> Self {
1294 Self {
1295 client,
1296 bucket,
1297 key,
1298 version_id: None,
1299 }
1300 }
1301
1302 pub fn version_id(mut self, id: impl Into<String>) -> Self {
1303 self.version_id = Some(id.into());
1304 self
1305 }
1306
1307 pub async fn send(self) -> Result<HeadObjectOutput> {
1308 let endpoint = self.client.endpoint.clone();
1309 let uri = oss_endpoint_url(
1310 &endpoint,
1311 Some(self.bucket.as_str()),
1312 Some(self.key.as_str()),
1313 );
1314
1315 let query_pairs: Vec<(String, String)> = vec![("objectMeta".into(), String::new())];
1316
1317 let full_uri = format!("{}?objectMeta", uri);
1318
1319 let request = HttpRequest::builder()
1320 .method(http::Method::HEAD)
1321 .uri(&full_uri)
1322 .build();
1323
1324 let response = self
1325 .client
1326 .send_signed(request, Some(&self.bucket), query_pairs)
1327 .await
1328 .map_err(|e| OssError {
1329 kind: OssErrorKind::TransportError,
1330 context: Box::new(ErrorContext {
1331 operation: Some("GetObjectMeta".into()),
1332 bucket: Some(self.bucket.to_string()),
1333 object_key: Some(self.key.to_string()),
1334 endpoint: Some(endpoint),
1335 ..Default::default()
1336 }),
1337 source: Some(Box::new(e)),
1338 })?;
1339
1340 if response.is_success() {
1341 Ok(HeadObjectOutput::from_response(&response))
1342 } else {
1343 Err(OssError {
1344 kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
1345 status_code: response.status().as_u16(),
1346 code: String::new(),
1347 message: String::new(),
1348 request_id: String::new(),
1349 host_id: String::new(),
1350 resource: Some(self.key.to_string()),
1351 string_to_sign: None,
1352 })),
1353 context: Box::new(ErrorContext {
1354 operation: Some("GetObjectMeta".into()),
1355 bucket: Some(self.bucket.to_string()),
1356 object_key: Some(self.key.to_string()),
1357 ..Default::default()
1358 }),
1359 source: None,
1360 })
1361 }
1362 }
1363}
1364
1365pub struct DeleteMultipleObjectsBuilder {
1366 client: Arc<OSSClientInner>,
1367 bucket: BucketName,
1368 keys: Vec<String>,
1369 quiet: bool,
1370}
1371
1372impl DeleteMultipleObjectsBuilder {
1373 pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName, keys: Vec<String>) -> Self {
1374 Self {
1375 client,
1376 bucket,
1377 keys,
1378 quiet: false,
1379 }
1380 }
1381
1382 pub fn quiet(mut self, quiet: bool) -> Self {
1383 self.quiet = quiet;
1384 self
1385 }
1386
1387 pub async fn send(self) -> Result<crate::types::response::DeleteResult> {
1388 let endpoint = self.client.endpoint.clone();
1389 let uri = format!("https://{}.{}/?delete", self.bucket.as_str(), endpoint);
1390
1391 let mut objects_xml = String::new();
1392 for key in &self.keys {
1393 objects_xml.push_str(&format!("<Object><Key>{}</Key></Object>", key));
1394 }
1395 let body_xml = format!(
1396 r#"<?xml version="1.0" encoding="UTF-8"?><Delete><Quiet>{}</Quiet>{}</Delete>"#,
1397 self.quiet, objects_xml
1398 );
1399
1400 let query_pairs: Vec<(String, String)> = vec![("delete".into(), String::new())];
1401
1402 let request = HttpRequest::builder()
1403 .method(http::Method::POST)
1404 .uri(&uri)
1405 .body(bytes::Bytes::from(body_xml))
1406 .build();
1407
1408 let response = self
1409 .client
1410 .send_signed(request, Some(&self.bucket), query_pairs)
1411 .await
1412 .map_err(|e| OssError {
1413 kind: OssErrorKind::TransportError,
1414 context: Box::new(ErrorContext {
1415 operation: Some("DeleteMultipleObjects".into()),
1416 bucket: Some(self.bucket.to_string()),
1417 endpoint: Some(endpoint),
1418 ..Default::default()
1419 }),
1420 source: Some(Box::new(e)),
1421 })?;
1422
1423 if response.is_success() {
1424 let body_str = response.body_as_str().unwrap_or("");
1425 Ok(crate::util::xml::from_xml(body_str)?)
1426 } else {
1427 Err(OssError {
1428 kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
1429 status_code: response.status().as_u16(),
1430 code: String::new(),
1431 message: String::new(),
1432 request_id: String::new(),
1433 host_id: String::new(),
1434 resource: Some(self.bucket.to_string()),
1435 string_to_sign: None,
1436 })),
1437 context: Box::new(ErrorContext {
1438 operation: Some("DeleteMultipleObjects".into()),
1439 bucket: Some(self.bucket.to_string()),
1440 ..Default::default()
1441 }),
1442 source: None,
1443 })
1444 }
1445 }
1446}
1447
1448#[cfg(test)]
1449mod tests {
1450 use std::str::FromStr;
1451 use std::sync::Mutex;
1452
1453 use http::HeaderMap;
1454
1455 use crate::client::OSSClientInner;
1456 use crate::config::credentials::Credentials;
1457 use crate::http::client::{HttpClient, HttpRequest, HttpResponse};
1458 use crate::operations::object_list::{ListObjectsBuilder, ListObjectsV2Builder};
1459 use crate::types::region::Region;
1460
1461 use super::*;
1462
1463 struct RecordingHttpClient {
1464 requests: Arc<Mutex<Vec<HttpRequest>>>,
1465 status_code: http::StatusCode,
1466 response_body: bytes::Bytes,
1467 response_headers: Vec<(&'static str, &'static str)>,
1468 }
1469
1470 #[async_trait::async_trait]
1471 impl HttpClient for RecordingHttpClient {
1472 async fn send(&self, request: HttpRequest) -> crate::error::Result<HttpResponse> {
1473 self.requests.lock().unwrap().push(request);
1474 let mut headers = HeaderMap::new();
1475 headers.insert(
1476 "x-oss-request-id",
1477 http::HeaderValue::from_static("rid-001"),
1478 );
1479 headers.insert("ETag", http::HeaderValue::from_static("\"abc123\""));
1480 for (name, value) in &self.response_headers {
1481 if let (Ok(n), Ok(v)) = (
1482 http::HeaderName::from_bytes(name.as_bytes()),
1483 http::HeaderValue::from_str(value),
1484 ) {
1485 headers.insert(n, v);
1486 }
1487 }
1488 Ok(HttpResponse {
1489 status: self.status_code,
1490 headers,
1491 body: self.response_body.clone(),
1492 })
1493 }
1494 }
1495
1496 fn create_test_inner() -> (Arc<OSSClientInner>, Arc<Mutex<Vec<HttpRequest>>>) {
1497 create_test_inner_with_response(http::StatusCode::OK, bytes::Bytes::new(), vec![])
1498 }
1499
1500 fn create_test_inner_with_response(
1501 status_code: http::StatusCode,
1502 response_body: bytes::Bytes,
1503 response_headers: Vec<(&'static str, &'static str)>,
1504 ) -> (Arc<OSSClientInner>, Arc<Mutex<Vec<HttpRequest>>>) {
1505 let requests = Arc::new(Mutex::new(Vec::new()));
1506 let http = Arc::new(RecordingHttpClient {
1507 requests: requests.clone(),
1508 status_code,
1509 response_body,
1510 response_headers,
1511 });
1512 let credentials = Arc::new(crate::config::credentials::StaticCredentialsProvider::new(
1513 Credentials::builder()
1514 .access_key_id("test-ak")
1515 .access_key_secret("test-sk")
1516 .build()
1517 .unwrap(),
1518 ));
1519 let inner = Arc::new(OSSClientInner {
1520 http,
1521 credentials,
1522 signer: Arc::from(crate::signer::create_signer(crate::signer::SignVersion::V4)),
1523 region: Region::CnHangzhou,
1524 endpoint: "oss-cn-hangzhou.aliyuncs.com".into(),
1525 });
1526 (inner, requests)
1527 }
1528
1529 fn test_bucket() -> BucketName {
1530 BucketName::new("test-bucket").unwrap()
1531 }
1532
1533 #[test]
1534 fn bucket_operations_put_object_rejects_empty_key() {
1535 let (inner, _) = create_test_inner();
1536 let ops = BucketOperations {
1537 client: inner,
1538 bucket: test_bucket(),
1539 };
1540 assert!(ops.put_object("").is_err());
1541 }
1542
1543 #[test]
1544 fn bucket_operations_put_object_rejects_overlength_key() {
1545 let (inner, _) = create_test_inner();
1546 let ops = BucketOperations {
1547 client: inner,
1548 bucket: test_bucket(),
1549 };
1550 assert!(ops.put_object("a".repeat(1025)).is_err());
1551 }
1552
1553 #[test]
1554 fn bucket_operations_put_object_accepts_valid_key() {
1555 let (inner, _) = create_test_inner();
1556 let ops = BucketOperations {
1557 client: inner,
1558 bucket: test_bucket(),
1559 };
1560 assert!(ops.put_object("valid-key.txt").is_ok());
1561 }
1562
1563 #[tokio::test]
1564 async fn list_objects_basic_request() {
1565 let list_xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1566<ListBucketResult>
1567 <Name>test-bucket</Name>
1568 <Prefix></Prefix>
1569 <MaxKeys>100</MaxKeys>
1570 <IsTruncated>false</IsTruncated>
1571</ListBucketResult>"#;
1572 let (inner, requests) = create_test_inner_with_response(
1573 http::StatusCode::OK,
1574 bytes::Bytes::from(list_xml),
1575 vec![],
1576 );
1577 let builder = ListObjectsBuilder::new(inner, BucketName::new("test-bucket").unwrap());
1578
1579 let output = builder.send().await.unwrap();
1580 assert_eq!(output.name, "test-bucket");
1581 assert!(!output.is_truncated);
1582
1583 let captured = requests.lock().unwrap();
1584 assert_eq!(captured[0].method, http::Method::GET);
1585 }
1586
1587 #[tokio::test]
1588 async fn list_objects_with_prefix_and_delimiter() {
1589 let (inner, requests) = create_test_inner_with_response(
1590 http::StatusCode::OK,
1591 bytes::Bytes::from(
1592 r#"<?xml version="1.0" encoding="UTF-8"?><ListBucketResult><Name>b</Name><MaxKeys>100</MaxKeys><IsTruncated>false</IsTruncated></ListBucketResult>"#,
1593 ),
1594 vec![],
1595 );
1596 let builder = ListObjectsBuilder::new(inner, BucketName::new("test-bucket").unwrap());
1597
1598 builder
1599 .prefix("dir/")
1600 .delimiter("/")
1601 .max_keys(10)
1602 .send()
1603 .await
1604 .unwrap();
1605
1606 let captured = requests.lock().unwrap();
1607 assert!(captured[0].uri.contains("prefix=dir/"));
1608 assert!(captured[0].uri.contains("delimiter=/"));
1609 assert!(captured[0].uri.contains("max-keys=10"));
1610 }
1611
1612 #[tokio::test]
1613 async fn list_objects_v2_basic_request() {
1614 let (inner, requests) = create_test_inner_with_response(
1615 http::StatusCode::OK,
1616 bytes::Bytes::from(
1617 r#"<?xml version="1.0" encoding="UTF-8"?><ListBucketResult><Name>b</Name><MaxKeys>100</MaxKeys><IsTruncated>false</IsTruncated><KeyCount>0</KeyCount></ListBucketResult>"#,
1618 ),
1619 vec![],
1620 );
1621 let builder = ListObjectsV2Builder::new(inner, BucketName::new("test-bucket").unwrap());
1622
1623 let output = builder.send().await.unwrap();
1624 assert!(!output.is_truncated);
1625
1626 let captured = requests.lock().unwrap();
1627 assert!(captured[0].uri.contains("list-type=2"));
1628 }
1629
1630 #[tokio::test]
1631 #[ignore = "requires valid OSS credentials"]
1632 async fn e2e_list_objects() {
1633 let ak = std::env::var("OSS_ACCESS_KEY_ID").expect("OSS_ACCESS_KEY_ID not set");
1634 let sk = std::env::var("OSS_ACCESS_KEY_SECRET").expect("OSS_ACCESS_KEY_SECRET not set");
1635 let region_str = std::env::var("OSS_REGION").unwrap_or_else(|_| "cn-wulanchabu".into());
1636 let bucket_str = std::env::var("OSS_BUCKET").expect("OSS_BUCKET not set");
1637
1638 let region = Region::from_str(®ion_str).unwrap_or_else(|_| Region::Custom {
1639 endpoint: format!("oss-{}.aliyuncs.com", region_str),
1640 region_id: region_str.clone(),
1641 });
1642
1643 let client = crate::client::OSSClient::builder()
1644 .region(region)
1645 .credentials(ak, sk)
1646 .build()
1647 .unwrap();
1648
1649 let output = client
1650 .bucket(&bucket_str)
1651 .unwrap()
1652 .list_objects()
1653 .max_keys(5)
1654 .send()
1655 .await
1656 .unwrap();
1657
1658 eprintln!(
1659 "LIST objects: {} objects, truncated={}",
1660 output.objects.len(),
1661 output.is_truncated
1662 );
1663 assert!(!output.name.is_empty());
1664 }
1665
1666 #[tokio::test]
1667 #[ignore = "requires valid OSS credentials"]
1668 async fn e2e_delete_object() {
1669 let ak = std::env::var("OSS_ACCESS_KEY_ID").expect("OSS_ACCESS_KEY_ID not set");
1670 let sk = std::env::var("OSS_ACCESS_KEY_SECRET").expect("OSS_ACCESS_KEY_SECRET not set");
1671 let region_str = std::env::var("OSS_REGION").unwrap_or_else(|_| "cn-wulanchabu".into());
1672 let bucket_str = std::env::var("OSS_BUCKET").expect("OSS_BUCKET not set");
1673
1674 let region = Region::from_str(®ion_str).unwrap_or_else(|_| Region::Custom {
1675 endpoint: format!("oss-{}.aliyuncs.com", region_str),
1676 region_id: region_str.clone(),
1677 });
1678
1679 let client = crate::client::OSSClient::builder()
1680 .region(region)
1681 .credentials(ak, sk)
1682 .build()
1683 .unwrap();
1684
1685 let key = format!("test-delete-{}.txt", chrono::Utc::now().timestamp());
1686 client
1687 .bucket(&bucket_str)
1688 .unwrap()
1689 .put_object(&key)
1690 .unwrap()
1691 .body(bytes::Bytes::from("to be deleted"))
1692 .send()
1693 .await
1694 .unwrap();
1695
1696 let output = client
1697 .bucket(&bucket_str)
1698 .unwrap()
1699 .delete_object(&key)
1700 .unwrap()
1701 .send()
1702 .await
1703 .unwrap();
1704
1705 assert!(!output.request_id.is_empty());
1706 eprintln!(
1707 "DELETE '{}' succeeded: request_id={}",
1708 key, output.request_id
1709 );
1710 }
1711
1712 #[tokio::test]
1713 #[ignore = "requires valid OSS credentials"]
1714 async fn e2e_head_existing_test_file() {
1715 let ak = std::env::var("OSS_ACCESS_KEY_ID").expect("OSS_ACCESS_KEY_ID not set");
1716 let sk = std::env::var("OSS_ACCESS_KEY_SECRET").expect("OSS_ACCESS_KEY_SECRET not set");
1717 let region_str = std::env::var("OSS_REGION").unwrap_or_else(|_| "cn-wulanchabu".into());
1718 let bucket_str = std::env::var("OSS_BUCKET").expect("OSS_BUCKET not set");
1719
1720 let region = Region::from_str(®ion_str).unwrap_or_else(|_| Region::Custom {
1721 endpoint: format!("oss-{}.aliyuncs.com", region_str),
1722 region_id: region_str.clone(),
1723 });
1724
1725 let client = crate::client::OSSClient::builder()
1726 .region(region)
1727 .credentials(ak, sk)
1728 .build()
1729 .unwrap();
1730
1731 let output = client
1732 .bucket(&bucket_str)
1733 .unwrap()
1734 .head_object("test.txt")
1735 .unwrap()
1736 .send()
1737 .await
1738 .unwrap();
1739
1740 assert!(!output.request_id.is_empty());
1741 assert!(output.content_length.unwrap() > 0);
1742 assert!(output.etag.is_some());
1743 eprintln!(
1744 "HEAD 'test.txt' OK: content-type={:?}, length={:?}, etag={:?}",
1745 output.content_type, output.content_length, output.etag
1746 );
1747 }
1748
1749 #[tokio::test]
1750 #[ignore = "requires valid OSS credentials"]
1751 async fn e2e_get_object_range() {
1752 let ak = std::env::var("OSS_ACCESS_KEY_ID").expect("OSS_ACCESS_KEY_ID not set");
1753 let sk = std::env::var("OSS_ACCESS_KEY_SECRET").expect("OSS_ACCESS_KEY_SECRET not set");
1754 let region_str = std::env::var("OSS_REGION").unwrap_or_else(|_| "cn-wulanchabu".into());
1755 let bucket_str = std::env::var("OSS_BUCKET").expect("OSS_BUCKET not set");
1756
1757 let region = Region::from_str(®ion_str).unwrap_or_else(|_| Region::Custom {
1758 endpoint: format!("oss-{}.aliyuncs.com", region_str),
1759 region_id: region_str.clone(),
1760 });
1761
1762 let client = crate::client::OSSClient::builder()
1763 .region(region)
1764 .credentials(ak, sk)
1765 .build()
1766 .unwrap();
1767
1768 let key = format!("test-range-{}.txt", chrono::Utc::now().timestamp());
1769 let content = "0123456789";
1770
1771 let _put = client
1772 .bucket(&bucket_str)
1773 .unwrap()
1774 .put_object(&key)
1775 .unwrap()
1776 .body(bytes::Bytes::from(content))
1777 .send()
1778 .await
1779 .unwrap();
1780
1781 let output = client
1782 .bucket(&bucket_str)
1783 .unwrap()
1784 .get_object(&key)
1785 .unwrap()
1786 .range("bytes=0-4")
1787 .send()
1788 .await
1789 .unwrap();
1790
1791 assert_eq!(output.body.as_ref(), b"01234");
1792
1793 eprintln!("GET range '{}' succeeded: {} bytes", key, output.body.len());
1794 }
1795
1796 #[tokio::test]
1797 #[ignore = "requires valid OSS credentials"]
1798 async fn e2e_get_existing_test_file() {
1799 let ak = std::env::var("OSS_ACCESS_KEY_ID").expect("OSS_ACCESS_KEY_ID not set");
1800 let sk = std::env::var("OSS_ACCESS_KEY_SECRET").expect("OSS_ACCESS_KEY_SECRET not set");
1801 let region_str = std::env::var("OSS_REGION").unwrap_or_else(|_| "cn-wulanchabu".into());
1802 let bucket_str = std::env::var("OSS_BUCKET").expect("OSS_BUCKET not set");
1803
1804 let region = Region::from_str(®ion_str).unwrap_or_else(|_| Region::Custom {
1805 endpoint: format!("oss-{}.aliyuncs.com", region_str),
1806 region_id: region_str.clone(),
1807 });
1808
1809 let client = crate::client::OSSClient::builder()
1810 .region(region)
1811 .credentials(ak, sk)
1812 .build()
1813 .unwrap();
1814
1815 let output = client
1816 .bucket(&bucket_str)
1817 .unwrap()
1818 .get_object("test.txt")
1819 .unwrap()
1820 .send()
1821 .await
1822 .unwrap();
1823
1824 assert!(!output.body.is_empty());
1825 eprintln!("GET 'test.txt' succeeded: {} bytes", output.body.len());
1826 }
1827
1828 #[tokio::test]
1829 #[ignore = "requires valid OSS credentials"]
1830 async fn e2e_put_object_real_oss() {
1831 let ak = std::env::var("OSS_ACCESS_KEY_ID").expect("OSS_ACCESS_KEY_ID not set");
1832 let sk = std::env::var("OSS_ACCESS_KEY_SECRET").expect("OSS_ACCESS_KEY_SECRET not set");
1833 let region_str = std::env::var("OSS_REGION").unwrap_or_else(|_| "cn-wulanchabu".into());
1834 let bucket_str = std::env::var("OSS_BUCKET").expect("OSS_BUCKET not set");
1835
1836 let region = Region::from_str(®ion_str).unwrap_or_else(|_| Region::Custom {
1837 endpoint: format!("oss-{}.aliyuncs.com", region_str),
1838 region_id: region_str.clone(),
1839 });
1840
1841 let client = crate::client::OSSClient::builder()
1842 .region(region)
1843 .credentials(ak, sk)
1844 .build()
1845 .unwrap();
1846
1847 let key = format!("test-put-object-{}.txt", chrono::Utc::now().timestamp());
1848 let content = "Hello from aliyun-oss SDK E2E test";
1849
1850 let output = client
1851 .bucket(&bucket_str)
1852 .unwrap()
1853 .put_object(&key)
1854 .unwrap()
1855 .body(bytes::Bytes::from(content))
1856 .content_type("text/plain")
1857 .send()
1858 .await
1859 .unwrap();
1860
1861 assert!(!output.request_id.is_empty());
1862 assert!(!output.etag.is_empty());
1863
1864 eprintln!(
1865 "PUT '{}' succeeded: request_id={}, etag={}",
1866 key, output.request_id, output.etag
1867 );
1868 }
1869
1870 #[tokio::test]
1871 #[ignore = "requires valid OSS credentials"]
1872 async fn e2e_get_object_meta() {
1873 let ak = std::env::var("OSS_ACCESS_KEY_ID").expect("OSS_ACCESS_KEY_ID not set");
1874 let sk = std::env::var("OSS_ACCESS_KEY_SECRET").expect("OSS_ACCESS_KEY_SECRET not set");
1875 let region_str = std::env::var("OSS_REGION").unwrap_or_else(|_| "cn-wulanchabu".into());
1876 let bucket_str = std::env::var("OSS_BUCKET").expect("OSS_BUCKET not set");
1877
1878 let region = Region::from_str(®ion_str).unwrap_or_else(|_| Region::Custom {
1879 endpoint: format!("oss-{}.aliyuncs.com", region_str),
1880 region_id: region_str.clone(),
1881 });
1882
1883 let client = crate::client::OSSClient::builder()
1884 .region(region)
1885 .credentials(ak, sk)
1886 .build()
1887 .unwrap();
1888
1889 let output = client
1890 .bucket(&bucket_str)
1891 .unwrap()
1892 .get_object_meta("test.txt")
1893 .unwrap()
1894 .send()
1895 .await
1896 .unwrap();
1897
1898 assert!(!output.request_id.is_empty());
1899 assert!(output.content_length.unwrap() > 0);
1900 eprintln!(
1901 "GetObjectMeta 'test.txt' OK: length={:?}",
1902 output.content_length
1903 );
1904 }
1905
1906 #[tokio::test]
1907 #[ignore = "requires valid OSS credentials"]
1908 async fn e2e_delete_multiple_objects() {
1909 let ak = std::env::var("OSS_ACCESS_KEY_ID").expect("OSS_ACCESS_KEY_ID not set");
1910 let sk = std::env::var("OSS_ACCESS_KEY_SECRET").expect("OSS_ACCESS_KEY_SECRET not set");
1911 let region_str = std::env::var("OSS_REGION").unwrap_or_else(|_| "cn-wulanchabu".into());
1912 let bucket_str = std::env::var("OSS_BUCKET").expect("OSS_BUCKET not set");
1913
1914 let region = Region::from_str(®ion_str).unwrap_or_else(|_| Region::Custom {
1915 endpoint: format!("oss-{}.aliyuncs.com", region_str),
1916 region_id: region_str.clone(),
1917 });
1918
1919 let client = crate::client::OSSClient::builder()
1920 .region(region)
1921 .credentials(ak, sk)
1922 .build()
1923 .unwrap();
1924
1925 let key1 = format!("test-batch-del-1-{}.txt", chrono::Utc::now().timestamp());
1926 let key2 = format!("test-batch-del-2-{}.txt", chrono::Utc::now().timestamp());
1927
1928 for k in &[key1.as_str(), key2.as_str()] {
1929 client
1930 .bucket(&bucket_str)
1931 .unwrap()
1932 .put_object(*k)
1933 .unwrap()
1934 .body(bytes::Bytes::from("data"))
1935 .send()
1936 .await
1937 .unwrap();
1938 }
1939
1940 let result = client
1941 .bucket(&bucket_str)
1942 .unwrap()
1943 .delete_multiple_objects(vec![key1.clone(), key2.clone()])
1944 .send()
1945 .await
1946 .unwrap();
1947
1948 assert_eq!(result.deleted.len(), 2);
1949 eprintln!("DeleteMultiple: {} objects deleted", result.deleted.len());
1950 }
1951
1952 #[tokio::test]
1953 #[ignore = "requires valid OSS credentials"]
1954 async fn e2e_list_object_versions() {
1955 let ak = std::env::var("OSS_ACCESS_KEY_ID").expect("OSS_ACCESS_KEY_ID not set");
1956 let sk = std::env::var("OSS_ACCESS_KEY_SECRET").expect("OSS_ACCESS_KEY_SECRET not set");
1957 let region_str = std::env::var("OSS_REGION").unwrap_or_else(|_| "cn-wulanchabu".into());
1958 let bucket_str = std::env::var("OSS_BUCKET").expect("OSS_BUCKET not set");
1959
1960 let region = Region::from_str(®ion_str).unwrap_or_else(|_| Region::Custom {
1961 endpoint: format!("oss-{}.aliyuncs.com", region_str),
1962 region_id: region_str.clone(),
1963 });
1964
1965 let client = crate::client::OSSClient::builder()
1966 .region(region)
1967 .credentials(ak, sk)
1968 .build()
1969 .unwrap();
1970
1971 let output = client
1972 .bucket(&bucket_str)
1973 .unwrap()
1974 .list_object_versions()
1975 .max_keys(5)
1976 .send()
1977 .await
1978 .unwrap();
1979
1980 eprintln!(
1981 "ListObjectVersions: {} versions, {} delete_markers",
1982 output.versions.len(),
1983 output.delete_markers.len()
1984 );
1985 assert!(!output.name.is_empty());
1986 }
1987}