1use std::fmt::Debug;
19use std::sync::Arc;
20
21use bytes::Bytes;
22use constants::X_OSS_META_PREFIX;
23use http::HeaderMap;
24use http::HeaderName;
25use http::HeaderValue;
26use http::Request;
27use http::Response;
28use http::header::CACHE_CONTROL;
29use http::header::CONTENT_DISPOSITION;
30use http::header::CONTENT_LENGTH;
31use http::header::CONTENT_TYPE;
32use http::header::IF_MATCH;
33use http::header::IF_MODIFIED_SINCE;
34use http::header::IF_NONE_MATCH;
35use http::header::IF_UNMODIFIED_SINCE;
36use http::header::RANGE;
37use reqsign::AliyunCredential;
38use reqsign::AliyunLoader;
39use reqsign::AliyunOssSigner;
40use serde::Deserialize;
41use serde::Serialize;
42
43use crate::raw::*;
44use crate::services::oss::core::constants::X_OSS_FORBID_OVERWRITE;
45use crate::*;
46
47pub mod constants {
48 pub const X_OSS_SERVER_SIDE_ENCRYPTION: &str = "x-oss-server-side-encryption";
49
50 pub const X_OSS_SERVER_SIDE_ENCRYPTION_KEY_ID: &str = "x-oss-server-side-encryption-key-id";
51
52 pub const X_OSS_FORBID_OVERWRITE: &str = "x-oss-forbid-overwrite";
53
54 pub const X_OSS_VERSION_ID: &str = "x-oss-version-id";
55
56 pub const RESPONSE_CONTENT_DISPOSITION: &str = "response-content-disposition";
57
58 pub const OSS_QUERY_VERSION_ID: &str = "versionId";
59
60 pub const X_OSS_META_PREFIX: &str = "x-oss-meta-";
61}
62
63pub struct OssCore {
64 pub info: Arc<AccessorInfo>,
65
66 pub root: String,
67 pub bucket: String,
68 pub host: String,
72 pub endpoint: String,
73 pub presign_endpoint: String,
74 pub allow_anonymous: bool,
75
76 pub server_side_encryption: Option<HeaderValue>,
77 pub server_side_encryption_key_id: Option<HeaderValue>,
78
79 pub loader: AliyunLoader,
80 pub signer: AliyunOssSigner,
81}
82
83impl Debug for OssCore {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 f.debug_struct("OssCore")
86 .field("root", &self.root)
87 .field("bucket", &self.bucket)
88 .field("endpoint", &self.endpoint)
89 .field("host", &self.host)
90 .finish_non_exhaustive()
91 }
92}
93
94impl OssCore {
95 async fn load_credential(&self) -> Result<Option<AliyunCredential>> {
96 let cred = self
97 .loader
98 .load()
99 .await
100 .map_err(new_request_credential_error)?;
101
102 if let Some(cred) = cred {
103 Ok(Some(cred))
104 } else if self.allow_anonymous {
105 Ok(None)
107 } else {
108 Err(Error::new(
110 ErrorKind::PermissionDenied,
111 "no valid credential found, please check configuration or try again",
112 )
113 .set_temporary())
114 }
115 }
116
117 pub async fn sign<T>(&self, req: &mut Request<T>) -> Result<()> {
118 let cred = if let Some(cred) = self.load_credential().await? {
119 cred
120 } else {
121 return Ok(());
122 };
123
124 self.signer.sign(req, &cred).map_err(new_request_sign_error)
125 }
126
127 pub async fn sign_query<T>(&self, req: &mut Request<T>, duration: Duration) -> Result<()> {
128 let cred = if let Some(cred) = self.load_credential().await? {
129 cred
130 } else {
131 return Ok(());
132 };
133
134 self.signer
135 .sign_query(req, duration, &cred)
136 .map_err(new_request_sign_error)
137 }
138
139 #[inline]
140 pub async fn send(&self, req: Request<Buffer>) -> Result<Response<Buffer>> {
141 self.info.http_client().send(req).await
142 }
143
144 pub fn insert_sse_headers(&self, mut req: http::request::Builder) -> http::request::Builder {
148 if let Some(v) = &self.server_side_encryption {
149 let mut v = v.clone();
150 v.set_sensitive(true);
151
152 req = req.header(
153 HeaderName::from_static(constants::X_OSS_SERVER_SIDE_ENCRYPTION),
154 v,
155 )
156 }
157 if let Some(v) = &self.server_side_encryption_key_id {
158 let mut v = v.clone();
159 v.set_sensitive(true);
160
161 req = req.header(
162 HeaderName::from_static(constants::X_OSS_SERVER_SIDE_ENCRYPTION_KEY_ID),
163 v,
164 )
165 }
166 req
167 }
168
169 fn insert_metadata_headers(
170 &self,
171 mut req: http::request::Builder,
172 size: Option<u64>,
173 args: &OpWrite,
174 ) -> Result<http::request::Builder> {
175 req = req.header(CONTENT_LENGTH, size.unwrap_or_default());
176
177 if let Some(mime) = args.content_type() {
178 req = req.header(CONTENT_TYPE, mime);
179 }
180
181 if let Some(pos) = args.content_disposition() {
182 req = req.header(CONTENT_DISPOSITION, pos);
183 }
184
185 if let Some(cache_control) = args.cache_control() {
186 req = req.header(CACHE_CONTROL, cache_control);
187 }
188
189 if args.if_not_exists() {
200 req = req.header(X_OSS_FORBID_OVERWRITE, "true");
201 }
202
203 if let Some(user_metadata) = args.user_metadata() {
204 for (key, value) in user_metadata {
205 if !self.check_user_metadata_key(key) {
207 return Err(Error::new(
208 ErrorKind::Unsupported,
209 "the format of the user metadata key is invalid, please refer the document",
210 ));
211 }
212 req = req.header(format!("{X_OSS_META_PREFIX}{key}"), value)
213 }
214 }
215
216 Ok(req)
217 }
218
219 fn check_user_metadata_key(&self, key: &str) -> bool {
222 key.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
223 }
224
225 pub fn parse_metadata(&self, path: &str, headers: &HeaderMap) -> Result<Metadata> {
236 let mut m = parse_into_metadata(path, headers)?;
237 let user_meta = parse_prefixed_headers(headers, X_OSS_META_PREFIX);
238 if !user_meta.is_empty() {
239 m = m.with_user_metadata(user_meta);
240 }
241
242 Ok(m)
243 }
244}
245
246impl OssCore {
247 #[allow(clippy::too_many_arguments)]
248 pub fn oss_put_object_request(
249 &self,
250 path: &str,
251 size: Option<u64>,
252 args: &OpWrite,
253 body: Buffer,
254 is_presign: bool,
255 ) -> Result<Request<Buffer>> {
256 let p = build_abs_path(&self.root, path);
257 let endpoint = self.get_endpoint(is_presign);
258 let url = format!("{}/{}", endpoint, percent_encode_path(&p));
259
260 let mut req = Request::put(&url);
261
262 req = self.insert_metadata_headers(req, size, args)?;
263
264 req = self.insert_sse_headers(req);
266
267 let req = req.extension(Operation::Write);
268
269 let req = req.body(body).map_err(new_request_build_error)?;
270 Ok(req)
271 }
272
273 pub fn oss_append_object_request(
274 &self,
275 path: &str,
276 position: u64,
277 size: u64,
278 args: &OpWrite,
279 body: Buffer,
280 ) -> Result<Request<Buffer>> {
281 let p = build_abs_path(&self.root, path);
282 let endpoint = self.get_endpoint(false);
283 let url = format!(
284 "{}/{}?append&position={}",
285 endpoint,
286 percent_encode_path(&p),
287 position
288 );
289
290 let mut req = Request::post(&url);
291
292 req = self.insert_metadata_headers(req, Some(size), args)?;
293
294 req = self.insert_sse_headers(req);
296
297 let req = req.extension(Operation::Write);
298
299 let req = req.body(body).map_err(new_request_build_error)?;
300 Ok(req)
301 }
302
303 pub fn oss_get_object_request(
304 &self,
305 path: &str,
306 is_presign: bool,
307 args: &OpRead,
308 ) -> Result<Request<Buffer>> {
309 let p = build_abs_path(&self.root, path);
310 let endpoint = self.get_endpoint(is_presign);
311 let range = args.range();
312 let mut url = format!("{}/{}", endpoint, percent_encode_path(&p));
313
314 let mut query_args = Vec::new();
316 if let Some(override_content_disposition) = args.override_content_disposition() {
317 query_args.push(format!(
318 "{}={}",
319 constants::RESPONSE_CONTENT_DISPOSITION,
320 percent_encode_path(override_content_disposition)
321 ))
322 }
323 if let Some(version) = args.version() {
324 query_args.push(format!(
325 "{}={}",
326 constants::OSS_QUERY_VERSION_ID,
327 percent_encode_path(version)
328 ))
329 }
330
331 if !query_args.is_empty() {
332 url.push_str(&format!("?{}", query_args.join("&")));
333 }
334
335 let mut req = Request::get(&url);
336
337 if !range.is_full() {
338 req = req.header(RANGE, range.to_header());
339 req = req.header("x-oss-range-behavior", "standard");
342 }
343
344 if let Some(if_match) = args.if_match() {
345 req = req.header(IF_MATCH, if_match)
346 }
347 if let Some(if_none_match) = args.if_none_match() {
348 req = req.header(IF_NONE_MATCH, if_none_match);
349 }
350
351 if let Some(if_modified_since) = args.if_modified_since() {
352 req = req.header(IF_MODIFIED_SINCE, if_modified_since.format_http_date());
353 }
354
355 if let Some(if_unmodified_since) = args.if_unmodified_since() {
356 req = req.header(IF_UNMODIFIED_SINCE, if_unmodified_since.format_http_date());
357 }
358
359 let req = req.extension(Operation::Read);
360
361 let req = req.body(Buffer::new()).map_err(new_request_build_error)?;
362
363 Ok(req)
364 }
365
366 fn oss_delete_object_request(&self, path: &str, args: &OpDelete) -> Result<Request<Buffer>> {
367 let p = build_abs_path(&self.root, path);
368 let endpoint = self.get_endpoint(false);
369 let mut url = format!("{}/{}", endpoint, percent_encode_path(&p));
370
371 let mut query_args = Vec::new();
372
373 if let Some(version) = args.version() {
374 query_args.push(format!(
375 "{}={}",
376 constants::OSS_QUERY_VERSION_ID,
377 percent_encode_path(version)
378 ))
379 }
380
381 if !query_args.is_empty() {
382 url.push_str(&format!("?{}", query_args.join("&")));
383 }
384
385 let req = Request::delete(&url);
386
387 let req = req.extension(Operation::Delete);
388
389 let req = req.body(Buffer::new()).map_err(new_request_build_error)?;
390
391 Ok(req)
392 }
393
394 pub fn oss_head_object_request(
395 &self,
396 path: &str,
397 is_presign: bool,
398 args: &OpStat,
399 ) -> Result<Request<Buffer>> {
400 let p = build_abs_path(&self.root, path);
401 let endpoint = self.get_endpoint(is_presign);
402 let mut url = format!("{}/{}", endpoint, percent_encode_path(&p));
403
404 let mut query_args = Vec::new();
405
406 if let Some(version) = args.version() {
407 query_args.push(format!(
408 "{}={}",
409 constants::OSS_QUERY_VERSION_ID,
410 percent_encode_path(version)
411 ))
412 }
413
414 if !query_args.is_empty() {
415 url.push_str(&format!("?{}", query_args.join("&")));
416 }
417
418 let mut req = Request::head(&url);
419 if let Some(if_match) = args.if_match() {
420 req = req.header(IF_MATCH, if_match)
421 }
422 if let Some(if_none_match) = args.if_none_match() {
423 req = req.header(IF_NONE_MATCH, if_none_match);
424 }
425
426 let req = req.extension(Operation::Stat);
427
428 let req = req.body(Buffer::new()).map_err(new_request_build_error)?;
429
430 Ok(req)
431 }
432
433 pub fn oss_list_object_request(
434 &self,
435 path: &str,
436 token: &str,
437 delimiter: &str,
438 limit: Option<usize>,
439 start_after: Option<String>,
440 ) -> Result<Request<Buffer>> {
441 let p = build_abs_path(&self.root, path);
442
443 let endpoint = self.get_endpoint(false);
444 let mut url = QueryPairsWriter::new(endpoint);
445 url = url.push("list-type", "2");
446 if !delimiter.is_empty() {
447 url = url.push("delimiter", delimiter);
448 }
449 if !p.is_empty() {
451 url = url.push("prefix", &percent_encode_path(&p));
452 }
453
454 if let Some(limit) = limit {
456 url = url.push("max-keys", &limit.to_string());
457 }
458
459 if !token.is_empty() {
461 url = url.push("continuation-token", &percent_encode_path(token));
462 }
463
464 if let Some(start_after) = start_after {
466 let start_after = build_abs_path(&self.root, &start_after);
467 url = url.push("start-after", &percent_encode_path(&start_after));
468 }
469
470 let req = Request::get(url.finish())
471 .extension(Operation::List)
472 .body(Buffer::new())
473 .map_err(new_request_build_error)?;
474 Ok(req)
475 }
476
477 pub async fn oss_get_object(&self, path: &str, args: &OpRead) -> Result<Response<HttpBody>> {
478 let mut req = self.oss_get_object_request(path, false, args)?;
479 self.sign(&mut req).await?;
480 self.info.http_client().fetch(req).await
481 }
482
483 pub async fn oss_head_object(&self, path: &str, args: &OpStat) -> Result<Response<Buffer>> {
484 let mut req = self.oss_head_object_request(path, false, args)?;
485
486 self.sign(&mut req).await?;
487 self.send(req).await
488 }
489
490 pub async fn oss_copy_object(&self, from: &str, to: &str) -> Result<Response<Buffer>> {
491 let source = build_abs_path(&self.root, from);
492 let target = build_abs_path(&self.root, to);
493
494 let url = format!(
495 "{}/{}",
496 self.get_endpoint(false),
497 percent_encode_path(&target)
498 );
499 let source = format!("/{}/{}", self.bucket, percent_encode_path(&source));
500
501 let mut req = Request::put(&url);
502
503 req = self.insert_sse_headers(req);
504
505 req = req.header("x-oss-copy-source", source);
506
507 let req = req.extension(Operation::Copy);
508
509 let mut req = req.body(Buffer::new()).map_err(new_request_build_error)?;
510
511 self.sign(&mut req).await?;
512 self.send(req).await
513 }
514
515 pub async fn oss_list_object(
516 &self,
517 path: &str,
518 token: &str,
519 delimiter: &str,
520 limit: Option<usize>,
521 start_after: Option<String>,
522 ) -> Result<Response<Buffer>> {
523 let mut req = self.oss_list_object_request(path, token, delimiter, limit, start_after)?;
524
525 self.sign(&mut req).await?;
526 self.send(req).await
527 }
528
529 pub async fn oss_list_object_versions(
530 &self,
531 prefix: &str,
532 delimiter: &str,
533 limit: Option<usize>,
534 key_marker: &str,
535 version_id_marker: &str,
536 ) -> Result<Response<Buffer>> {
537 let p = build_abs_path(&self.root, prefix);
538
539 let mut url = QueryPairsWriter::new(&self.endpoint);
540 url = url.push("versions", "");
541
542 if !p.is_empty() {
543 url = url.push("prefix", &percent_encode_path(p.as_str()));
544 }
545 if !delimiter.is_empty() {
546 url = url.push("delimiter", delimiter);
547 }
548
549 if let Some(limit) = limit {
550 url = url.push("max-keys", &limit.to_string());
551 }
552 if !key_marker.is_empty() {
553 url = url.push("key-marker", &percent_encode_path(key_marker));
554 }
555 if !version_id_marker.is_empty() {
556 url = url.push("version-id-marker", &percent_encode_path(version_id_marker));
557 }
558
559 let mut req = Request::get(url.finish())
560 .extension(Operation::List)
561 .body(Buffer::new())
562 .map_err(new_request_build_error)?;
563
564 self.sign(&mut req).await?;
565
566 self.send(req).await
567 }
568
569 pub async fn oss_delete_object(&self, path: &str, args: &OpDelete) -> Result<Response<Buffer>> {
570 let mut req = self.oss_delete_object_request(path, args)?;
571 self.sign(&mut req).await?;
572 self.send(req).await
573 }
574
575 pub async fn oss_delete_objects(
576 &self,
577 paths: Vec<(String, OpDelete)>,
578 ) -> Result<Response<Buffer>> {
579 let url = format!("{}/?delete", self.endpoint);
580
581 let req = Request::post(&url);
582
583 let content = quick_xml::se::to_string(&DeleteObjectsRequest {
584 object: paths
585 .into_iter()
586 .map(|(path, op)| DeleteObjectsRequestObject {
587 key: build_abs_path(&self.root, &path),
588 version_id: op.version().map(|v| v.to_owned()),
589 })
590 .collect(),
591 })
592 .map_err(new_xml_serialize_error)?;
593
594 let req = req.header(CONTENT_LENGTH, content.len());
596 let req = req.header(CONTENT_TYPE, "application/xml");
598 let req = req.header("CONTENT-MD5", format_content_md5(content.as_bytes()));
600
601 let req = req.extension(Operation::Delete);
602
603 let mut req = req
604 .body(Buffer::from(Bytes::from(content)))
605 .map_err(new_request_build_error)?;
606
607 self.sign(&mut req).await?;
608
609 self.send(req).await
610 }
611
612 fn get_endpoint(&self, is_presign: bool) -> &str {
613 if is_presign {
614 &self.presign_endpoint
615 } else {
616 &self.endpoint
617 }
618 }
619
620 pub async fn oss_initiate_upload(
621 &self,
622 path: &str,
623 content_type: Option<&str>,
624 content_disposition: Option<&str>,
625 cache_control: Option<&str>,
626 is_presign: bool,
627 ) -> Result<Response<Buffer>> {
628 let path = build_abs_path(&self.root, path);
629 let endpoint = self.get_endpoint(is_presign);
630 let url = format!("{}/{}?uploads", endpoint, percent_encode_path(&path));
631 let mut req = Request::post(&url);
632 if let Some(mime) = content_type {
633 req = req.header(CONTENT_TYPE, mime);
634 }
635 if let Some(disposition) = content_disposition {
636 req = req.header(CONTENT_DISPOSITION, disposition);
637 }
638 if let Some(cache_control) = cache_control {
639 req = req.header(CACHE_CONTROL, cache_control);
640 }
641 req = self.insert_sse_headers(req);
642
643 let req = req.extension(Operation::Write);
644
645 let mut req = req.body(Buffer::new()).map_err(new_request_build_error)?;
646 self.sign(&mut req).await?;
647 self.send(req).await
648 }
649
650 pub async fn oss_upload_part_request(
652 &self,
653 path: &str,
654 upload_id: &str,
655 part_number: usize,
656 is_presign: bool,
657 size: u64,
658 body: Buffer,
659 ) -> Result<Response<Buffer>> {
660 let p = build_abs_path(&self.root, path);
661 let endpoint = self.get_endpoint(is_presign);
662
663 let url = format!(
664 "{}/{}?partNumber={}&uploadId={}",
665 endpoint,
666 percent_encode_path(&p),
667 part_number,
668 percent_encode_path(upload_id)
669 );
670
671 let mut req = Request::put(&url);
672 req = req.header(CONTENT_LENGTH, size);
673
674 let req = req.extension(Operation::Write);
675
676 let mut req = req.body(body).map_err(new_request_build_error)?;
677 self.sign(&mut req).await?;
678 self.send(req).await
679 }
680
681 pub async fn oss_complete_multipart_upload_request(
682 &self,
683 path: &str,
684 upload_id: &str,
685 is_presign: bool,
686 parts: Vec<MultipartUploadPart>,
687 ) -> Result<Response<Buffer>> {
688 let p = build_abs_path(&self.root, path);
689 let endpoint = self.get_endpoint(is_presign);
690 let url = format!(
691 "{}/{}?uploadId={}",
692 endpoint,
693 percent_encode_path(&p),
694 percent_encode_path(upload_id)
695 );
696
697 let req = Request::post(&url);
698
699 let content = quick_xml::se::to_string(&CompleteMultipartUploadRequest {
700 part: parts.to_vec(),
701 })
702 .map_err(new_xml_serialize_error)?;
703 let req = req.header(CONTENT_LENGTH, content.len());
705 let req = req.header(CONTENT_TYPE, "application/xml");
707
708 let req = req.extension(Operation::Write);
709
710 let mut req = req
711 .body(Buffer::from(Bytes::from(content)))
712 .map_err(new_request_build_error)?;
713
714 self.sign(&mut req).await?;
715 self.send(req).await
716 }
717
718 pub async fn oss_abort_multipart_upload(
721 &self,
722 path: &str,
723 upload_id: &str,
724 ) -> Result<Response<Buffer>> {
725 let p = build_abs_path(&self.root, path);
726
727 let url = format!(
728 "{}/{}?uploadId={}",
729 self.endpoint,
730 percent_encode_path(&p),
731 percent_encode_path(upload_id)
732 );
733
734 let mut req = Request::delete(&url)
735 .extension(Operation::Write)
736 .body(Buffer::new())
737 .map_err(new_request_build_error)?;
738 self.sign(&mut req).await?;
739 self.send(req).await
740 }
741}
742
743#[derive(Default, Debug, Serialize)]
745#[serde(default, rename = "Delete", rename_all = "PascalCase")]
746pub struct DeleteObjectsRequest {
747 pub object: Vec<DeleteObjectsRequestObject>,
748}
749
750#[derive(Default, Debug, Serialize)]
751#[serde(rename_all = "PascalCase")]
752pub struct DeleteObjectsRequestObject {
753 pub key: String,
754 #[serde(skip_serializing_if = "Option::is_none")]
755 pub version_id: Option<String>,
756}
757
758#[derive(Default, Debug, Deserialize)]
760#[serde(default, rename = "DeleteResult", rename_all = "PascalCase")]
761pub struct DeleteObjectsResult {
762 pub deleted: Vec<DeleteObjectsResultDeleted>,
763}
764
765#[derive(Default, Debug, Deserialize)]
766#[serde(rename_all = "PascalCase")]
767pub struct DeleteObjectsResultDeleted {
768 pub key: String,
769 pub version_id: Option<String>,
770}
771
772#[derive(Default, Debug, Deserialize)]
773#[serde(rename_all = "PascalCase")]
774pub struct InitiateMultipartUploadResult {
775 #[cfg(test)]
776 pub bucket: String,
777 #[cfg(test)]
778 pub key: String,
779 pub upload_id: String,
780}
781
782#[derive(Clone, Default, Debug, Serialize)]
783#[serde(default, rename_all = "PascalCase")]
784pub struct MultipartUploadPart {
785 #[serde(rename = "PartNumber")]
786 pub part_number: usize,
787 #[serde(rename = "ETag")]
788 pub etag: String,
789}
790
791#[derive(Default, Debug, Serialize)]
792#[serde(default, rename = "CompleteMultipartUpload", rename_all = "PascalCase")]
793pub struct CompleteMultipartUploadRequest {
794 pub part: Vec<MultipartUploadPart>,
795}
796
797#[derive(Default, Debug, Deserialize)]
798#[serde(default, rename_all = "PascalCase")]
799pub struct ListObjectsOutput {
800 pub prefix: String,
801 pub max_keys: u64,
802 pub encoding_type: String,
803 pub is_truncated: bool,
804 pub common_prefixes: Vec<CommonPrefix>,
805 pub contents: Vec<ListObjectsOutputContent>,
806 pub key_count: u64,
807
808 pub next_continuation_token: Option<String>,
809}
810
811#[derive(Default, Debug, Deserialize, PartialEq, Eq)]
812#[serde(default, rename_all = "PascalCase")]
813pub struct ListObjectsOutputContent {
814 pub key: String,
815 pub last_modified: String,
816 #[serde(rename = "ETag")]
817 pub etag: String,
818 pub size: u64,
819}
820
821#[derive(Default, Debug, Deserialize)]
822#[serde(default, rename_all = "PascalCase")]
823pub struct CommonPrefix {
824 pub prefix: String,
825}
826
827#[derive(Default, Debug, Eq, PartialEq, Deserialize)]
828#[serde(rename_all = "PascalCase")]
829pub struct OutputCommonPrefix {
830 pub prefix: String,
831}
832
833#[derive(Default, Debug, Deserialize)]
835#[serde(default, rename_all = "PascalCase")]
836pub struct ListObjectVersionsOutput {
837 pub is_truncated: Option<bool>,
838 pub next_key_marker: Option<String>,
839 pub next_version_id_marker: Option<String>,
840 pub common_prefixes: Vec<OutputCommonPrefix>,
841 pub version: Vec<ListObjectVersionsOutputVersion>,
842 pub delete_marker: Vec<ListObjectVersionsOutputDeleteMarker>,
843}
844
845#[derive(Default, Debug, Eq, PartialEq, Deserialize)]
846#[serde(rename_all = "PascalCase")]
847pub struct ListObjectVersionsOutputVersion {
848 pub key: String,
849 pub version_id: String,
850 pub is_latest: bool,
851 pub size: u64,
852 pub last_modified: String,
853 #[serde(rename = "ETag")]
854 pub etag: Option<String>,
855}
856
857#[derive(Default, Debug, Eq, PartialEq, Deserialize)]
858#[serde(rename_all = "PascalCase")]
859pub struct ListObjectVersionsOutputDeleteMarker {
860 pub key: String,
861 pub version_id: String,
862 pub is_latest: bool,
863 pub last_modified: String,
864}
865
866#[cfg(test)]
867mod tests {
868 use bytes::Buf;
869 use bytes::Bytes;
870
871 use super::*;
872
873 #[test]
875 fn test_serialize_delete_objects_request() {
876 let req = DeleteObjectsRequest {
877 object: vec![
878 DeleteObjectsRequestObject {
879 key: "multipart.data".to_string(),
880 version_id: None,
881 },
882 DeleteObjectsRequestObject {
883 key: "test.jpg".to_string(),
884 version_id: None,
885 },
886 DeleteObjectsRequestObject {
887 key: "demo.jpg".to_string(),
888 version_id: None,
889 },
890 ],
891 };
892
893 let actual = quick_xml::se::to_string(&req).expect("must succeed");
894
895 pretty_assertions::assert_eq!(
896 actual,
897 r#"<Delete>
898 <Object>
899 <Key>multipart.data</Key>
900 </Object>
901 <Object>
902 <Key>test.jpg</Key>
903 </Object>
904 <Object>
905 <Key>demo.jpg</Key>
906 </Object>
907</Delete>"#
908 .replace([' ', '\n'], "")
910 )
911 }
912
913 #[test]
915 fn test_deserialize_delete_objects_result() {
916 let bs = Bytes::from(
917 r#"<?xml version="1.0" encoding="UTF-8"?>
918<DeleteResult xmlns="http://doc.oss-cn-hangzhou.aliyuncs.com">
919 <Deleted>
920 <Key>multipart.data</Key>
921 </Deleted>
922 <Deleted>
923 <Key>test.jpg</Key>
924 </Deleted>
925 <Deleted>
926 <Key>demo.jpg</Key>
927 </Deleted>
928</DeleteResult>"#,
929 );
930
931 let out: DeleteObjectsResult =
932 quick_xml::de::from_reader(bs.reader()).expect("must success");
933
934 assert_eq!(out.deleted.len(), 3);
935 assert_eq!(out.deleted[0].key, "multipart.data");
936 assert_eq!(out.deleted[1].key, "test.jpg");
937 assert_eq!(out.deleted[2].key, "demo.jpg");
938 }
939
940 #[test]
941 fn test_deserialize_initiate_multipart_upload_response() {
942 let bs = Bytes::from(
943 r#"<?xml version="1.0" encoding="UTF-8"?>
944<InitiateMultipartUploadResult xmlns="http://doc.oss-cn-hangzhou.aliyuncs.com">
945 <Bucket>oss-example</Bucket>
946 <Key>multipart.data</Key>
947 <UploadId>0004B9894A22E5B1888A1E29F823****</UploadId>
948</InitiateMultipartUploadResult>"#,
949 );
950 let out: InitiateMultipartUploadResult =
951 quick_xml::de::from_reader(bs.reader()).expect("must success");
952
953 assert_eq!("0004B9894A22E5B1888A1E29F823****", out.upload_id);
954 assert_eq!("multipart.data", out.key);
955 assert_eq!("oss-example", out.bucket);
956 }
957
958 #[test]
959 fn test_serialize_complete_multipart_upload_request() {
960 let req = CompleteMultipartUploadRequest {
961 part: vec![
962 MultipartUploadPart {
963 part_number: 1,
964 etag: "\"3349DC700140D7F86A0784842780****\"".to_string(),
965 },
966 MultipartUploadPart {
967 part_number: 5,
968 etag: "\"8EFDA8BE206636A695359836FE0A****\"".to_string(),
969 },
970 MultipartUploadPart {
971 part_number: 8,
972 etag: "\"8C315065167132444177411FDA14****\"".to_string(),
973 },
974 ],
975 };
976
977 let mut serialized = String::new();
979 let mut serializer = quick_xml::se::Serializer::new(&mut serialized);
980 serializer.indent(' ', 4);
981 req.serialize(serializer).unwrap();
982 pretty_assertions::assert_eq!(
983 serialized,
984 r#"<CompleteMultipartUpload>
985 <Part>
986 <PartNumber>1</PartNumber>
987 <ETag>"3349DC700140D7F86A0784842780****"</ETag>
988 </Part>
989 <Part>
990 <PartNumber>5</PartNumber>
991 <ETag>"8EFDA8BE206636A695359836FE0A****"</ETag>
992 </Part>
993 <Part>
994 <PartNumber>8</PartNumber>
995 <ETag>"8C315065167132444177411FDA14****"</ETag>
996 </Part>
997</CompleteMultipartUpload>"#
998 )
999 }
1000
1001 #[test]
1002 fn test_parse_list_output() {
1003 let bs = bytes::Bytes::from(
1004 r#"<?xml version="1.0" encoding="UTF-8"?>
1005<ListBucketResult xmlns="https://doc.oss-cn-hangzhou.aliyuncs.com">
1006 <Name>examplebucket</Name>
1007 <Prefix></Prefix>
1008 <StartAfter>b</StartAfter>
1009 <MaxKeys>3</MaxKeys>
1010 <EncodingType>url</EncodingType>
1011 <IsTruncated>true</IsTruncated>
1012 <NextContinuationToken>CgJiYw--</NextContinuationToken>
1013 <Contents>
1014 <Key>b/c</Key>
1015 <LastModified>2020-05-18T05:45:54.000Z</LastModified>
1016 <ETag>"35A27C2B9EAEEB6F48FD7FB5861D****"</ETag>
1017 <Size>25</Size>
1018 <StorageClass>STANDARD</StorageClass>
1019 <Owner>
1020 <ID>1686240967192623</ID>
1021 <DisplayName>1686240967192623</DisplayName>
1022 </Owner>
1023 </Contents>
1024 <Contents>
1025 <Key>ba</Key>
1026 <LastModified>2020-05-18T11:17:58.000Z</LastModified>
1027 <ETag>"35A27C2B9EAEEB6F48FD7FB5861D****"</ETag>
1028 <Size>25</Size>
1029 <StorageClass>STANDARD</StorageClass>
1030 <Owner>
1031 <ID>1686240967192623</ID>
1032 <DisplayName>1686240967192623</DisplayName>
1033 </Owner>
1034 </Contents>
1035 <Contents>
1036 <Key>bc</Key>
1037 <LastModified>2020-05-18T05:45:59.000Z</LastModified>
1038 <ETag>"35A27C2B9EAEEB6F48FD7FB5861D****"</ETag>
1039 <Size>25</Size>
1040 <StorageClass>STANDARD</StorageClass>
1041 <Owner>
1042 <ID>1686240967192623</ID>
1043 <DisplayName>1686240967192623</DisplayName>
1044 </Owner>
1045 </Contents>
1046 <KeyCount>3</KeyCount>
1047</ListBucketResult>"#,
1048 );
1049
1050 let out: ListObjectsOutput = quick_xml::de::from_reader(bs.reader()).expect("must_success");
1051
1052 assert!(out.is_truncated);
1053 assert_eq!(out.next_continuation_token, Some("CgJiYw--".to_string()));
1054 assert!(out.common_prefixes.is_empty());
1055
1056 assert_eq!(
1057 out.contents,
1058 vec![
1059 ListObjectsOutputContent {
1060 key: "b/c".to_string(),
1061 last_modified: "2020-05-18T05:45:54.000Z".to_string(),
1062 etag: "\"35A27C2B9EAEEB6F48FD7FB5861D****\"".to_string(),
1063 size: 25,
1064 },
1065 ListObjectsOutputContent {
1066 key: "ba".to_string(),
1067 last_modified: "2020-05-18T11:17:58.000Z".to_string(),
1068 etag: "\"35A27C2B9EAEEB6F48FD7FB5861D****\"".to_string(),
1069 size: 25,
1070 },
1071 ListObjectsOutputContent {
1072 key: "bc".to_string(),
1073 last_modified: "2020-05-18T05:45:59.000Z".to_string(),
1074 etag: "\"35A27C2B9EAEEB6F48FD7FB5861D****\"".to_string(),
1075 size: 25,
1076 }
1077 ]
1078 )
1079 }
1080}