1use std::sync::Arc;
4
5use serde::{Deserialize, Serialize};
6
7use crate::client::{BucketOperations, OSSClientInner};
8use crate::error::{ErrorContext, OssError, OssErrorKind, Result};
9use crate::http::client::HttpRequest;
10use crate::types::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(©_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(®ion_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(®ion_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(®ion_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(©_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(®ion_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(®ion_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}