Skip to main content

aliyun_oss/operations/
object.rs

1//! Object-level operations (PUT, GET, HEAD, DELETE, list, meta, batch delete).
2
3use 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(&region_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(&region_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(&region_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(&region_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(&region_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(&region_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(&region_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(&region_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(&region_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}