Skip to main content

aliyun_oss/operations/
object_multipart.rs

1//! Multipart upload operations (initiate, upload, copy, complete, abort, list).
2
3use std::sync::Arc;
4
5use serde::{Deserialize, Serialize};
6
7use crate::client::{BucketOperations, OSSClientInner};
8use crate::error::{ErrorContext, OssError, OssErrorKind, Result};
9use crate::http::client::HttpRequest;
10use crate::types::acl::ObjectAcl;
11use crate::types::bucket::BucketName;
12use crate::types::object::ObjectKey;
13use crate::types::storage::{ServerSideEncryption, StorageClass};
14use crate::util::uri::oss_endpoint_url;
15
16#[derive(Debug, Clone, Deserialize)]
17#[serde(rename = "InitiateMultipartUploadResult")]
18struct InitiateMultipartUploadResult {
19    #[serde(rename = "Bucket")]
20    bucket: String,
21    #[serde(rename = "Key")]
22    key: String,
23    #[serde(rename = "UploadId")]
24    upload_id: String,
25}
26
27#[derive(Debug, Clone, Deserialize)]
28#[serde(rename = "ListMultipartUploadsResult")]
29struct ListMultipartUploadsResult {
30    #[serde(rename = "Bucket")]
31    bucket: String,
32    #[serde(rename = "Upload", default)]
33    uploads: Vec<MultipartUpload>,
34    #[serde(rename = "IsTruncated")]
35    is_truncated: bool,
36    #[serde(rename = "NextKeyMarker", default)]
37    next_key_marker: String,
38    #[serde(rename = "NextUploadIdMarker", default)]
39    next_upload_id_marker: String,
40    #[serde(rename = "MaxUploads")]
41    max_uploads: i32,
42}
43
44#[derive(Debug, Clone, Deserialize)]
45struct MultipartUpload {
46    #[serde(rename = "Key")]
47    key: String,
48    #[serde(rename = "UploadId")]
49    upload_id: String,
50    #[serde(rename = "Initiated")]
51    initiated: String,
52}
53
54#[derive(Debug, Clone, Deserialize)]
55#[serde(rename = "ListPartsResult")]
56struct ListPartsResult {
57    #[serde(rename = "Bucket")]
58    bucket: String,
59    #[serde(rename = "Key")]
60    key: String,
61    #[serde(rename = "UploadId")]
62    upload_id: String,
63    #[serde(rename = "Part", default)]
64    parts: Vec<PartSummary>,
65    #[serde(rename = "MaxParts")]
66    max_parts: i32,
67    #[serde(rename = "IsTruncated")]
68    is_truncated: bool,
69    #[serde(rename = "NextPartNumberMarker", default)]
70    next_part_number_marker: String,
71}
72
73#[derive(Debug, Clone, Deserialize)]
74struct PartSummary {
75    #[serde(rename = "PartNumber")]
76    part_number: i32,
77    #[serde(rename = "LastModified")]
78    last_modified: String,
79    #[serde(rename = "ETag")]
80    etag: String,
81    #[serde(rename = "Size")]
82    size: u64,
83}
84
85pub struct InitiateMultipartUploadBuilder {
86    client: Arc<OSSClientInner>,
87    bucket: BucketName,
88    key: ObjectKey,
89    cache_control: Option<String>,
90    content_type: Option<String>,
91    content_disposition: Option<String>,
92    content_encoding: Option<String>,
93    expires: Option<String>,
94    acl: Option<ObjectAcl>,
95    storage_class: Option<StorageClass>,
96    sse: Option<ServerSideEncryption>,
97    sse_key_id: Option<String>,
98    tagging: Option<String>,
99    metadata: Vec<(String, String)>,
100}
101
102impl InitiateMultipartUploadBuilder {
103    pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName, key: ObjectKey) -> Self {
104        Self {
105            client,
106            bucket,
107            key,
108            cache_control: None,
109            content_type: None,
110            content_disposition: None,
111            content_encoding: None,
112            expires: None,
113            acl: None,
114            storage_class: None,
115            sse: None,
116            sse_key_id: None,
117            tagging: None,
118            metadata: Vec::new(),
119        }
120    }
121
122    pub fn cache_control(mut self, v: impl Into<String>) -> Self {
123        self.cache_control = Some(v.into());
124        self
125    }
126
127    pub fn content_type(mut self, v: impl Into<String>) -> Self {
128        self.content_type = Some(v.into());
129        self
130    }
131
132    pub fn content_disposition(mut self, v: impl Into<String>) -> Self {
133        self.content_disposition = Some(v.into());
134        self
135    }
136
137    pub fn content_encoding(mut self, v: impl Into<String>) -> Self {
138        self.content_encoding = Some(v.into());
139        self
140    }
141
142    pub fn expires(mut self, v: impl Into<String>) -> Self {
143        self.expires = Some(v.into());
144        self
145    }
146
147    pub fn acl(mut self, acl: ObjectAcl) -> Self {
148        self.acl = Some(acl);
149        self
150    }
151
152    pub fn storage_class(mut self, sc: StorageClass) -> Self {
153        self.storage_class = Some(sc);
154        self
155    }
156
157    pub fn server_side_encryption(mut self, sse: impl Into<String>) -> Self {
158        match sse.into().as_str() {
159            "AES256" => self.sse = Some(ServerSideEncryption::AES256),
160            "KMS" => self.sse = Some(ServerSideEncryption::KMS),
161            _ => {}
162        }
163        self
164    }
165
166    pub fn sse_key_id(mut self, key_id: impl Into<String>) -> Self {
167        self.sse_key_id = Some(key_id.into());
168        self
169    }
170
171    pub fn tagging(mut self, tag: impl Into<String>) -> Self {
172        self.tagging = Some(tag.into());
173        self
174    }
175
176    pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
177        self.metadata.push((key.into(), value.into()));
178        self
179    }
180
181    pub async fn send(self) -> Result<InitiateMultipartUploadOutput> {
182        let endpoint = self.client.endpoint.clone();
183        let uri = oss_endpoint_url(
184            &endpoint,
185            Some(self.bucket.as_str()),
186            Some(self.key.as_str()),
187        );
188        let full_uri = format!("{}?uploads", uri);
189
190        let mut req = HttpRequest::builder()
191            .method(http::Method::POST)
192            .uri(&full_uri);
193
194        if let Some(ref ct) = self.content_type {
195            req = req.header(
196                http::HeaderName::from_static("content-type"),
197                http::HeaderValue::from_str(ct).map_err(|e| OssError {
198                    kind: OssErrorKind::ValidationError,
199                    context: Box::new(ErrorContext {
200                        operation: Some("set content-type header".into()),
201                        bucket: Some(self.bucket.to_string()),
202                        object_key: Some(self.key.to_string()),
203                        ..Default::default()
204                    }),
205                    source: Some(Box::new(e)),
206                })?,
207            );
208        }
209
210        if let Some(ref cc) = self.cache_control {
211            req = req.header(
212                http::HeaderName::from_static("cache-control"),
213                http::HeaderValue::from_str(cc).map_err(|e| OssError {
214                    kind: OssErrorKind::ValidationError,
215                    context: Box::new(ErrorContext {
216                        operation: Some("set cache-control header".into()),
217                        bucket: Some(self.bucket.to_string()),
218                        object_key: Some(self.key.to_string()),
219                        ..Default::default()
220                    }),
221                    source: Some(Box::new(e)),
222                })?,
223            );
224        }
225
226        if let Some(ref cd) = self.content_disposition {
227            req = req.header(
228                http::HeaderName::from_static("content-disposition"),
229                http::HeaderValue::from_str(cd).map_err(|e| OssError {
230                    kind: OssErrorKind::ValidationError,
231                    context: Box::new(ErrorContext {
232                        operation: Some("set content-disposition header".into()),
233                        bucket: Some(self.bucket.to_string()),
234                        object_key: Some(self.key.to_string()),
235                        ..Default::default()
236                    }),
237                    source: Some(Box::new(e)),
238                })?,
239            );
240        }
241
242        if let Some(ref ce) = self.content_encoding {
243            req = req.header(
244                http::HeaderName::from_static("content-encoding"),
245                http::HeaderValue::from_str(ce).map_err(|e| OssError {
246                    kind: OssErrorKind::ValidationError,
247                    context: Box::new(ErrorContext {
248                        operation: Some("set content-encoding header".into()),
249                        bucket: Some(self.bucket.to_string()),
250                        object_key: Some(self.key.to_string()),
251                        ..Default::default()
252                    }),
253                    source: Some(Box::new(e)),
254                })?,
255            );
256        }
257
258        if let Some(ref exp) = self.expires {
259            req = req.header(
260                http::HeaderName::from_static("expires"),
261                http::HeaderValue::from_str(exp).map_err(|e| OssError {
262                    kind: OssErrorKind::ValidationError,
263                    context: Box::new(ErrorContext {
264                        operation: Some("set expires header".into()),
265                        bucket: Some(self.bucket.to_string()),
266                        object_key: Some(self.key.to_string()),
267                        ..Default::default()
268                    }),
269                    source: Some(Box::new(e)),
270                })?,
271            );
272        }
273
274        if let Some(acl) = self.acl {
275            req = req.header(
276                http::HeaderName::from_static("x-oss-object-acl"),
277                http::HeaderValue::from_str(acl.as_str()).map_err(|e| OssError {
278                    kind: OssErrorKind::ValidationError,
279                    context: Box::new(ErrorContext {
280                        operation: Some("set x-oss-object-acl header".into()),
281                        bucket: Some(self.bucket.to_string()),
282                        object_key: Some(self.key.to_string()),
283                        ..Default::default()
284                    }),
285                    source: Some(Box::new(e)),
286                })?,
287            );
288        }
289
290        if let Some(sc) = self.storage_class {
291            req = req.header(
292                http::HeaderName::from_static("x-oss-storage-class"),
293                http::HeaderValue::from_str(sc.as_str()).map_err(|e| OssError {
294                    kind: OssErrorKind::ValidationError,
295                    context: Box::new(ErrorContext {
296                        operation: Some("set x-oss-storage-class header".into()),
297                        bucket: Some(self.bucket.to_string()),
298                        object_key: Some(self.key.to_string()),
299                        ..Default::default()
300                    }),
301                    source: Some(Box::new(e)),
302                })?,
303            );
304        }
305
306        if let Some(ref sse) = self.sse {
307            req = req.header(
308                http::HeaderName::from_static("x-oss-server-side-encryption"),
309                http::HeaderValue::from_str(sse.as_str()).map_err(|e| OssError {
310                    kind: OssErrorKind::ValidationError,
311                    context: Box::new(ErrorContext {
312                        operation: Some("set x-oss-server-side-encryption header".into()),
313                        bucket: Some(self.bucket.to_string()),
314                        object_key: Some(self.key.to_string()),
315                        ..Default::default()
316                    }),
317                    source: Some(Box::new(e)),
318                })?,
319            );
320            if let Some(key_id) = sse.key_id() {
321                req = req.header(
322                    http::HeaderName::from_static("x-oss-server-side-encryption-key-id"),
323                    http::HeaderValue::from_str(key_id).map_err(|e| OssError {
324                        kind: OssErrorKind::ValidationError,
325                        context: Box::new(ErrorContext {
326                            operation: Some(
327                                "set x-oss-server-side-encryption-key-id header".into(),
328                            ),
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        } else if let Some(ref key_id) = self.sse_key_id {
338            req = req.header(
339                http::HeaderName::from_static("x-oss-server-side-encryption"),
340                http::HeaderValue::from_str("KMS").map_err(|e| OssError {
341                    kind: OssErrorKind::ValidationError,
342                    context: Box::new(ErrorContext {
343                        operation: Some("set x-oss-server-side-encryption header".into()),
344                        bucket: Some(self.bucket.to_string()),
345                        object_key: Some(self.key.to_string()),
346                        ..Default::default()
347                    }),
348                    source: Some(Box::new(e)),
349                })?,
350            );
351            req = req.header(
352                http::HeaderName::from_static("x-oss-server-side-encryption-key-id"),
353                http::HeaderValue::from_str(key_id).map_err(|e| OssError {
354                    kind: OssErrorKind::ValidationError,
355                    context: Box::new(ErrorContext {
356                        operation: Some("set x-oss-server-side-encryption-key-id header".into()),
357                        bucket: Some(self.bucket.to_string()),
358                        object_key: Some(self.key.to_string()),
359                        ..Default::default()
360                    }),
361                    source: Some(Box::new(e)),
362                })?,
363            );
364        }
365
366        if let Some(ref tag) = self.tagging {
367            req = req.header(
368                http::HeaderName::from_static("x-oss-tagging"),
369                http::HeaderValue::from_str(tag).map_err(|e| OssError {
370                    kind: OssErrorKind::ValidationError,
371                    context: Box::new(ErrorContext {
372                        operation: Some("set x-oss-tagging header".into()),
373                        bucket: Some(self.bucket.to_string()),
374                        object_key: Some(self.key.to_string()),
375                        ..Default::default()
376                    }),
377                    source: Some(Box::new(e)),
378                })?,
379            );
380        }
381
382        for (k, v) in &self.metadata {
383            let header_name = http::HeaderName::from_bytes(k.as_bytes()).map_err(|e| OssError {
384                kind: OssErrorKind::ValidationError,
385                context: Box::new(ErrorContext {
386                    operation: Some(format!("set metadata header '{}'", k)),
387                    bucket: Some(self.bucket.to_string()),
388                    object_key: Some(self.key.to_string()),
389                    ..Default::default()
390                }),
391                source: Some(Box::new(e)),
392            })?;
393            req = req.header(
394                header_name,
395                http::HeaderValue::from_str(v).map_err(|e| OssError {
396                    kind: OssErrorKind::ValidationError,
397                    context: Box::new(ErrorContext {
398                        operation: Some(format!("set metadata header value '{}'", k)),
399                        bucket: Some(self.bucket.to_string()),
400                        object_key: Some(self.key.to_string()),
401                        ..Default::default()
402                    }),
403                    source: Some(Box::new(e)),
404                })?,
405            );
406        }
407
408        let query_params: Vec<(String, String)> = vec![("uploads".into(), String::new())];
409        let request = req.build();
410
411        let response = self
412            .client
413            .send_signed(request, Some(&self.bucket), query_params)
414            .await
415            .map_err(|e| OssError {
416                kind: OssErrorKind::TransportError,
417                context: Box::new(ErrorContext {
418                    operation: Some("InitiateMultipartUpload".into()),
419                    bucket: Some(self.bucket.to_string()),
420                    object_key: Some(self.key.to_string()),
421                    endpoint: Some(endpoint),
422                    ..Default::default()
423                }),
424                source: Some(Box::new(e)),
425            })?;
426
427        if response.is_success() {
428            let request_id = response
429                .headers
430                .get("x-oss-request-id")
431                .and_then(|v| v.to_str().ok())
432                .unwrap_or("")
433                .to_string();
434
435            let body_str = response.body_as_str().unwrap_or("");
436
437            let result: InitiateMultipartUploadResult = crate::util::xml::from_xml(body_str)
438                .map_err(|e| OssError {
439                    kind: OssErrorKind::DeserializationError,
440                    context: Box::new(ErrorContext {
441                        operation: Some("InitiateMultipartUpload: parse XML".into()),
442                        bucket: Some(self.bucket.to_string()),
443                        object_key: Some(self.key.to_string()),
444                        ..Default::default()
445                    }),
446                    source: Some(Box::new(e)),
447                })?;
448
449            Ok(InitiateMultipartUploadOutput {
450                request_id,
451                bucket: result.bucket,
452                key: result.key,
453                upload_id: result.upload_id,
454            })
455        } else {
456            Err(OssError {
457                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
458                    status_code: response.status().as_u16(),
459                    code: String::new(),
460                    message: String::new(),
461                    request_id: String::new(),
462                    host_id: String::new(),
463                    resource: Some(self.key.to_string()),
464                    string_to_sign: None,
465                })),
466                context: Box::new(ErrorContext {
467                    operation: Some("InitiateMultipartUpload".into()),
468                    bucket: Some(self.bucket.to_string()),
469                    object_key: Some(self.key.to_string()),
470                    ..Default::default()
471                }),
472                source: None,
473            })
474        }
475    }
476}
477
478#[derive(Debug, Clone)]
479pub struct InitiateMultipartUploadOutput {
480    pub request_id: String,
481    pub bucket: String,
482    pub key: String,
483    pub upload_id: String,
484}
485
486pub struct UploadPartBuilder {
487    client: Arc<OSSClientInner>,
488    bucket: BucketName,
489    key: ObjectKey,
490    upload_id: String,
491    part_number: u32,
492    body: Option<bytes::Bytes>,
493    content_md5: Option<String>,
494}
495
496impl UploadPartBuilder {
497    pub(crate) fn new(
498        client: Arc<OSSClientInner>,
499        bucket: BucketName,
500        key: ObjectKey,
501        upload_id: impl Into<String>,
502        part_number: u32,
503    ) -> Self {
504        Self {
505            client,
506            bucket,
507            key,
508            upload_id: upload_id.into(),
509            part_number,
510            body: None,
511            content_md5: None,
512        }
513    }
514
515    pub fn body(mut self, body: impl Into<bytes::Bytes>) -> Self {
516        self.body = Some(body.into());
517        self
518    }
519
520    pub fn content_md5(mut self, md5: impl Into<String>) -> Self {
521        self.content_md5 = Some(md5.into());
522        self
523    }
524
525    pub(crate) fn compute_md5(body: &[u8]) -> String {
526        use md5::{Digest, Md5};
527        let digest = Md5::digest(body);
528        base64::Engine::encode(
529            &base64::engine::general_purpose::STANDARD,
530            digest.as_slice(),
531        )
532    }
533
534    pub async fn send(self) -> Result<UploadPartOutput> {
535        let body = self.body.ok_or_else(|| OssError {
536            kind: OssErrorKind::ValidationError,
537            context: Box::new(ErrorContext {
538                operation: Some("UploadPart: body is required".into()),
539                bucket: Some(self.bucket.to_string()),
540                object_key: Some(self.key.to_string()),
541                ..Default::default()
542            }),
543            source: None,
544        })?;
545
546        if self.part_number < 1 || self.part_number > 10000 {
547            return Err(OssError {
548                kind: OssErrorKind::ValidationError,
549                context: Box::new(ErrorContext {
550                    operation: Some("UploadPart: part_number must be 1..=10000".into()),
551                    bucket: Some(self.bucket.to_string()),
552                    object_key: Some(self.key.to_string()),
553                    ..Default::default()
554                }),
555                source: None,
556            });
557        }
558
559        let endpoint = self.client.endpoint.clone();
560        let uri = oss_endpoint_url(
561            &endpoint,
562            Some(self.bucket.as_str()),
563            Some(self.key.as_str()),
564        );
565        let full_uri = format!(
566            "{}?partNumber={}&uploadId={}",
567            uri, self.part_number, self.upload_id
568        );
569
570        let query_params: Vec<(String, String)> = vec![
571            ("partNumber".into(), self.part_number.to_string()),
572            ("uploadId".into(), self.upload_id.clone()),
573        ];
574
575        let mut req = HttpRequest::builder()
576            .method(http::Method::PUT)
577            .uri(&full_uri);
578
579        let md5_value = self.content_md5.unwrap_or_else(|| Self::compute_md5(&body));
580        req = req.header(
581            http::HeaderName::from_static("content-md5"),
582            http::HeaderValue::from_str(&md5_value).map_err(|e| OssError {
583                kind: OssErrorKind::ValidationError,
584                context: Box::new(ErrorContext {
585                    operation: Some("set content-md5 header".into()),
586                    bucket: Some(self.bucket.to_string()),
587                    object_key: Some(self.key.to_string()),
588                    ..Default::default()
589                }),
590                source: Some(Box::new(e)),
591            })?,
592        );
593
594        let request = req.body(body).build();
595
596        let response = self
597            .client
598            .send_signed(request, Some(&self.bucket), query_params)
599            .await
600            .map_err(|e| OssError {
601                kind: OssErrorKind::TransportError,
602                context: Box::new(ErrorContext {
603                    operation: Some("UploadPart".into()),
604                    bucket: Some(self.bucket.to_string()),
605                    object_key: Some(self.key.to_string()),
606                    endpoint: Some(endpoint),
607                    ..Default::default()
608                }),
609                source: Some(Box::new(e)),
610            })?;
611
612        if response.is_success() {
613            let request_id = response
614                .headers
615                .get("x-oss-request-id")
616                .and_then(|v| v.to_str().ok())
617                .unwrap_or("")
618                .to_string();
619
620            let etag = response
621                .headers
622                .get("ETag")
623                .or_else(|| response.headers.get("etag"))
624                .and_then(|v| v.to_str().ok())
625                .map(|s| s.trim_matches('"').to_string())
626                .unwrap_or_default();
627
628            Ok(UploadPartOutput {
629                request_id,
630                etag,
631                part_number: self.part_number,
632            })
633        } else {
634            Err(OssError {
635                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
636                    status_code: response.status().as_u16(),
637                    code: String::new(),
638                    message: String::new(),
639                    request_id: String::new(),
640                    host_id: String::new(),
641                    resource: Some(self.key.to_string()),
642                    string_to_sign: None,
643                })),
644                context: Box::new(ErrorContext {
645                    operation: Some("UploadPart".into()),
646                    bucket: Some(self.bucket.to_string()),
647                    object_key: Some(self.key.to_string()),
648                    ..Default::default()
649                }),
650                source: None,
651            })
652        }
653    }
654}
655
656#[derive(Debug, Clone)]
657pub struct UploadPartOutput {
658    pub request_id: String,
659    pub etag: String,
660    pub part_number: u32,
661}
662
663#[derive(Debug, Clone, Deserialize)]
664#[serde(rename = "CopyPartResult")]
665struct CopyPartResult {
666    #[serde(rename = "ETag")]
667    etag: String,
668    #[serde(rename = "LastModified")]
669    last_modified: String,
670}
671
672#[derive(Debug, Clone, Serialize)]
673#[serde(rename = "CompleteMultipartUpload")]
674struct CompleteMultipartUpload {
675    #[serde(rename = "Part")]
676    parts: Vec<UploadPartItem>,
677}
678
679#[derive(Debug, Clone, Serialize)]
680struct UploadPartItem {
681    #[serde(rename = "PartNumber")]
682    part_number: i32,
683    #[serde(rename = "ETag")]
684    etag: String,
685}
686
687#[derive(Debug, Clone, Deserialize)]
688#[serde(rename = "CompleteMultipartUploadResult")]
689struct CompleteMultipartUploadResult {
690    #[serde(rename = "Location")]
691    location: String,
692    #[serde(rename = "Bucket")]
693    bucket: String,
694    #[serde(rename = "Key")]
695    key: String,
696    #[serde(rename = "ETag")]
697    etag: String,
698}
699
700pub struct AbortMultipartUploadBuilder {
701    client: Arc<OSSClientInner>,
702    bucket: BucketName,
703    key: ObjectKey,
704    upload_id: String,
705}
706
707impl AbortMultipartUploadBuilder {
708    pub(crate) fn new(
709        client: Arc<OSSClientInner>,
710        bucket: BucketName,
711        key: ObjectKey,
712        upload_id: impl Into<String>,
713    ) -> Self {
714        Self {
715            client,
716            bucket,
717            key,
718            upload_id: upload_id.into(),
719        }
720    }
721
722    pub async fn send(self) -> Result<AbortMultipartUploadOutput> {
723        let endpoint = self.client.endpoint.clone();
724        let uri = oss_endpoint_url(
725            &endpoint,
726            Some(self.bucket.as_str()),
727            Some(self.key.as_str()),
728        );
729        let full_uri = format!("{}?uploadId={}", uri, self.upload_id);
730
731        let query_params: Vec<(String, String)> = vec![("uploadId".into(), self.upload_id.clone())];
732
733        let request = HttpRequest::builder()
734            .method(http::Method::DELETE)
735            .uri(&full_uri)
736            .build();
737
738        let response = self
739            .client
740            .send_signed(request, Some(&self.bucket), query_params)
741            .await
742            .map_err(|e| OssError {
743                kind: OssErrorKind::TransportError,
744                context: Box::new(ErrorContext {
745                    operation: Some("AbortMultipartUpload".into()),
746                    bucket: Some(self.bucket.to_string()),
747                    object_key: Some(self.key.to_string()),
748                    endpoint: Some(endpoint),
749                    ..Default::default()
750                }),
751                source: Some(Box::new(e)),
752            })?;
753
754        if response.status().is_success() {
755            Ok(AbortMultipartUploadOutput {
756                request_id: response
757                    .headers
758                    .get("x-oss-request-id")
759                    .and_then(|v| v.to_str().ok())
760                    .unwrap_or("")
761                    .to_string(),
762            })
763        } else {
764            Err(OssError {
765                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
766                    status_code: response.status().as_u16(),
767                    code: String::new(),
768                    message: String::new(),
769                    request_id: String::new(),
770                    host_id: String::new(),
771                    resource: Some(self.key.to_string()),
772                    string_to_sign: None,
773                })),
774                context: Box::new(ErrorContext {
775                    operation: Some("AbortMultipartUpload".into()),
776                    bucket: Some(self.bucket.to_string()),
777                    object_key: Some(self.key.to_string()),
778                    ..Default::default()
779                }),
780                source: None,
781            })
782        }
783    }
784}
785
786#[derive(Debug, Clone)]
787pub struct AbortMultipartUploadOutput {
788    pub request_id: String,
789}
790
791pub struct CompleteMultipartUploadBuilder {
792    client: Arc<OSSClientInner>,
793    bucket: BucketName,
794    key: ObjectKey,
795    upload_id: String,
796    parts: Vec<(i32, String)>,
797}
798
799impl CompleteMultipartUploadBuilder {
800    pub(crate) fn new(
801        client: Arc<OSSClientInner>,
802        bucket: BucketName,
803        key: ObjectKey,
804        upload_id: impl Into<String>,
805    ) -> Self {
806        Self {
807            client,
808            bucket,
809            key,
810            upload_id: upload_id.into(),
811            parts: Vec::new(),
812        }
813    }
814
815    pub fn part(mut self, part_number: i32, etag: impl Into<String>) -> Self {
816        self.parts.push((part_number, etag.into()));
817        self
818    }
819
820    pub async fn send(self) -> Result<CompleteMultipartUploadOutput> {
821        if self.parts.is_empty() {
822            return Err(OssError {
823                kind: OssErrorKind::ValidationError,
824                context: Box::new(ErrorContext {
825                    operation: Some("CompleteMultipartUpload: at least one part required".into()),
826                    bucket: Some(self.bucket.to_string()),
827                    object_key: Some(self.key.to_string()),
828                    ..Default::default()
829                }),
830                source: None,
831            });
832        }
833
834        let endpoint = self.client.endpoint.clone();
835        let uri = oss_endpoint_url(
836            &endpoint,
837            Some(self.bucket.as_str()),
838            Some(self.key.as_str()),
839        );
840        let full_uri = format!("{}?uploadId={}", uri, self.upload_id);
841
842        let query_params: Vec<(String, String)> = vec![("uploadId".into(), self.upload_id.clone())];
843
844        let complete_xml = CompleteMultipartUpload {
845            parts: self
846                .parts
847                .iter()
848                .map(|(n, e)| UploadPartItem {
849                    part_number: *n,
850                    etag: e.clone(),
851                })
852                .collect(),
853        };
854        let body_xml = crate::util::xml::to_xml(&complete_xml)?;
855
856        let request = HttpRequest::builder()
857            .method(http::Method::POST)
858            .uri(&full_uri)
859            .body(bytes::Bytes::from(body_xml))
860            .build();
861
862        let response = self
863            .client
864            .send_signed(request, Some(&self.bucket), query_params)
865            .await
866            .map_err(|e| OssError {
867                kind: OssErrorKind::TransportError,
868                context: Box::new(ErrorContext {
869                    operation: Some("CompleteMultipartUpload".into()),
870                    bucket: Some(self.bucket.to_string()),
871                    object_key: Some(self.key.to_string()),
872                    endpoint: Some(endpoint),
873                    ..Default::default()
874                }),
875                source: Some(Box::new(e)),
876            })?;
877
878        if response.is_success() {
879            let request_id = response
880                .headers
881                .get("x-oss-request-id")
882                .and_then(|v| v.to_str().ok())
883                .unwrap_or("")
884                .to_string();
885
886            let body_str = response.body_as_str().unwrap_or("");
887            let result: CompleteMultipartUploadResult = crate::util::xml::from_xml(body_str)
888                .map_err(|e| OssError {
889                    kind: OssErrorKind::DeserializationError,
890                    context: Box::new(ErrorContext {
891                        operation: Some("CompleteMultipartUpload: parse XML".into()),
892                        bucket: Some(self.bucket.to_string()),
893                        object_key: Some(self.key.to_string()),
894                        ..Default::default()
895                    }),
896                    source: Some(Box::new(e)),
897                })?;
898
899            Ok(CompleteMultipartUploadOutput {
900                request_id,
901                bucket: result.bucket,
902                key: result.key,
903                location: result.location,
904                etag: result.etag.trim_matches('"').to_string(),
905            })
906        } else {
907            Err(OssError {
908                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
909                    status_code: response.status().as_u16(),
910                    code: String::new(),
911                    message: String::new(),
912                    request_id: String::new(),
913                    host_id: String::new(),
914                    resource: Some(self.key.to_string()),
915                    string_to_sign: None,
916                })),
917                context: Box::new(ErrorContext {
918                    operation: Some("CompleteMultipartUpload".into()),
919                    bucket: Some(self.bucket.to_string()),
920                    object_key: Some(self.key.to_string()),
921                    ..Default::default()
922                }),
923                source: None,
924            })
925        }
926    }
927}
928
929#[derive(Debug, Clone)]
930pub struct CompleteMultipartUploadOutput {
931    pub request_id: String,
932    pub bucket: String,
933    pub key: String,
934    pub location: String,
935    pub etag: String,
936}
937
938pub struct ListMultipartUploadsBuilder {
939    client: Arc<OSSClientInner>,
940    bucket: BucketName,
941    prefix: Option<String>,
942    delimiter: Option<String>,
943    max_uploads: Option<i32>,
944    key_marker: Option<String>,
945    upload_id_marker: Option<String>,
946    encoding_type: Option<String>,
947}
948
949impl ListMultipartUploadsBuilder {
950    pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName) -> Self {
951        Self {
952            client,
953            bucket,
954            prefix: None,
955            delimiter: None,
956            max_uploads: None,
957            key_marker: None,
958            upload_id_marker: None,
959            encoding_type: None,
960        }
961    }
962
963    pub fn prefix(mut self, v: impl Into<String>) -> Self {
964        self.prefix = Some(v.into());
965        self
966    }
967
968    pub fn delimiter(mut self, v: impl Into<String>) -> Self {
969        self.delimiter = Some(v.into());
970        self
971    }
972
973    pub fn max_uploads(mut self, v: i32) -> Self {
974        self.max_uploads = Some(v);
975        self
976    }
977
978    pub fn key_marker(mut self, v: impl Into<String>) -> Self {
979        self.key_marker = Some(v.into());
980        self
981    }
982
983    pub fn upload_id_marker(mut self, v: impl Into<String>) -> Self {
984        self.upload_id_marker = Some(v.into());
985        self
986    }
987
988    pub fn encoding_type(mut self, v: impl Into<String>) -> Self {
989        self.encoding_type = Some(v.into());
990        self
991    }
992
993    pub async fn send(self) -> Result<ListMultipartUploadsOutput> {
994        let endpoint = self.client.endpoint.clone();
995        let uri = oss_endpoint_url(&endpoint, Some(self.bucket.as_str()), None);
996
997        let mut query_pairs: Vec<(String, String)> = Vec::new();
998        query_pairs.push(("uploads".into(), String::new()));
999        if let Some(ref p) = self.prefix {
1000            query_pairs.push(("prefix".into(), crate::util::uri::uri_encode(p)));
1001        }
1002        if let Some(ref d) = self.delimiter {
1003            query_pairs.push(("delimiter".into(), d.clone()));
1004        }
1005        if let Some(mu) = self.max_uploads {
1006            query_pairs.push(("max-uploads".into(), mu.to_string()));
1007        }
1008        if let Some(ref km) = self.key_marker {
1009            query_pairs.push(("key-marker".into(), km.clone()));
1010        }
1011        if let Some(ref uim) = self.upload_id_marker {
1012            query_pairs.push(("upload-id-marker".into(), uim.clone()));
1013        }
1014        if let Some(ref et) = self.encoding_type {
1015            query_pairs.push(("encoding-type".into(), et.clone()));
1016        }
1017
1018        let query_string: String = if query_pairs.is_empty() {
1019            String::new()
1020        } else {
1021            let parts: Vec<String> = query_pairs
1022                .iter()
1023                .map(|(k, v)| format!("{}={}", k, v))
1024                .collect();
1025            format!("?{}", parts.join("&"))
1026        };
1027        let full_uri = format!("{}{}", uri, query_string);
1028
1029        let request = HttpRequest::builder()
1030            .method(http::Method::GET)
1031            .uri(&full_uri)
1032            .build();
1033
1034        let response = self
1035            .client
1036            .send_signed(request, Some(&self.bucket), query_pairs)
1037            .await
1038            .map_err(|e| OssError {
1039                kind: OssErrorKind::TransportError,
1040                context: Box::new(ErrorContext {
1041                    operation: Some("ListMultipartUploads".into()),
1042                    bucket: Some(self.bucket.to_string()),
1043                    endpoint: Some(endpoint),
1044                    ..Default::default()
1045                }),
1046                source: Some(Box::new(e)),
1047            })?;
1048
1049        if response.is_success() {
1050            let body_str = response.body_as_str().unwrap_or("");
1051            let result: ListMultipartUploadsResult =
1052                crate::util::xml::from_xml(body_str).map_err(|e| OssError {
1053                    kind: OssErrorKind::DeserializationError,
1054                    context: Box::new(ErrorContext {
1055                        operation: Some("ListMultipartUploads: parse XML".into()),
1056                        bucket: Some(self.bucket.to_string()),
1057                        ..Default::default()
1058                    }),
1059                    source: Some(Box::new(e)),
1060                })?;
1061
1062            Ok(ListMultipartUploadsOutput {
1063                bucket: result.bucket,
1064                uploads: result
1065                    .uploads
1066                    .into_iter()
1067                    .map(|u| MultipartUploadInfo {
1068                        key: u.key,
1069                        upload_id: u.upload_id,
1070                        initiated: u.initiated,
1071                    })
1072                    .collect(),
1073                is_truncated: result.is_truncated,
1074                next_key_marker: result.next_key_marker,
1075                next_upload_id_marker: result.next_upload_id_marker,
1076                max_uploads: result.max_uploads,
1077            })
1078        } else {
1079            Err(OssError {
1080                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
1081                    status_code: response.status().as_u16(),
1082                    code: String::new(),
1083                    message: String::new(),
1084                    request_id: String::new(),
1085                    host_id: String::new(),
1086                    resource: Some(self.bucket.to_string()),
1087                    string_to_sign: None,
1088                })),
1089                context: Box::new(ErrorContext {
1090                    operation: Some("ListMultipartUploads".into()),
1091                    bucket: Some(self.bucket.to_string()),
1092                    ..Default::default()
1093                }),
1094                source: None,
1095            })
1096        }
1097    }
1098}
1099
1100#[derive(Debug, Clone)]
1101pub struct ListMultipartUploadsOutput {
1102    pub bucket: String,
1103    pub uploads: Vec<MultipartUploadInfo>,
1104    pub is_truncated: bool,
1105    pub next_key_marker: String,
1106    pub next_upload_id_marker: String,
1107    pub max_uploads: i32,
1108}
1109
1110#[derive(Debug, Clone)]
1111pub struct MultipartUploadInfo {
1112    pub key: String,
1113    pub upload_id: String,
1114    pub initiated: String,
1115}
1116
1117pub struct ListPartsBuilder {
1118    client: Arc<OSSClientInner>,
1119    bucket: BucketName,
1120    key: ObjectKey,
1121    upload_id: String,
1122    max_parts: Option<i32>,
1123    part_number_marker: Option<i32>,
1124    encoding_type: Option<String>,
1125}
1126
1127impl ListPartsBuilder {
1128    pub(crate) fn new(
1129        client: Arc<OSSClientInner>,
1130        bucket: BucketName,
1131        key: ObjectKey,
1132        upload_id: impl Into<String>,
1133    ) -> Self {
1134        Self {
1135            client,
1136            bucket,
1137            key,
1138            upload_id: upload_id.into(),
1139            max_parts: None,
1140            part_number_marker: None,
1141            encoding_type: None,
1142        }
1143    }
1144
1145    pub fn max_parts(mut self, v: i32) -> Self {
1146        self.max_parts = Some(v);
1147        self
1148    }
1149
1150    pub fn part_number_marker(mut self, v: i32) -> Self {
1151        self.part_number_marker = Some(v);
1152        self
1153    }
1154
1155    pub fn encoding_type(mut self, v: impl Into<String>) -> Self {
1156        self.encoding_type = Some(v.into());
1157        self
1158    }
1159
1160    pub async fn send(self) -> Result<ListPartsOutput> {
1161        let endpoint = self.client.endpoint.clone();
1162        let uri = oss_endpoint_url(
1163            &endpoint,
1164            Some(self.bucket.as_str()),
1165            Some(self.key.as_str()),
1166        );
1167        let full_uri = format!("{}?uploadId={}", uri, self.upload_id);
1168
1169        let mut query_pairs: Vec<(String, String)> = Vec::new();
1170        query_pairs.push(("uploadId".into(), self.upload_id.clone()));
1171        if let Some(mp) = self.max_parts {
1172            query_pairs.push(("max-parts".into(), mp.to_string()));
1173        }
1174        if let Some(pnm) = self.part_number_marker {
1175            query_pairs.push(("part-number-marker".into(), pnm.to_string()));
1176        }
1177        if let Some(ref et) = self.encoding_type {
1178            query_pairs.push(("encoding-type".into(), et.clone()));
1179        }
1180
1181        let request = HttpRequest::builder()
1182            .method(http::Method::GET)
1183            .uri(&full_uri)
1184            .build();
1185
1186        let response = self
1187            .client
1188            .send_signed(request, Some(&self.bucket), query_pairs)
1189            .await
1190            .map_err(|e| OssError {
1191                kind: OssErrorKind::TransportError,
1192                context: Box::new(ErrorContext {
1193                    operation: Some("ListParts".into()),
1194                    bucket: Some(self.bucket.to_string()),
1195                    object_key: Some(self.key.to_string()),
1196                    endpoint: Some(endpoint),
1197                    ..Default::default()
1198                }),
1199                source: Some(Box::new(e)),
1200            })?;
1201
1202        if response.is_success() {
1203            let body_str = response.body_as_str().unwrap_or("");
1204            let result: ListPartsResult =
1205                crate::util::xml::from_xml(body_str).map_err(|e| OssError {
1206                    kind: OssErrorKind::DeserializationError,
1207                    context: Box::new(ErrorContext {
1208                        operation: Some("ListParts: parse XML".into()),
1209                        bucket: Some(self.bucket.to_string()),
1210                        object_key: Some(self.key.to_string()),
1211                        ..Default::default()
1212                    }),
1213                    source: Some(Box::new(e)),
1214                })?;
1215
1216            Ok(ListPartsOutput {
1217                bucket: result.bucket,
1218                key: result.key,
1219                upload_id: result.upload_id,
1220                parts: result
1221                    .parts
1222                    .into_iter()
1223                    .map(|p| PartInfo {
1224                        part_number: p.part_number,
1225                        last_modified: p.last_modified,
1226                        etag: p.etag.trim_matches('"').to_string(),
1227                        size: p.size,
1228                    })
1229                    .collect(),
1230                max_parts: result.max_parts,
1231                is_truncated: result.is_truncated,
1232                next_part_number_marker: result.next_part_number_marker,
1233            })
1234        } else {
1235            Err(OssError {
1236                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
1237                    status_code: response.status().as_u16(),
1238                    code: String::new(),
1239                    message: String::new(),
1240                    request_id: String::new(),
1241                    host_id: String::new(),
1242                    resource: Some(self.key.to_string()),
1243                    string_to_sign: None,
1244                })),
1245                context: Box::new(ErrorContext {
1246                    operation: Some("ListParts".into()),
1247                    bucket: Some(self.bucket.to_string()),
1248                    object_key: Some(self.key.to_string()),
1249                    ..Default::default()
1250                }),
1251                source: None,
1252            })
1253        }
1254    }
1255}
1256
1257#[derive(Debug, Clone)]
1258pub struct ListPartsOutput {
1259    pub bucket: String,
1260    pub key: String,
1261    pub upload_id: String,
1262    pub parts: Vec<PartInfo>,
1263    pub max_parts: i32,
1264    pub is_truncated: bool,
1265    pub next_part_number_marker: String,
1266}
1267
1268#[derive(Debug, Clone)]
1269pub struct PartInfo {
1270    pub part_number: i32,
1271    pub last_modified: String,
1272    pub etag: String,
1273    pub size: u64,
1274}
1275
1276pub struct UploadPartCopyBuilder {
1277    client: Arc<OSSClientInner>,
1278    bucket: BucketName,
1279    key: ObjectKey,
1280    upload_id: String,
1281    part_number: u32,
1282    copy_source: Option<String>,
1283    copy_source_range: Option<String>,
1284    copy_source_if_match: Option<String>,
1285    copy_source_if_none_match: Option<String>,
1286    copy_source_if_modified_since: Option<String>,
1287    copy_source_if_unmodified_since: Option<String>,
1288}
1289
1290impl UploadPartCopyBuilder {
1291    pub(crate) fn new(
1292        client: Arc<OSSClientInner>,
1293        bucket: BucketName,
1294        key: ObjectKey,
1295        upload_id: impl Into<String>,
1296        part_number: u32,
1297    ) -> Self {
1298        Self {
1299            client,
1300            bucket,
1301            key,
1302            upload_id: upload_id.into(),
1303            part_number,
1304            copy_source: None,
1305            copy_source_range: None,
1306            copy_source_if_match: None,
1307            copy_source_if_none_match: None,
1308            copy_source_if_modified_since: None,
1309            copy_source_if_unmodified_since: None,
1310        }
1311    }
1312
1313    pub fn copy_source(mut self, source: impl Into<String>) -> Self {
1314        self.copy_source = Some(source.into());
1315        self
1316    }
1317
1318    pub fn copy_source_range(mut self, range: impl Into<String>) -> Self {
1319        self.copy_source_range = Some(range.into());
1320        self
1321    }
1322
1323    pub fn copy_source_if_match(mut self, etag: impl Into<String>) -> Self {
1324        self.copy_source_if_match = Some(etag.into());
1325        self
1326    }
1327
1328    pub fn copy_source_if_none_match(mut self, etag: impl Into<String>) -> Self {
1329        self.copy_source_if_none_match = Some(etag.into());
1330        self
1331    }
1332
1333    pub fn copy_source_if_modified_since(mut self, time: impl Into<String>) -> Self {
1334        self.copy_source_if_modified_since = Some(time.into());
1335        self
1336    }
1337
1338    pub fn copy_source_if_unmodified_since(mut self, time: impl Into<String>) -> Self {
1339        self.copy_source_if_unmodified_since = Some(time.into());
1340        self
1341    }
1342
1343    pub async fn send(self) -> Result<UploadPartCopyOutput> {
1344        let copy_source = self.copy_source.ok_or_else(|| OssError {
1345            kind: OssErrorKind::ValidationError,
1346            context: Box::new(ErrorContext {
1347                operation: Some("UploadPartCopy: copy_source is required".into()),
1348                bucket: Some(self.bucket.to_string()),
1349                object_key: Some(self.key.to_string()),
1350                ..Default::default()
1351            }),
1352            source: None,
1353        })?;
1354
1355        let endpoint = self.client.endpoint.clone();
1356        let uri = oss_endpoint_url(
1357            &endpoint,
1358            Some(self.bucket.as_str()),
1359            Some(self.key.as_str()),
1360        );
1361        let full_uri = format!(
1362            "{}?partNumber={}&uploadId={}",
1363            uri, self.part_number, self.upload_id
1364        );
1365
1366        let query_params: Vec<(String, String)> = vec![
1367            ("partNumber".into(), self.part_number.to_string()),
1368            ("uploadId".into(), self.upload_id.clone()),
1369        ];
1370
1371        let mut req = HttpRequest::builder()
1372            .method(http::Method::PUT)
1373            .uri(&full_uri);
1374
1375        req = req.header(
1376            http::HeaderName::from_static("x-oss-copy-source"),
1377            http::HeaderValue::from_str(&copy_source).map_err(|e| OssError {
1378                kind: OssErrorKind::ValidationError,
1379                context: Box::new(ErrorContext {
1380                    operation: Some("set x-oss-copy-source header".into()),
1381                    bucket: Some(self.bucket.to_string()),
1382                    object_key: Some(self.key.to_string()),
1383                    ..Default::default()
1384                }),
1385                source: Some(Box::new(e)),
1386            })?,
1387        );
1388
1389        if let Some(ref r) = self.copy_source_range {
1390            req = req.header(
1391                http::HeaderName::from_static("x-oss-copy-source-range"),
1392                http::HeaderValue::from_str(r).map_err(|e| OssError {
1393                    kind: OssErrorKind::ValidationError,
1394                    context: Box::new(ErrorContext {
1395                        operation: Some("set x-oss-copy-source-range header".into()),
1396                        bucket: Some(self.bucket.to_string()),
1397                        object_key: Some(self.key.to_string()),
1398                        ..Default::default()
1399                    }),
1400                    source: Some(Box::new(e)),
1401                })?,
1402            );
1403        }
1404
1405        if let Some(ref im) = self.copy_source_if_match {
1406            req = req.header(
1407                http::HeaderName::from_static("x-oss-copy-source-if-match"),
1408                http::HeaderValue::from_str(im).map_err(|e| OssError {
1409                    kind: OssErrorKind::ValidationError,
1410                    context: Box::new(ErrorContext {
1411                        operation: Some("set x-oss-copy-source-if-match header".into()),
1412                        bucket: Some(self.bucket.to_string()),
1413                        object_key: Some(self.key.to_string()),
1414                        ..Default::default()
1415                    }),
1416                    source: Some(Box::new(e)),
1417                })?,
1418            );
1419        }
1420
1421        if let Some(ref inm) = self.copy_source_if_none_match {
1422            req = req.header(
1423                http::HeaderName::from_static("x-oss-copy-source-if-none-match"),
1424                http::HeaderValue::from_str(inm).map_err(|e| OssError {
1425                    kind: OssErrorKind::ValidationError,
1426                    context: Box::new(ErrorContext {
1427                        operation: Some("set x-oss-copy-source-if-none-match header".into()),
1428                        bucket: Some(self.bucket.to_string()),
1429                        object_key: Some(self.key.to_string()),
1430                        ..Default::default()
1431                    }),
1432                    source: Some(Box::new(e)),
1433                })?,
1434            );
1435        }
1436
1437        if let Some(ref ims) = self.copy_source_if_modified_since {
1438            req = req.header(
1439                http::HeaderName::from_static("x-oss-copy-source-if-modified-since"),
1440                http::HeaderValue::from_str(ims).map_err(|e| OssError {
1441                    kind: OssErrorKind::ValidationError,
1442                    context: Box::new(ErrorContext {
1443                        operation: Some("set x-oss-copy-source-if-modified-since header".into()),
1444                        bucket: Some(self.bucket.to_string()),
1445                        object_key: Some(self.key.to_string()),
1446                        ..Default::default()
1447                    }),
1448                    source: Some(Box::new(e)),
1449                })?,
1450            );
1451        }
1452
1453        if let Some(ref ius) = self.copy_source_if_unmodified_since {
1454            req = req.header(
1455                http::HeaderName::from_static("x-oss-copy-source-if-unmodified-since"),
1456                http::HeaderValue::from_str(ius).map_err(|e| OssError {
1457                    kind: OssErrorKind::ValidationError,
1458                    context: Box::new(ErrorContext {
1459                        operation: Some("set x-oss-copy-source-if-unmodified-since header".into()),
1460                        bucket: Some(self.bucket.to_string()),
1461                        object_key: Some(self.key.to_string()),
1462                        ..Default::default()
1463                    }),
1464                    source: Some(Box::new(e)),
1465                })?,
1466            );
1467        }
1468
1469        let request = req.build();
1470
1471        let response = self
1472            .client
1473            .send_signed(request, Some(&self.bucket), query_params)
1474            .await
1475            .map_err(|e| OssError {
1476                kind: OssErrorKind::TransportError,
1477                context: Box::new(ErrorContext {
1478                    operation: Some("UploadPartCopy".into()),
1479                    bucket: Some(self.bucket.to_string()),
1480                    object_key: Some(self.key.to_string()),
1481                    endpoint: Some(endpoint),
1482                    ..Default::default()
1483                }),
1484                source: Some(Box::new(e)),
1485            })?;
1486
1487        if response.is_success() {
1488            let request_id = response
1489                .headers
1490                .get("x-oss-request-id")
1491                .and_then(|v| v.to_str().ok())
1492                .unwrap_or("")
1493                .to_string();
1494
1495            let body_str = response.body_as_str().unwrap_or("");
1496            let result: CopyPartResult =
1497                crate::util::xml::from_xml(body_str).map_err(|e| OssError {
1498                    kind: OssErrorKind::DeserializationError,
1499                    context: Box::new(ErrorContext {
1500                        operation: Some("UploadPartCopy: parse XML".into()),
1501                        bucket: Some(self.bucket.to_string()),
1502                        object_key: Some(self.key.to_string()),
1503                        ..Default::default()
1504                    }),
1505                    source: Some(Box::new(e)),
1506                })?;
1507
1508            Ok(UploadPartCopyOutput {
1509                request_id,
1510                etag: result.etag.trim_matches('"').to_string(),
1511                part_number: self.part_number,
1512                last_modified: result.last_modified,
1513            })
1514        } else {
1515            Err(OssError {
1516                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
1517                    status_code: response.status().as_u16(),
1518                    code: String::new(),
1519                    message: String::new(),
1520                    request_id: String::new(),
1521                    host_id: String::new(),
1522                    resource: Some(self.key.to_string()),
1523                    string_to_sign: None,
1524                })),
1525                context: Box::new(ErrorContext {
1526                    operation: Some("UploadPartCopy".into()),
1527                    bucket: Some(self.bucket.to_string()),
1528                    object_key: Some(self.key.to_string()),
1529                    ..Default::default()
1530                }),
1531                source: None,
1532            })
1533        }
1534    }
1535}
1536
1537#[derive(Debug, Clone)]
1538pub struct UploadPartCopyOutput {
1539    pub request_id: String,
1540    pub etag: String,
1541    pub part_number: u32,
1542    pub last_modified: String,
1543}
1544
1545impl BucketOperations {
1546    pub fn initiate_multipart_upload(
1547        &self,
1548        key: impl Into<String>,
1549    ) -> Result<InitiateMultipartUploadBuilder> {
1550        let object_key = ObjectKey::new(key.into())?;
1551        Ok(InitiateMultipartUploadBuilder::new(
1552            self.client_inner().clone(),
1553            self.bucket_name().clone(),
1554            object_key,
1555        ))
1556    }
1557
1558    pub fn upload_part(
1559        &self,
1560        key: impl Into<String>,
1561        upload_id: impl Into<String>,
1562        part_number: u32,
1563    ) -> Result<UploadPartBuilder> {
1564        let object_key = ObjectKey::new(key.into())?;
1565        Ok(UploadPartBuilder::new(
1566            self.client_inner().clone(),
1567            self.bucket_name().clone(),
1568            object_key,
1569            upload_id,
1570            part_number,
1571        ))
1572    }
1573
1574    pub fn upload_part_copy(
1575        &self,
1576        key: impl Into<String>,
1577        upload_id: impl Into<String>,
1578        part_number: u32,
1579    ) -> Result<UploadPartCopyBuilder> {
1580        let object_key = ObjectKey::new(key.into())?;
1581        Ok(UploadPartCopyBuilder::new(
1582            self.client_inner().clone(),
1583            self.bucket_name().clone(),
1584            object_key,
1585            upload_id,
1586            part_number,
1587        ))
1588    }
1589
1590    pub fn complete_multipart_upload(
1591        &self,
1592        key: impl Into<String>,
1593        upload_id: impl Into<String>,
1594    ) -> Result<CompleteMultipartUploadBuilder> {
1595        let object_key = ObjectKey::new(key.into())?;
1596        Ok(CompleteMultipartUploadBuilder::new(
1597            self.client_inner().clone(),
1598            self.bucket_name().clone(),
1599            object_key,
1600            upload_id,
1601        ))
1602    }
1603
1604    pub fn abort_multipart_upload(
1605        &self,
1606        key: impl Into<String>,
1607        upload_id: impl Into<String>,
1608    ) -> Result<AbortMultipartUploadBuilder> {
1609        let object_key = ObjectKey::new(key.into())?;
1610        Ok(AbortMultipartUploadBuilder::new(
1611            self.client_inner().clone(),
1612            self.bucket_name().clone(),
1613            object_key,
1614            upload_id,
1615        ))
1616    }
1617
1618    pub fn list_multipart_uploads(&self) -> ListMultipartUploadsBuilder {
1619        ListMultipartUploadsBuilder::new(self.client_inner().clone(), self.bucket_name().clone())
1620    }
1621
1622    pub fn list_parts(
1623        &self,
1624        key: impl Into<String>,
1625        upload_id: impl Into<String>,
1626    ) -> Result<ListPartsBuilder> {
1627        let object_key = ObjectKey::new(key.into())?;
1628        Ok(ListPartsBuilder::new(
1629            self.client_inner().clone(),
1630            self.bucket_name().clone(),
1631            object_key,
1632            upload_id,
1633        ))
1634    }
1635}
1636
1637#[cfg(test)]
1638mod tests {
1639    use std::str::FromStr;
1640    use std::sync::Mutex;
1641
1642    use crate::client::OSSClientInner;
1643    use crate::config::credentials::Credentials;
1644    use crate::http::client::{HttpClient, HttpRequest, HttpResponse};
1645    use crate::types::region::Region;
1646
1647    use super::*;
1648
1649    struct RecordingHttpClient {
1650        requests: Arc<Mutex<Vec<HttpRequest>>>,
1651        status_code: http::StatusCode,
1652        response_body: bytes::Bytes,
1653        response_headers: Vec<(&'static str, &'static str)>,
1654    }
1655
1656    #[async_trait::async_trait]
1657    impl HttpClient for RecordingHttpClient {
1658        async fn send(&self, request: HttpRequest) -> crate::error::Result<HttpResponse> {
1659            self.requests.lock().unwrap().push(request);
1660            let mut headers = http::HeaderMap::new();
1661            headers.insert(
1662                "x-oss-request-id",
1663                http::HeaderValue::from_static("rid-multipart"),
1664            );
1665            for (name, value) in &self.response_headers {
1666                if let (Ok(n), Ok(v)) = (
1667                    http::HeaderName::from_bytes(name.as_bytes()),
1668                    http::HeaderValue::from_str(value),
1669                ) {
1670                    headers.insert(n, v);
1671                }
1672            }
1673            Ok(HttpResponse {
1674                status: self.status_code,
1675                headers,
1676                body: self.response_body.clone(),
1677            })
1678        }
1679    }
1680
1681    fn create_test_inner_with_response(
1682        status_code: http::StatusCode,
1683        response_body: bytes::Bytes,
1684        response_headers: Vec<(&'static str, &'static str)>,
1685    ) -> (Arc<OSSClientInner>, Arc<Mutex<Vec<HttpRequest>>>) {
1686        let requests = Arc::new(Mutex::new(Vec::new()));
1687        let http = Arc::new(RecordingHttpClient {
1688            requests: requests.clone(),
1689            status_code,
1690            response_body,
1691            response_headers,
1692        });
1693        let credentials = Arc::new(crate::config::credentials::StaticCredentialsProvider::new(
1694            Credentials::builder()
1695                .access_key_id("test-ak")
1696                .access_key_secret("test-sk")
1697                .build()
1698                .unwrap(),
1699        ));
1700        let inner = Arc::new(OSSClientInner {
1701            http,
1702            credentials,
1703            signer: Arc::from(crate::signer::create_signer(crate::signer::SignVersion::V4)),
1704            region: Region::CnHangzhou,
1705            endpoint: "oss-cn-hangzhou.aliyuncs.com".into(),
1706        });
1707        (inner, requests)
1708    }
1709
1710    #[test]
1711    fn initiate_multipart_builder_has_bucket_and_key() {
1712        let (inner, _) =
1713            create_test_inner_with_response(http::StatusCode::OK, bytes::Bytes::new(), vec![]);
1714        let _builder = InitiateMultipartUploadBuilder::new(
1715            inner,
1716            BucketName::new("test-bucket").unwrap(),
1717            ObjectKey::new("test-key.txt").unwrap(),
1718        );
1719    }
1720
1721    #[tokio::test]
1722    async fn initiate_multipart_builder_sends_post_with_uploads_query() {
1723        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1724<InitiateMultipartUploadResult>
1725  <Bucket>oss-bucket</Bucket>
1726  <Key>test-key.txt</Key>
1727  <UploadId>upload-id-123</UploadId>
1728</InitiateMultipartUploadResult>"#;
1729        let (inner, requests) =
1730            create_test_inner_with_response(http::StatusCode::OK, bytes::Bytes::from(xml), vec![]);
1731        let builder = InitiateMultipartUploadBuilder::new(
1732            inner,
1733            BucketName::new("test-bucket").unwrap(),
1734            ObjectKey::new("test-key.txt").unwrap(),
1735        );
1736
1737        let output = builder.send().await.unwrap();
1738        assert_eq!(output.upload_id, "upload-id-123");
1739        assert_eq!(output.bucket, "oss-bucket");
1740        assert_eq!(output.key, "test-key.txt");
1741
1742        let captured = requests.lock().unwrap();
1743        assert_eq!(captured[0].method, http::Method::POST);
1744        assert!(captured[0].uri.contains("?uploads"));
1745    }
1746
1747    #[tokio::test]
1748    async fn initiate_multipart_builder_sets_optional_headers() {
1749        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1750<InitiateMultipartUploadResult>
1751  <Bucket>b</Bucket>
1752  <Key>k</Key>
1753  <UploadId>uid</UploadId>
1754</InitiateMultipartUploadResult>"#;
1755        let (inner, requests) =
1756            create_test_inner_with_response(http::StatusCode::OK, bytes::Bytes::from(xml), vec![]);
1757        let builder = InitiateMultipartUploadBuilder::new(
1758            inner,
1759            BucketName::new("test-bucket").unwrap(),
1760            ObjectKey::new("test-key.txt").unwrap(),
1761        )
1762        .cache_control("max-age=300")
1763        .content_type("application/octet-stream")
1764        .acl(ObjectAcl::Private)
1765        .storage_class(StorageClass::Standard);
1766
1767        builder.send().await.unwrap();
1768
1769        let captured = requests.lock().unwrap();
1770        let has_header = |name: &str, val: &str| -> bool {
1771            captured[0]
1772                .headers
1773                .get(http::HeaderName::from_bytes(name.as_bytes()).unwrap())
1774                .map(|v| v.to_str().ok() == Some(val))
1775                .unwrap_or(false)
1776        };
1777        assert!(has_header("cache-control", "max-age=300"));
1778        assert!(has_header("content-type", "application/octet-stream"));
1779        assert!(has_header("x-oss-object-acl", "private"));
1780        assert!(has_header("x-oss-storage-class", "Standard"));
1781    }
1782
1783    #[tokio::test]
1784    async fn initiate_multipart_returns_error_on_failure() {
1785        let (inner, _) = create_test_inner_with_response(
1786            http::StatusCode::BAD_REQUEST,
1787            bytes::Bytes::from(""),
1788            vec![],
1789        );
1790        let builder = InitiateMultipartUploadBuilder::new(
1791            inner,
1792            BucketName::new("test-bucket").unwrap(),
1793            ObjectKey::new("test-key.txt").unwrap(),
1794        );
1795
1796        let result = builder.send().await;
1797        assert!(result.is_err());
1798    }
1799
1800    #[tokio::test]
1801    #[ignore = "requires valid OSS credentials"]
1802    async fn e2e_initiate_multipart_upload() {
1803        let ak = std::env::var("OSS_ACCESS_KEY_ID").expect("OSS_ACCESS_KEY_ID not set");
1804        let sk = std::env::var("OSS_ACCESS_KEY_SECRET").expect("OSS_ACCESS_KEY_SECRET not set");
1805        let region_str = std::env::var("OSS_REGION").unwrap_or_else(|_| "cn-wulanchabu".into());
1806        let bucket_str = std::env::var("OSS_BUCKET").expect("OSS_BUCKET not set");
1807
1808        let region = Region::from_str(&region_str).unwrap_or_else(|_| Region::Custom {
1809            endpoint: format!("oss-{}.aliyuncs.com", region_str),
1810            region_id: region_str.clone(),
1811        });
1812
1813        let client = crate::client::OSSClient::builder()
1814            .region(region)
1815            .credentials(ak, sk)
1816            .build()
1817            .unwrap();
1818
1819        let key = format!("test-multipart-{}.bin", chrono::Utc::now().timestamp());
1820        let output = client
1821            .bucket(&bucket_str)
1822            .unwrap()
1823            .initiate_multipart_upload(&key)
1824            .unwrap()
1825            .send()
1826            .await
1827            .unwrap();
1828
1829        assert!(!output.upload_id.is_empty());
1830        assert_eq!(output.key, key);
1831        eprintln!(
1832            "InitiateMultipartUpload: key={}, upload_id={}",
1833            output.key, output.upload_id
1834        );
1835    }
1836
1837    #[tokio::test]
1838    async fn upload_part_builder_rejects_part_number_zero() {
1839        let (inner, _) =
1840            create_test_inner_with_response(http::StatusCode::OK, bytes::Bytes::new(), vec![]);
1841        let builder = UploadPartBuilder::new(
1842            inner,
1843            BucketName::new("test-bucket").unwrap(),
1844            ObjectKey::new("k").unwrap(),
1845            "upload-id",
1846            0,
1847        )
1848        .body(bytes::Bytes::from("data"));
1849        let result = builder.send().await;
1850        assert!(result.is_err());
1851    }
1852
1853    #[tokio::test]
1854    async fn upload_part_builder_rejects_part_number_over_10000() {
1855        let (inner, _) =
1856            create_test_inner_with_response(http::StatusCode::OK, bytes::Bytes::new(), vec![]);
1857        let builder = UploadPartBuilder::new(
1858            inner,
1859            BucketName::new("test-bucket").unwrap(),
1860            ObjectKey::new("k").unwrap(),
1861            "upload-id",
1862            10001,
1863        )
1864        .body(bytes::Bytes::from("data"));
1865        let result = builder.send().await;
1866        assert!(result.is_err());
1867    }
1868
1869    #[test]
1870    fn content_md5_computation_known_answer() {
1871        let body = b"hello world";
1872        let md5 = UploadPartBuilder::compute_md5(body);
1873        assert_eq!(md5, "XrY7u+Ae7tCTyyK7j1rNww==");
1874    }
1875
1876    #[tokio::test]
1877    async fn upload_part_returns_etag_from_response() {
1878        let (inner, requests) = create_test_inner_with_response(
1879            http::StatusCode::OK,
1880            bytes::Bytes::new(),
1881            vec![("ETag", "\"abc123\"")],
1882        );
1883        let builder = UploadPartBuilder::new(
1884            inner,
1885            BucketName::new("test-bucket").unwrap(),
1886            ObjectKey::new("test-key.txt").unwrap(),
1887            "upload-123",
1888            1,
1889        )
1890        .body(bytes::Bytes::from("part data"));
1891
1892        let output = builder.send().await.unwrap();
1893        assert_eq!(output.etag, "abc123");
1894        assert_eq!(output.part_number, 1);
1895
1896        let captured = requests.lock().unwrap();
1897        assert_eq!(captured[0].method, http::Method::PUT);
1898        assert!(captured[0].uri.contains("partNumber=1"));
1899        assert!(captured[0].uri.contains("uploadId=upload-123"));
1900    }
1901
1902    #[tokio::test]
1903    #[ignore = "requires valid OSS credentials"]
1904    async fn e2e_upload_part() {
1905        let ak = std::env::var("OSS_ACCESS_KEY_ID").expect("OSS_ACCESS_KEY_ID not set");
1906        let sk = std::env::var("OSS_ACCESS_KEY_SECRET").expect("OSS_ACCESS_KEY_SECRET not set");
1907        let region_str = std::env::var("OSS_REGION").unwrap_or_else(|_| "cn-wulanchabu".into());
1908        let bucket_str = std::env::var("OSS_BUCKET").expect("OSS_BUCKET not set");
1909
1910        let region = Region::from_str(&region_str).unwrap_or_else(|_| Region::Custom {
1911            endpoint: format!("oss-{}.aliyuncs.com", region_str),
1912            region_id: region_str.clone(),
1913        });
1914
1915        let client = crate::client::OSSClient::builder()
1916            .region(region)
1917            .credentials(ak, sk)
1918            .build()
1919            .unwrap();
1920
1921        let key = format!("test-part-{}.bin", chrono::Utc::now().timestamp());
1922        let upload_id = client
1923            .bucket(&bucket_str)
1924            .unwrap()
1925            .initiate_multipart_upload(&key)
1926            .unwrap()
1927            .send()
1928            .await
1929            .unwrap()
1930            .upload_id;
1931
1932        let output = client
1933            .bucket(&bucket_str)
1934            .unwrap()
1935            .upload_part(&key, &upload_id, 1)
1936            .unwrap()
1937            .body(bytes::Bytes::from("hello part"))
1938            .send()
1939            .await
1940            .unwrap();
1941
1942        assert!(!output.etag.is_empty());
1943        eprintln!(
1944            "UploadPart: part_number={}, etag={}",
1945            output.part_number, output.etag
1946        );
1947    }
1948
1949    #[tokio::test]
1950    async fn upload_part_copy_requires_copy_source() {
1951        let (inner, _) =
1952            create_test_inner_with_response(http::StatusCode::OK, bytes::Bytes::new(), vec![]);
1953        let builder = UploadPartCopyBuilder::new(
1954            inner,
1955            BucketName::new("test-bucket").unwrap(),
1956            ObjectKey::new("k").unwrap(),
1957            "upload-id",
1958            1,
1959        );
1960        let result = builder.send().await;
1961        assert!(result.is_err());
1962    }
1963
1964    #[tokio::test]
1965    async fn upload_part_copy_parses_etag_from_xml() {
1966        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1967<CopyPartResult>
1968  <ETag>"abc123"</ETag>
1969  <LastModified>2024-01-01T00:00:00.000Z</LastModified>
1970</CopyPartResult>"#;
1971        let (inner, requests) =
1972            create_test_inner_with_response(http::StatusCode::OK, bytes::Bytes::from(xml), vec![]);
1973        let builder = UploadPartCopyBuilder::new(
1974            inner,
1975            BucketName::new("test-bucket").unwrap(),
1976            ObjectKey::new("dest-key.txt").unwrap(),
1977            "upload-123",
1978            1,
1979        )
1980        .copy_source("/src-bucket/src-key");
1981
1982        let output = builder.send().await.unwrap();
1983        assert_eq!(output.etag, "abc123");
1984        assert_eq!(output.part_number, 1);
1985
1986        let captured = requests.lock().unwrap();
1987        assert!(
1988            captured[0]
1989                .headers
1990                .get("x-oss-copy-source")
1991                .map(|v| v.to_str().ok() == Some("/src-bucket/src-key"))
1992                .unwrap_or(false)
1993        );
1994    }
1995
1996    #[tokio::test]
1997    #[ignore = "requires valid OSS credentials"]
1998    async fn e2e_upload_part_copy() {
1999        let ak = std::env::var("OSS_ACCESS_KEY_ID").expect("OSS_ACCESS_KEY_ID not set");
2000        let sk = std::env::var("OSS_ACCESS_KEY_SECRET").expect("OSS_ACCESS_KEY_SECRET not set");
2001        let region_str = std::env::var("OSS_REGION").unwrap_or_else(|_| "cn-wulanchabu".into());
2002        let bucket_str = std::env::var("OSS_BUCKET").expect("OSS_BUCKET not set");
2003
2004        let region = Region::from_str(&region_str).unwrap_or_else(|_| Region::Custom {
2005            endpoint: format!("oss-{}.aliyuncs.com", region_str),
2006            region_id: region_str.clone(),
2007        });
2008
2009        let client = crate::client::OSSClient::builder()
2010            .region(region)
2011            .credentials(ak, sk)
2012            .build()
2013            .unwrap();
2014
2015        let src_key = format!("test-pc-src-{}.txt", chrono::Utc::now().timestamp());
2016        let dst_key = format!("test-pc-dst-{}.bin", chrono::Utc::now().timestamp());
2017
2018        client
2019            .bucket(&bucket_str)
2020            .unwrap()
2021            .put_object(&src_key)
2022            .unwrap()
2023            .body(bytes::Bytes::from("source content for copy"))
2024            .send()
2025            .await
2026            .unwrap();
2027
2028        let upload_id = client
2029            .bucket(&bucket_str)
2030            .unwrap()
2031            .initiate_multipart_upload(&dst_key)
2032            .unwrap()
2033            .send()
2034            .await
2035            .unwrap()
2036            .upload_id;
2037
2038        let copy_source = format!("/{}/{}", bucket_str, src_key);
2039        let output = client
2040            .bucket(&bucket_str)
2041            .unwrap()
2042            .upload_part_copy(&dst_key, &upload_id, 1)
2043            .unwrap()
2044            .copy_source(&copy_source)
2045            .send()
2046            .await
2047            .unwrap();
2048
2049        assert!(!output.etag.is_empty());
2050        eprintln!("UploadPartCopy: etag={}", output.etag);
2051    }
2052
2053    #[test]
2054    fn complete_multipart_xml_contains_parts() {
2055        let complete = CompleteMultipartUpload {
2056            parts: vec![
2057                UploadPartItem {
2058                    part_number: 1,
2059                    etag: "etag1".into(),
2060                },
2061                UploadPartItem {
2062                    part_number: 2,
2063                    etag: "etag2".into(),
2064                },
2065            ],
2066        };
2067        let xml = crate::util::xml::to_xml(&complete).unwrap();
2068        assert!(xml.contains("<PartNumber>1</PartNumber>"));
2069        assert!(xml.contains("<ETag>etag1</ETag>"));
2070        assert!(xml.contains("<PartNumber>2</PartNumber>"));
2071        assert!(xml.contains("<ETag>etag2</ETag>"));
2072    }
2073
2074    #[tokio::test]
2075    async fn complete_multipart_requires_at_least_one_part() {
2076        let (inner, _) =
2077            create_test_inner_with_response(http::StatusCode::OK, bytes::Bytes::new(), vec![]);
2078        let builder = CompleteMultipartUploadBuilder::new(
2079            inner,
2080            BucketName::new("test-bucket").unwrap(),
2081            ObjectKey::new("k").unwrap(),
2082            "upload-id",
2083        );
2084        let result = builder.send().await;
2085        assert!(result.is_err());
2086    }
2087
2088    #[tokio::test]
2089    async fn complete_multipart_parses_result_xml() {
2090        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
2091<CompleteMultipartUploadResult>
2092  <Location>http://bucket.oss-cn-hangzhou.aliyuncs.com/key</Location>
2093  <Bucket>bucket</Bucket>
2094  <Key>key</Key>
2095  <ETag>"final-etag"</ETag>
2096</CompleteMultipartUploadResult>"#;
2097        let (inner, requests) =
2098            create_test_inner_with_response(http::StatusCode::OK, bytes::Bytes::from(xml), vec![]);
2099        let builder = CompleteMultipartUploadBuilder::new(
2100            inner,
2101            BucketName::new("test-bucket").unwrap(),
2102            ObjectKey::new("key").unwrap(),
2103            "upload-id",
2104        )
2105        .part(1, "etag1");
2106
2107        let output = builder.send().await.unwrap();
2108        assert_eq!(output.etag, "final-etag");
2109        assert_eq!(output.bucket, "bucket");
2110        assert_eq!(output.key, "key");
2111
2112        let captured = requests.lock().unwrap();
2113        assert_eq!(captured[0].method, http::Method::POST);
2114        assert!(captured[0].uri.contains("uploadId=upload-id"));
2115    }
2116
2117    #[tokio::test]
2118    #[ignore = "requires valid OSS credentials"]
2119    async fn e2e_complete_multipart_upload() {
2120        let ak = std::env::var("OSS_ACCESS_KEY_ID").expect("OSS_ACCESS_KEY_ID not set");
2121        let sk = std::env::var("OSS_ACCESS_KEY_SECRET").expect("OSS_ACCESS_KEY_SECRET not set");
2122        let region_str = std::env::var("OSS_REGION").unwrap_or_else(|_| "cn-wulanchabu".into());
2123        let bucket_str = std::env::var("OSS_BUCKET").expect("OSS_BUCKET not set");
2124
2125        let region = Region::from_str(&region_str).unwrap_or_else(|_| Region::Custom {
2126            endpoint: format!("oss-{}.aliyuncs.com", region_str),
2127            region_id: region_str.clone(),
2128        });
2129
2130        let client = crate::client::OSSClient::builder()
2131            .region(region)
2132            .credentials(ak, sk)
2133            .build()
2134            .unwrap();
2135
2136        let key = format!("test-complete-{}.bin", chrono::Utc::now().timestamp());
2137        let bucket = client.bucket(&bucket_str).unwrap();
2138
2139        let upload_id = bucket
2140            .initiate_multipart_upload(&key)
2141            .unwrap()
2142            .send()
2143            .await
2144            .unwrap()
2145            .upload_id;
2146
2147        let part = bucket
2148            .upload_part(&key, &upload_id, 1)
2149            .unwrap()
2150            .body(bytes::Bytes::from("hello"))
2151            .send()
2152            .await
2153            .unwrap();
2154
2155        let output = bucket
2156            .complete_multipart_upload(&key, &upload_id)
2157            .unwrap()
2158            .part(1, &part.etag)
2159            .send()
2160            .await
2161            .unwrap();
2162
2163        assert!(!output.etag.is_empty());
2164        assert_eq!(output.key, key);
2165        eprintln!(
2166            "CompleteMultipart: key={}, etag={}",
2167            output.key, output.etag
2168        );
2169    }
2170
2171    #[tokio::test]
2172    async fn abort_multipart_sends_delete_request() {
2173        let (inner, requests) = create_test_inner_with_response(
2174            http::StatusCode::NO_CONTENT,
2175            bytes::Bytes::new(),
2176            vec![],
2177        );
2178        let builder = AbortMultipartUploadBuilder::new(
2179            inner,
2180            BucketName::new("test-bucket").unwrap(),
2181            ObjectKey::new("k").unwrap(),
2182            "upload-123",
2183        );
2184
2185        let output = builder.send().await.unwrap();
2186        assert!(!output.request_id.is_empty());
2187
2188        let captured = requests.lock().unwrap();
2189        assert_eq!(captured[0].method, http::Method::DELETE);
2190        assert!(captured[0].uri.contains("uploadId=upload-123"));
2191    }
2192
2193    #[tokio::test]
2194    #[ignore = "requires valid OSS credentials"]
2195    async fn e2e_abort_multipart_upload() {
2196        let ak = std::env::var("OSS_ACCESS_KEY_ID").expect("OSS_ACCESS_KEY_ID not set");
2197        let sk = std::env::var("OSS_ACCESS_KEY_SECRET").expect("OSS_ACCESS_KEY_SECRET not set");
2198        let region_str = std::env::var("OSS_REGION").unwrap_or_else(|_| "cn-wulanchabu".into());
2199        let bucket_str = std::env::var("OSS_BUCKET").expect("OSS_BUCKET not set");
2200
2201        let region = Region::from_str(&region_str).unwrap_or_else(|_| Region::Custom {
2202            endpoint: format!("oss-{}.aliyuncs.com", region_str),
2203            region_id: region_str.clone(),
2204        });
2205
2206        let client = crate::client::OSSClient::builder()
2207            .region(region)
2208            .credentials(ak, sk)
2209            .build()
2210            .unwrap();
2211
2212        let key = format!("test-abort-{}.bin", chrono::Utc::now().timestamp());
2213        let bucket = client.bucket(&bucket_str).unwrap();
2214
2215        let upload_id = bucket
2216            .initiate_multipart_upload(&key)
2217            .unwrap()
2218            .send()
2219            .await
2220            .unwrap()
2221            .upload_id;
2222
2223        let output = bucket
2224            .abort_multipart_upload(&key, &upload_id)
2225            .unwrap()
2226            .send()
2227            .await
2228            .unwrap();
2229
2230        assert!(!output.request_id.is_empty());
2231        eprintln!("AbortMultipartUpload: key={}", key);
2232    }
2233
2234    #[tokio::test]
2235    async fn list_multipart_uploads_parses_xml() {
2236        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
2237<ListMultipartUploadsResult>
2238  <Bucket>bucket</Bucket>
2239  <Upload>
2240    <Key>obj1</Key>
2241    <UploadId>upload-id-1</UploadId>
2242    <Initiated>2024-01-01T00:00:00.000Z</Initiated>
2243  </Upload>
2244  <IsTruncated>false</IsTruncated>
2245  <MaxUploads>100</MaxUploads>
2246</ListMultipartUploadsResult>"#;
2247        let (inner, _) =
2248            create_test_inner_with_response(http::StatusCode::OK, bytes::Bytes::from(xml), vec![]);
2249        let builder =
2250            ListMultipartUploadsBuilder::new(inner, BucketName::new("test-bucket").unwrap());
2251        let output = builder.send().await.unwrap();
2252        assert_eq!(output.uploads.len(), 1);
2253        assert_eq!(output.uploads[0].key, "obj1");
2254        assert!(!output.is_truncated);
2255    }
2256
2257    #[tokio::test]
2258    async fn list_parts_parses_xml() {
2259        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
2260<ListPartsResult>
2261  <Bucket>bucket</Bucket>
2262  <Key>key</Key>
2263  <UploadId>upload-123</UploadId>
2264  <Part>
2265    <PartNumber>1</PartNumber>
2266    <LastModified>2024-01-01T00:00:00.000Z</LastModified>
2267    <ETag>"etag1"</ETag>
2268    <Size>1024</Size>
2269  </Part>
2270  <MaxParts>1000</MaxParts>
2271  <IsTruncated>false</IsTruncated>
2272</ListPartsResult>"#;
2273        let (inner, _) =
2274            create_test_inner_with_response(http::StatusCode::OK, bytes::Bytes::from(xml), vec![]);
2275        let builder = ListPartsBuilder::new(
2276            inner,
2277            BucketName::new("test-bucket").unwrap(),
2278            ObjectKey::new("key").unwrap(),
2279            "upload-123",
2280        );
2281        let output = builder.send().await.unwrap();
2282        assert_eq!(output.parts.len(), 1);
2283        assert_eq!(output.parts[0].part_number, 1);
2284        assert_eq!(output.parts[0].etag, "etag1");
2285    }
2286}