1use std::mem;
19use std::str::FromStr;
20
21use bytes::Bytes;
22use bytes::BytesMut;
23use http::header::CONTENT_DISPOSITION;
24use http::header::CONTENT_LENGTH;
25use http::header::CONTENT_TYPE;
26use http::uri::PathAndQuery;
27use http::HeaderMap;
28use http::HeaderName;
29use http::HeaderValue;
30use http::Method;
31use http::Request;
32use http::Response;
33use http::StatusCode;
34use http::Uri;
35use http::Version;
36
37use super::new_request_build_error;
38use crate::*;
39
40#[derive(Debug)]
42pub struct Multipart<T: Part> {
43 boundary: String,
44 parts: Vec<T>,
45}
46
47impl<T: Part> Default for Multipart<T> {
48 fn default() -> Self {
49 Self::new()
50 }
51}
52
53impl<T: Part> Multipart<T> {
54 pub fn new() -> Self {
56 Multipart {
57 boundary: format!("opendal-{}", uuid::Uuid::new_v4()),
58 parts: Vec::default(),
59 }
60 }
61
62 pub fn with_boundary(mut self, boundary: &str) -> Self {
64 self.boundary = boundary.to_string();
65 self
66 }
67
68 pub fn part(mut self, part: T) -> Self {
70 self.parts.push(part);
71 self
72 }
73
74 pub fn into_parts(self) -> Vec<T> {
76 self.parts
77 }
78
79 pub fn parse(mut self, bs: Bytes) -> Result<Self> {
81 let s = String::from_utf8(bs.to_vec()).map_err(|err| {
82 Error::new(
83 ErrorKind::Unexpected,
84 "multipart response contains invalid utf-8 chars",
85 )
86 .set_source(err)
87 })?;
88
89 let parts = s
90 .split(format!("--{}", self.boundary).as_str())
91 .collect::<Vec<&str>>();
92
93 for part in parts {
94 if part.is_empty() || part.starts_with("--") {
95 continue;
96 }
97
98 self.parts.push(T::parse(part)?);
99 }
100
101 Ok(self)
102 }
103
104 pub(crate) fn build(self) -> Buffer {
105 let mut bufs = Vec::with_capacity(self.parts.len() + 2);
106
107 let mut bs = BytesMut::new();
109 bs.extend_from_slice(b"--");
110 bs.extend_from_slice(self.boundary.as_bytes());
111 bs.extend_from_slice(b"\r\n");
112 let pre_part = Buffer::from(bs.freeze());
113
114 for part in self.parts {
116 bufs.push(pre_part.clone());
117 bufs.push(part.format());
118 }
119
120 let mut bs = BytesMut::new();
122 bs.extend_from_slice(b"--");
123 bs.extend_from_slice(self.boundary.as_bytes());
124 bs.extend_from_slice(b"--");
125 bs.extend_from_slice(b"\r\n");
126
127 bufs.push(Buffer::from(bs.freeze()));
129
130 bufs.into_iter().flatten().collect()
131 }
132
133 pub fn apply(self, mut builder: http::request::Builder) -> Result<Request<Buffer>> {
137 let boundary = self.boundary.clone();
138 let buf = self.build();
139 let content_length = buf.len();
140
141 builder = builder.header(
143 CONTENT_TYPE,
144 format!("multipart/{}; boundary={}", T::TYPE, boundary).as_str(),
145 );
146 builder = builder.header(CONTENT_LENGTH, content_length);
148
149 builder.body(buf).map_err(new_request_build_error)
150 }
151}
152
153pub trait Part: Sized + 'static {
155 const TYPE: &'static str;
159
160 fn format(self) -> Buffer;
162
163 fn parse(s: &str) -> Result<Self>;
165}
166
167pub struct FormDataPart {
169 headers: HeaderMap,
170
171 content: Buffer,
172}
173
174impl FormDataPart {
175 pub fn new(name: &str) -> Self {
181 let mut headers = HeaderMap::new();
183 headers.insert(
184 CONTENT_DISPOSITION,
185 format!("form-data; name=\"{}\"", name).parse().unwrap(),
186 );
187
188 Self {
189 headers,
190 content: Buffer::new(),
191 }
192 }
193
194 pub fn header(mut self, key: HeaderName, value: HeaderValue) -> Self {
196 self.headers.insert(key, value);
197 self
198 }
199
200 pub fn content(mut self, content: impl Into<Buffer>) -> Self {
202 self.content = content.into();
203 self
204 }
205}
206
207impl Part for FormDataPart {
208 const TYPE: &'static str = "form-data";
209
210 fn format(self) -> Buffer {
211 let mut bufs = Vec::with_capacity(3);
212
213 let mut bs = BytesMut::new();
215 for (k, v) in self.headers.iter() {
216 match k.as_str() {
222 "content-disposition" => {
223 bs.extend_from_slice("Content-Disposition".as_bytes());
224 }
225 _ => {
226 bs.extend_from_slice(k.as_str().as_bytes());
227 }
228 }
229 bs.extend_from_slice(b": ");
230 bs.extend_from_slice(v.as_bytes());
231 bs.extend_from_slice(b"\r\n");
232 }
233 bs.extend_from_slice(b"\r\n");
234 bufs.push(Buffer::from(bs.freeze()));
235
236 bufs.push(self.content);
238
239 bufs.push(Buffer::from("\r\n"));
241
242 bufs.into_iter().flatten().collect()
243 }
244
245 fn parse(_: &str) -> Result<Self> {
246 Err(Error::new(
247 ErrorKind::Unsupported,
248 "parse of form-data is not supported",
249 ))
250 }
251}
252
253pub struct MixedPart {
255 part_headers: HeaderMap,
256
257 version: Version,
259 headers: HeaderMap,
260 content: Buffer,
261
262 method: Option<Method>,
264 uri: Option<Uri>,
265
266 status_code: Option<StatusCode>,
268}
269
270impl MixedPart {
271 pub fn new(uri: &str) -> Self {
273 let mut part_headers = HeaderMap::new();
274 part_headers.insert(CONTENT_TYPE, "application/http".parse().unwrap());
275 part_headers.insert("content-transfer-encoding", "binary".parse().unwrap());
276
277 let uri = Uri::from_str(uri).expect("the uri used to build a mixed part must be valid");
278
279 Self {
280 part_headers,
281
282 version: Version::HTTP_11,
283 headers: HeaderMap::new(),
284 content: Buffer::new(),
285
286 uri: Some(uri),
287 method: None,
288
289 status_code: None,
290 }
291 }
292
293 pub fn from_request(req: Request<Buffer>) -> Self {
295 let mut part_headers = HeaderMap::new();
296 part_headers.insert(CONTENT_TYPE, "application/http".parse().unwrap());
297 part_headers.insert("content-transfer-encoding", "binary".parse().unwrap());
298
299 let (parts, content) = req.into_parts();
300
301 Self {
302 part_headers,
303 uri: Some(
304 Uri::from_str(
305 parts
306 .uri
307 .path_and_query()
308 .unwrap_or(&PathAndQuery::from_static("/"))
309 .as_str(),
310 )
311 .expect("the uri used to build a mixed part must be valid"),
312 ),
313 version: parts.version,
314 headers: parts.headers,
315 content,
316
317 method: Some(parts.method),
318 status_code: None,
319 }
320 }
321
322 pub fn into_response(mut self) -> Response<Buffer> {
324 let mut builder = Response::builder();
325
326 builder = builder.status(self.status_code.unwrap_or(StatusCode::OK));
327 builder = builder.version(self.version);
328 mem::swap(builder.headers_mut().unwrap(), &mut self.headers);
330
331 builder
332 .body(self.content)
333 .expect("mixed part must be valid response")
334 }
335
336 pub fn part_header(mut self, key: HeaderName, value: HeaderValue) -> Self {
338 self.part_headers.insert(key, value);
339 self
340 }
341
342 pub fn method(mut self, method: Method) -> Self {
344 self.method = Some(method);
345 self
346 }
347
348 pub fn version(mut self, version: Version) -> Self {
350 self.version = version;
351 self
352 }
353
354 pub fn header(mut self, key: HeaderName, value: HeaderValue) -> Self {
356 self.headers.insert(key, value);
357 self
358 }
359
360 pub fn content(mut self, content: impl Into<Buffer>) -> Self {
362 self.content = content.into();
363 self
364 }
365}
366
367impl Part for MixedPart {
368 const TYPE: &'static str = "mixed";
369
370 fn format(self) -> Buffer {
371 let mut bufs = Vec::with_capacity(3);
372
373 let mut bs = BytesMut::new();
375 for (k, v) in self.part_headers.iter() {
376 match k.as_str() {
382 "content-type" => {
383 bs.extend_from_slice("Content-Type".as_bytes());
384 }
385 "content-id" => {
386 bs.extend_from_slice("Content-ID".as_bytes());
387 }
388 "content-transfer-encoding" => {
389 bs.extend_from_slice("Content-Transfer-Encoding".as_bytes());
390 }
391 _ => {
392 bs.extend_from_slice(k.as_str().as_bytes());
393 }
394 }
395 bs.extend_from_slice(b": ");
396 bs.extend_from_slice(v.as_bytes());
397 bs.extend_from_slice(b"\r\n");
398 }
399
400 bs.extend_from_slice(b"\r\n");
402 bs.extend_from_slice(
403 self.method
404 .as_ref()
405 .expect("mixed part must be a valid request that contains method")
406 .as_str()
407 .as_bytes(),
408 );
409 bs.extend_from_slice(b" ");
410 bs.extend_from_slice(
411 self.uri
412 .as_ref()
413 .expect("mixed part must be a valid request that contains uri")
414 .path()
415 .as_bytes(),
416 );
417 bs.extend_from_slice(b" ");
418 bs.extend_from_slice(format!("{:?}", self.version).as_bytes());
419 bs.extend_from_slice(b"\r\n");
420
421 for (k, v) in self.headers.iter() {
423 bs.extend_from_slice(k.as_str().as_bytes());
424 bs.extend_from_slice(b": ");
425 bs.extend_from_slice(v.as_bytes());
426 bs.extend_from_slice(b"\r\n");
427 }
428 bs.extend_from_slice(b"\r\n");
429 bufs.push(Buffer::from(bs.freeze()));
430
431 if !self.content.is_empty() {
432 bufs.push(self.content);
433 bufs.push(Buffer::from("\r\n"))
434 }
435
436 bufs.into_iter().flatten().collect()
437 }
438
439 fn parse(s: &str) -> Result<Self> {
443 let parts = s.splitn(2, "\r\n\r\n").collect::<Vec<&str>>();
444 let part_headers_content = parts[0];
445 let http_response = parts.get(1).unwrap_or(&"");
446
447 let mut part_headers = HeaderMap::new();
448 for line in part_headers_content.lines() {
449 let parts = line.splitn(2, ": ").collect::<Vec<&str>>();
450 if parts.len() == 2 {
451 let header_name = HeaderName::from_str(parts[0]).map_err(|err| {
452 Error::new(
453 ErrorKind::Unexpected,
454 "multipart response contains invalid part header name",
455 )
456 .set_source(err)
457 })?;
458 let header_value = parts[1].parse().map_err(|err| {
459 Error::new(
460 ErrorKind::Unexpected,
461 "multipart response contains invalid part header value",
462 )
463 .set_source(err)
464 })?;
465
466 part_headers.insert(header_name, header_value);
467 }
468 }
469
470 let parts = http_response.split("\r\n\r\n").collect::<Vec<&str>>();
471 let headers_content = parts[0];
472 let body_content = parts.get(1).unwrap_or(&"");
473 let body_bytes = Buffer::from(body_content.to_string());
474
475 let status_line = headers_content.lines().next().unwrap_or("");
476 let status_code = status_line
477 .split_whitespace()
478 .nth(1)
479 .unwrap_or("")
480 .parse::<u16>()
481 .unwrap_or(200);
482
483 let mut headers = HeaderMap::new();
484 for line in headers_content.lines().skip(1) {
485 let parts = line.splitn(2, ": ").collect::<Vec<&str>>();
486 if parts.len() == 2 {
487 let header_name = HeaderName::from_str(parts[0]).map_err(|err| {
488 Error::new(
489 ErrorKind::Unexpected,
490 "multipart response contains invalid part header name",
491 )
492 .set_source(err)
493 })?;
494 let header_value = parts[1].parse().map_err(|err| {
495 Error::new(
496 ErrorKind::Unexpected,
497 "multipart response contains invalid part header value",
498 )
499 .set_source(err)
500 })?;
501
502 headers.insert(header_name, header_value);
503 }
504 }
505
506 Ok(Self {
507 part_headers,
508 version: Version::HTTP_11,
509 headers,
510 content: body_bytes,
511
512 method: None,
513 uri: None,
514
515 status_code: Some(StatusCode::from_u16(status_code).map_err(|err| {
516 Error::new(
517 ErrorKind::Unexpected,
518 "multipart response contains invalid status code",
519 )
520 .set_source(err)
521 })?),
522 })
523 }
524}
525
526pub struct RelatedPart {
528 headers: HeaderMap,
530 content: Buffer,
531}
532
533impl Default for RelatedPart {
534 fn default() -> Self {
535 Self::new()
536 }
537}
538
539impl RelatedPart {
540 pub fn new() -> Self {
542 Self {
543 headers: HeaderMap::new(),
544 content: Buffer::new(),
545 }
546 }
547
548 pub fn from_request(req: Request<Buffer>) -> Self {
550 let (parts, content) = req.into_parts();
551
552 Self {
553 headers: parts.headers,
554 content,
555 }
556 }
557
558 pub fn into_response(mut self) -> Response<Buffer> {
560 let mut builder = Response::builder();
561
562 mem::swap(builder.headers_mut().unwrap(), &mut self.headers);
564
565 builder
566 .body(self.content)
567 .expect("a related part must be valid response")
568 }
569
570 pub fn header(mut self, key: HeaderName, value: HeaderValue) -> Self {
572 self.headers.insert(key, value);
573 self
574 }
575
576 pub fn content(mut self, content: impl Into<Buffer>) -> Self {
578 self.content = content.into();
579 self
580 }
581}
582
583impl Part for RelatedPart {
584 const TYPE: &'static str = "related";
585
586 fn format(self) -> Buffer {
587 let mut bufs = Vec::with_capacity(3);
603 let mut bs = BytesMut::new();
604
605 for (k, v) in self.headers.iter() {
607 bs.extend_from_slice(k.as_str().as_bytes());
608 bs.extend_from_slice(b": ");
609 bs.extend_from_slice(v.as_bytes());
610 bs.extend_from_slice(b"\r\n");
611 }
612 bs.extend_from_slice(b"\r\n");
613 bufs.push(Buffer::from(bs.freeze()));
614
615 if !self.content.is_empty() {
616 bufs.push(self.content);
617 bufs.push(Buffer::from("\r\n"))
618 }
619
620 bufs.into_iter().flatten().collect()
621 }
622
623 fn parse(_s: &str) -> Result<Self> {
624 Err(Error::new(
625 ErrorKind::Unsupported,
626 "parsing multipart/related is not supported",
627 ))
628 }
629}
630
631#[cfg(test)]
632mod tests {
633 use http::header::CONTENT_TYPE;
634 use pretty_assertions::assert_eq;
635
636 use super::*;
637
638 #[test]
639 fn test_multipart_formdata_basic() -> Result<()> {
640 let multipart = Multipart::new()
641 .with_boundary("lalala")
642 .part(FormDataPart::new("foo").content(Bytes::from("bar")))
643 .part(FormDataPart::new("hello").content(Bytes::from("world")));
644
645 let bs = multipart.build();
646
647 let expected = "--lalala\r\n\
648 Content-Disposition: form-data; name=\"foo\"\r\n\
649 \r\n\
650 bar\r\n\
651 --lalala\r\n\
652 Content-Disposition: form-data; name=\"hello\"\r\n\
653 \r\n\
654 world\r\n\
655 --lalala--\r\n";
656
657 assert_eq!(Bytes::from(expected), bs.to_bytes());
658 Ok(())
659 }
660
661 #[test]
663 fn test_multipart_formdata_s3_form_upload() -> Result<()> {
664 let multipart = Multipart::new()
665 .with_boundary("9431149156168")
666 .part(FormDataPart::new("key").content("user/eric/MyPicture.jpg"))
667 .part(FormDataPart::new("acl").content("public-read"))
668 .part(FormDataPart::new("success_action_redirect").content(
669 "https://awsexamplebucket1.s3.us-west-1.amazonaws.com/successful_upload.html",
670 ))
671 .part(FormDataPart::new("content-type").content("image/jpeg"))
672 .part(FormDataPart::new("x-amz-meta-uuid").content("14365123651274"))
673 .part(FormDataPart::new("x-amz-meta-tag").content("Some,Tag,For,Picture"))
674 .part(FormDataPart::new("AWSAccessKeyId").content("AKIAIOSFODNN7EXAMPLE"))
675 .part(FormDataPart::new("Policy").content("eyAiZXhwaXJhdGlvbiI6ICIyMDA3LTEyLTAxVDEyOjAwOjAwLjAwMFoiLAogICJjb25kaXRpb25zIjogWwogICAgeyJidWNrZXQiOiAiam9obnNtaXRoIn0sCiAgICBbInN0YXJ0cy13aXRoIiwgIiRrZXkiLCAidXNlci9lcmljLyJdLAogICAgeyJhY2wiOiAicHVibGljLXJlYWQifSwKICAgIHsic3VjY2Vzc19hY3Rpb25fcmVkaXJlY3QiOiAiaHR0cDovL2pvaG5zbWl0aC5zMy5hbWF6b25hd3MuY29tL3N1Y2Nlc3NmdWxfdXBsb2FkLmh0bWwifSwKICAgIFsic3RhcnRzLXdpdGgiLCAiJENvbnRlbnQtVHlwZSIsICJpbWFnZS8iXSwKICAgIHsieC1hbXotbWV0YS11dWlkIjogIjE0MzY1MTIzNjUxMjc0In0sCiAgICBbInN0YXJ0cy13aXRoIiwgIiR4LWFtei1tZXRhLXRhZyIsICIiXQogIF0KfQo="))
676 .part(FormDataPart::new("Signature").content("0RavWzkygo6QX9caELEqKi9kDbU="))
677 .part(FormDataPart::new("file").header(CONTENT_TYPE, "image/jpeg".parse().unwrap()).content("...file content...")).part(FormDataPart::new("submit").content("Upload to Amazon S3"));
678
679 let bs = multipart.build();
680
681 let expected = r#"--9431149156168
682Content-Disposition: form-data; name="key"
683
684user/eric/MyPicture.jpg
685--9431149156168
686Content-Disposition: form-data; name="acl"
687
688public-read
689--9431149156168
690Content-Disposition: form-data; name="success_action_redirect"
691
692https://awsexamplebucket1.s3.us-west-1.amazonaws.com/successful_upload.html
693--9431149156168
694Content-Disposition: form-data; name="content-type"
695
696image/jpeg
697--9431149156168
698Content-Disposition: form-data; name="x-amz-meta-uuid"
699
70014365123651274
701--9431149156168
702Content-Disposition: form-data; name="x-amz-meta-tag"
703
704Some,Tag,For,Picture
705--9431149156168
706Content-Disposition: form-data; name="AWSAccessKeyId"
707
708AKIAIOSFODNN7EXAMPLE
709--9431149156168
710Content-Disposition: form-data; name="Policy"
711
712eyAiZXhwaXJhdGlvbiI6ICIyMDA3LTEyLTAxVDEyOjAwOjAwLjAwMFoiLAogICJjb25kaXRpb25zIjogWwogICAgeyJidWNrZXQiOiAiam9obnNtaXRoIn0sCiAgICBbInN0YXJ0cy13aXRoIiwgIiRrZXkiLCAidXNlci9lcmljLyJdLAogICAgeyJhY2wiOiAicHVibGljLXJlYWQifSwKICAgIHsic3VjY2Vzc19hY3Rpb25fcmVkaXJlY3QiOiAiaHR0cDovL2pvaG5zbWl0aC5zMy5hbWF6b25hd3MuY29tL3N1Y2Nlc3NmdWxfdXBsb2FkLmh0bWwifSwKICAgIFsic3RhcnRzLXdpdGgiLCAiJENvbnRlbnQtVHlwZSIsICJpbWFnZS8iXSwKICAgIHsieC1hbXotbWV0YS11dWlkIjogIjE0MzY1MTIzNjUxMjc0In0sCiAgICBbInN0YXJ0cy13aXRoIiwgIiR4LWFtei1tZXRhLXRhZyIsICIiXQogIF0KfQo=
713--9431149156168
714Content-Disposition: form-data; name="Signature"
715
7160RavWzkygo6QX9caELEqKi9kDbU=
717--9431149156168
718Content-Disposition: form-data; name="file"
719content-type: image/jpeg
720
721...file content...
722--9431149156168
723Content-Disposition: form-data; name="submit"
724
725Upload to Amazon S3
726--9431149156168--
727"#;
728
729 assert_eq!(
730 expected,
731 String::from_utf8(bs.to_bytes().to_vec())
734 .unwrap()
735 .replace("\r\n", "\n")
736 );
737
738 Ok(())
739 }
740
741 #[test]
743 fn test_multipart_mixed_gcs_batch_metadata() -> Result<()> {
744 let multipart = Multipart::new()
745 .with_boundary("===============7330845974216740156==")
746 .part(
747 MixedPart::new("/storage/v1/b/example-bucket/o/obj1")
748 .method(Method::PATCH)
749 .part_header(
750 "content-id".parse().unwrap(),
751 "<b29c5de2-0db4-490b-b421-6a51b598bd22+1>".parse().unwrap(),
752 )
753 .header(
754 "content-type".parse().unwrap(),
755 "application/json".parse().unwrap(),
756 )
757 .header(
758 "accept".parse().unwrap(),
759 "application/json".parse().unwrap(),
760 )
761 .header("content-length".parse().unwrap(), "31".parse().unwrap())
762 .content(r#"{"metadata": {"type": "tabby"}}"#),
763 )
764 .part(
765 MixedPart::new("/storage/v1/b/example-bucket/o/obj2")
766 .method(Method::PATCH)
767 .part_header(
768 "content-id".parse().unwrap(),
769 "<b29c5de2-0db4-490b-b421-6a51b598bd22+2>".parse().unwrap(),
770 )
771 .header(
772 "content-type".parse().unwrap(),
773 "application/json".parse().unwrap(),
774 )
775 .header(
776 "accept".parse().unwrap(),
777 "application/json".parse().unwrap(),
778 )
779 .header("content-length".parse().unwrap(), "32".parse().unwrap())
780 .content(r#"{"metadata": {"type": "tuxedo"}}"#),
781 )
782 .part(
783 MixedPart::new("/storage/v1/b/example-bucket/o/obj3")
784 .method(Method::PATCH)
785 .part_header(
786 "content-id".parse().unwrap(),
787 "<b29c5de2-0db4-490b-b421-6a51b598bd22+3>".parse().unwrap(),
788 )
789 .header(
790 "content-type".parse().unwrap(),
791 "application/json".parse().unwrap(),
792 )
793 .header(
794 "accept".parse().unwrap(),
795 "application/json".parse().unwrap(),
796 )
797 .header("content-length".parse().unwrap(), "32".parse().unwrap())
798 .content(r#"{"metadata": {"type": "calico"}}"#),
799 );
800
801 let bs = multipart.build();
802
803 let expected = r#"--===============7330845974216740156==
804Content-Type: application/http
805Content-Transfer-Encoding: binary
806Content-ID: <b29c5de2-0db4-490b-b421-6a51b598bd22+1>
807
808PATCH /storage/v1/b/example-bucket/o/obj1 HTTP/1.1
809content-type: application/json
810accept: application/json
811content-length: 31
812
813{"metadata": {"type": "tabby"}}
814--===============7330845974216740156==
815Content-Type: application/http
816Content-Transfer-Encoding: binary
817Content-ID: <b29c5de2-0db4-490b-b421-6a51b598bd22+2>
818
819PATCH /storage/v1/b/example-bucket/o/obj2 HTTP/1.1
820content-type: application/json
821accept: application/json
822content-length: 32
823
824{"metadata": {"type": "tuxedo"}}
825--===============7330845974216740156==
826Content-Type: application/http
827Content-Transfer-Encoding: binary
828Content-ID: <b29c5de2-0db4-490b-b421-6a51b598bd22+3>
829
830PATCH /storage/v1/b/example-bucket/o/obj3 HTTP/1.1
831content-type: application/json
832accept: application/json
833content-length: 32
834
835{"metadata": {"type": "calico"}}
836--===============7330845974216740156==--
837"#;
838
839 assert_eq!(
840 expected,
841 String::from_utf8(bs.to_bytes().to_vec())
844 .unwrap()
845 .replace("\r\n", "\n")
846 );
847
848 Ok(())
849 }
850
851 #[test]
853 fn test_multipart_mixed_azblob_batch_delete() -> Result<()> {
854 let multipart = Multipart::new()
855 .with_boundary("batch_357de4f7-6d0b-4e02-8cd2-6361411a9525")
856 .part(
857 MixedPart::new("/container0/blob0")
858 .method(Method::DELETE)
859 .part_header("content-id".parse().unwrap(), "0".parse().unwrap())
860 .header(
861 "x-ms-date".parse().unwrap(),
862 "Thu, 14 Jun 2018 16:46:54 GMT".parse().unwrap(),
863 )
864 .header(
865 "authorization".parse().unwrap(),
866 "SharedKey account:G4jjBXA7LI/RnWKIOQ8i9xH4p76pAQ+4Fs4R1VxasaE="
867 .parse()
868 .unwrap(),
869 )
870 .header("content-length".parse().unwrap(), "0".parse().unwrap()),
871 )
872 .part(
873 MixedPart::new("/container1/blob1")
874 .method(Method::DELETE)
875 .part_header("content-id".parse().unwrap(), "1".parse().unwrap())
876 .header(
877 "x-ms-date".parse().unwrap(),
878 "Thu, 14 Jun 2018 16:46:54 GMT".parse().unwrap(),
879 )
880 .header(
881 "authorization".parse().unwrap(),
882 "SharedKey account:IvCoYDQ+0VcaA/hKFjUmQmIxXv2RT3XwwTsOTHL39HI="
883 .parse()
884 .unwrap(),
885 )
886 .header("content-length".parse().unwrap(), "0".parse().unwrap()),
887 )
888 .part(
889 MixedPart::new("/container2/blob2")
890 .method(Method::DELETE)
891 .part_header("content-id".parse().unwrap(), "2".parse().unwrap())
892 .header(
893 "x-ms-date".parse().unwrap(),
894 "Thu, 14 Jun 2018 16:46:54 GMT".parse().unwrap(),
895 )
896 .header(
897 "authorization".parse().unwrap(),
898 "SharedKey account:S37N2JTjcmOQVLHLbDmp2johz+KpTJvKhbVc4M7+UqI="
899 .parse()
900 .unwrap(),
901 )
902 .header("content-length".parse().unwrap(), "0".parse().unwrap()),
903 );
904
905 let bs = multipart.build();
906
907 let expected = r#"--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525
908Content-Type: application/http
909Content-Transfer-Encoding: binary
910Content-ID: 0
911
912DELETE /container0/blob0 HTTP/1.1
913x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT
914authorization: SharedKey account:G4jjBXA7LI/RnWKIOQ8i9xH4p76pAQ+4Fs4R1VxasaE=
915content-length: 0
916
917--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525
918Content-Type: application/http
919Content-Transfer-Encoding: binary
920Content-ID: 1
921
922DELETE /container1/blob1 HTTP/1.1
923x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT
924authorization: SharedKey account:IvCoYDQ+0VcaA/hKFjUmQmIxXv2RT3XwwTsOTHL39HI=
925content-length: 0
926
927--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525
928Content-Type: application/http
929Content-Transfer-Encoding: binary
930Content-ID: 2
931
932DELETE /container2/blob2 HTTP/1.1
933x-ms-date: Thu, 14 Jun 2018 16:46:54 GMT
934authorization: SharedKey account:S37N2JTjcmOQVLHLbDmp2johz+KpTJvKhbVc4M7+UqI=
935content-length: 0
936
937--batch_357de4f7-6d0b-4e02-8cd2-6361411a9525--
938"#;
939
940 assert_eq!(
941 expected,
942 String::from_utf8(bs.to_bytes().to_vec())
945 .unwrap()
946 .replace("\r\n", "\n")
947 );
948
949 Ok(())
950 }
951
952 #[test]
954 fn test_multipart_mixed_gcs_batch_metadata_response() {
955 let response = r#"--batch_pK7JBAk73-E=_AA5eFwv4m2Q=
956Content-Type: application/http
957Content-ID: <response-b29c5de2-0db4-490b-b421-6a51b598bd22+1>
958
959HTTP/1.1 200 OK
960ETag: "lGaP-E0memYDumK16YuUDM_6Gf0/V43j6azD55CPRGb9b6uytDYl61Y"
961Content-Type: application/json; charset=UTF-8
962Date: Mon, 22 Jan 2018 18:56:00 GMT
963Expires: Mon, 22 Jan 2018 18:56:00 GMT
964Cache-Control: private, max-age=0
965Content-Length: 846
966
967{"kind": "storage#object","id": "example-bucket/obj1/1495822576643790","metadata": {"type": "tabby"}}
968
969--batch_pK7JBAk73-E=_AA5eFwv4m2Q=
970Content-Type: application/http
971Content-ID: <response-b29c5de2-0db4-490b-b421-6a51b598bd22+2>
972
973HTTP/1.1 200 OK
974ETag: "lGaP-E0memYDumK16YuUDM_6Gf0/91POdd-sxSAkJnS8Dm7wMxBSDKk"
975Content-Type: application/json; charset=UTF-8
976Date: Mon, 22 Jan 2018 18:56:00 GMT
977Expires: Mon, 22 Jan 2018 18:56:00 GMT
978Cache-Control: private, max-age=0
979Content-Length: 846
980
981{"kind": "storage#object","id": "example-bucket/obj2/1495822576643790","metadata": {"type": "tuxedo"}}
982
983--batch_pK7JBAk73-E=_AA5eFwv4m2Q=
984Content-Type: application/http
985Content-ID: <response-b29c5de2-0db4-490b-b421-6a51b598bd22+3>
986
987HTTP/1.1 200 OK
988ETag: "lGaP-E0memYDumK16YuUDM_6Gf0/d2Z1F1_ZVbB1dC0YKM9rX5VAgIQ"
989Content-Type: application/json; charset=UTF-8
990Date: Mon, 22 Jan 2018 18:56:00 GMT
991Expires: Mon, 22 Jan 2018 18:56:00 GMT
992Cache-Control: private, max-age=0
993Content-Length: 846
994
995{"kind": "storage#object","id": "example-bucket/obj3/1495822576643790","metadata": {"type": "calico"}}
996
997--batch_pK7JBAk73-E=_AA5eFwv4m2Q=--"#.replace('\n', "\r\n");
998
999 let multipart: Multipart<MixedPart> = Multipart::new()
1000 .with_boundary("batch_pK7JBAk73-E=_AA5eFwv4m2Q=")
1001 .parse(Bytes::from(response))
1002 .unwrap();
1003
1004 let part0_bs = Bytes::from_static(
1005 r#"{"kind": "storage#object","id": "example-bucket/obj1/1495822576643790","metadata": {"type": "tabby"}}"#.as_bytes());
1006 let part1_bs = Bytes::from_static(
1007 r#"{"kind": "storage#object","id": "example-bucket/obj2/1495822576643790","metadata": {"type": "tuxedo"}}"#
1008 .as_bytes()
1009 );
1010 let part2_bs = Bytes::from_static(
1011 r#"{"kind": "storage#object","id": "example-bucket/obj3/1495822576643790","metadata": {"type": "calico"}}"#
1012 .as_bytes()
1013 );
1014
1015 assert_eq!(multipart.parts.len(), 3);
1016
1017 assert_eq!(multipart.parts[0].part_headers, {
1018 let mut h = HeaderMap::new();
1019 h.insert("Content-Type", "application/http".parse().unwrap());
1020 h.insert(
1021 "Content-ID",
1022 "<response-b29c5de2-0db4-490b-b421-6a51b598bd22+1>"
1023 .parse()
1024 .unwrap(),
1025 );
1026
1027 h
1028 });
1029 assert_eq!(multipart.parts[0].version, Version::HTTP_11);
1030 assert_eq!(multipart.parts[0].headers, {
1031 let mut h = HeaderMap::new();
1032 h.insert(
1033 "ETag",
1034 "\"lGaP-E0memYDumK16YuUDM_6Gf0/V43j6azD55CPRGb9b6uytDYl61Y\""
1035 .parse()
1036 .unwrap(),
1037 );
1038 h.insert(
1039 "Content-Type",
1040 "application/json; charset=UTF-8".parse().unwrap(),
1041 );
1042 h.insert("Date", "Mon, 22 Jan 2018 18:56:00 GMT".parse().unwrap());
1043 h.insert("Expires", "Mon, 22 Jan 2018 18:56:00 GMT".parse().unwrap());
1044 h.insert("Cache-Control", "private, max-age=0".parse().unwrap());
1045 h.insert("Content-Length", "846".parse().unwrap());
1046
1047 h
1048 });
1049 assert_eq!(multipart.parts[0].content.len(), part0_bs.len());
1050 assert_eq!(multipart.parts[0].uri, None);
1051 assert_eq!(multipart.parts[0].method, None);
1052 assert_eq!(
1053 multipart.parts[0].status_code,
1054 Some(StatusCode::from_u16(200).unwrap())
1055 );
1056
1057 assert_eq!(multipart.parts[1].part_headers, {
1058 let mut h = HeaderMap::new();
1059 h.insert("Content-Type", "application/http".parse().unwrap());
1060 h.insert(
1061 "Content-ID",
1062 "<response-b29c5de2-0db4-490b-b421-6a51b598bd22+2>"
1063 .parse()
1064 .unwrap(),
1065 );
1066
1067 h
1068 });
1069 assert_eq!(multipart.parts[1].version, Version::HTTP_11);
1070 assert_eq!(multipart.parts[1].headers, {
1071 let mut h = HeaderMap::new();
1072 h.insert(
1073 "ETag",
1074 "\"lGaP-E0memYDumK16YuUDM_6Gf0/91POdd-sxSAkJnS8Dm7wMxBSDKk\""
1075 .parse()
1076 .unwrap(),
1077 );
1078 h.insert(
1079 "Content-Type",
1080 "application/json; charset=UTF-8".parse().unwrap(),
1081 );
1082 h.insert("Date", "Mon, 22 Jan 2018 18:56:00 GMT".parse().unwrap());
1083 h.insert("Expires", "Mon, 22 Jan 2018 18:56:00 GMT".parse().unwrap());
1084 h.insert("Cache-Control", "private, max-age=0".parse().unwrap());
1085 h.insert("Content-Length", "846".parse().unwrap());
1086
1087 h
1088 });
1089 assert_eq!(multipart.parts[1].content.len(), part1_bs.len());
1090 assert_eq!(multipart.parts[1].uri, None);
1091 assert_eq!(multipart.parts[1].method, None);
1092 assert_eq!(
1093 multipart.parts[1].status_code,
1094 Some(StatusCode::from_u16(200).unwrap())
1095 );
1096
1097 assert_eq!(multipart.parts[2].part_headers, {
1098 let mut h = HeaderMap::new();
1099 h.insert("Content-Type", "application/http".parse().unwrap());
1100 h.insert(
1101 "Content-ID",
1102 "<response-b29c5de2-0db4-490b-b421-6a51b598bd22+3>"
1103 .parse()
1104 .unwrap(),
1105 );
1106
1107 h
1108 });
1109 assert_eq!(multipart.parts[2].version, Version::HTTP_11);
1110 assert_eq!(multipart.parts[2].headers, {
1111 let mut h = HeaderMap::new();
1112 h.insert(
1113 "ETag",
1114 "\"lGaP-E0memYDumK16YuUDM_6Gf0/d2Z1F1_ZVbB1dC0YKM9rX5VAgIQ\""
1115 .parse()
1116 .unwrap(),
1117 );
1118 h.insert(
1119 "Content-Type",
1120 "application/json; charset=UTF-8".parse().unwrap(),
1121 );
1122 h.insert("Date", "Mon, 22 Jan 2018 18:56:00 GMT".parse().unwrap());
1123 h.insert("Expires", "Mon, 22 Jan 2018 18:56:00 GMT".parse().unwrap());
1124 h.insert("Cache-Control", "private, max-age=0".parse().unwrap());
1125 h.insert("Content-Length", "846".parse().unwrap());
1126
1127 h
1128 });
1129 assert_eq!(multipart.parts[2].content.len(), part2_bs.len());
1130 assert_eq!(multipart.parts[2].uri, None);
1131 assert_eq!(multipart.parts[2].method, None);
1132 assert_eq!(
1133 multipart.parts[2].status_code,
1134 Some(StatusCode::from_u16(200).unwrap())
1135 );
1136 }
1137
1138 #[test]
1139 fn test_multipart_related_gcs_simple() {
1140 let expected = r#"--separator_string
1143content-type: application/json; charset=UTF-8
1144
1145{"name":"my-document.txt"}
1146--separator_string
1147content-type: text/plain
1148
1149This is a text file.
1150--separator_string--
1151"#;
1152
1153 let multipart = Multipart::new()
1154 .with_boundary("separator_string")
1155 .part(
1156 RelatedPart::new()
1157 .header(
1158 "Content-Type".parse().unwrap(),
1159 "application/json; charset=UTF-8".parse().unwrap(),
1160 )
1161 .content(r#"{"name":"my-document.txt"}"#),
1162 )
1163 .part(
1164 RelatedPart::new()
1165 .header(
1166 "Content-Type".parse().unwrap(),
1167 "text/plain".parse().unwrap(),
1168 )
1169 .content("This is a text file."),
1170 );
1171
1172 let bs = multipart.build();
1173
1174 let output = String::from_utf8(bs.to_bytes().to_vec())
1175 .unwrap()
1176 .replace("\r\n", "\n");
1177
1178 assert_eq!(output, expected);
1179 }
1180}