qiniu_http_client/client/request/
multipart.rs

1use assert_impl::assert_impl;
2use mime::Mime;
3use once_cell::sync::Lazy;
4use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
5use qiniu_http::{
6    header::{HeaderName, IntoHeaderName, CONTENT_TYPE},
7    HeaderMap, HeaderValue,
8};
9use qiniu_utils::{smallstr::SmallString, wrap_smallstr};
10use rand::random;
11use regex::Regex;
12use serde::{
13    de::{Deserialize, Deserializer, Error, Visitor},
14    ser::{Serialize, Serializer},
15};
16use smallvec::SmallVec;
17use std::{
18    borrow::{Borrow, BorrowMut, Cow},
19    collections::VecDeque,
20    ffi::OsStr,
21    fmt,
22    iter::FromIterator,
23    ops::{Deref, DerefMut, Index, IndexMut, Range, RangeFrom, RangeFull, RangeTo},
24};
25
26/// 文件名
27#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub struct FileName {
29    inner: SmallString<[u8; 64]>,
30}
31wrap_smallstr!(FileName);
32
33/// Multipart 字段名称
34#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
35pub struct FieldName {
36    inner: SmallString<[u8; 16]>,
37}
38wrap_smallstr!(FieldName);
39
40#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
41struct Boundary {
42    inner: SmallString<[u8; 32]>,
43}
44wrap_smallstr!(Boundary);
45
46type HeaderBuffer = SmallVec<[u8; 256]>;
47
48/// Multipart 表单
49///
50/// ### 发送 Mutlipart 表单代码实例
51///
52/// ```
53/// async fn example() -> anyhow::Result<()> {
54/// # use async_std::io::ReadExt;
55/// use qiniu_credential::Credential;
56/// use qiniu_http_client::{
57///     prelude::*, AsyncMultipart, AsyncPart, BucketRegionsQueryer, HttpClient, Idempotent, PartMetadata, RegionsProviderEndpoints, ServiceName,
58/// };
59/// use qiniu_upload_token::UploadPolicy;
60/// use serde_json::Value;
61/// use std::time::Duration;
62///
63/// # let file = async_std::io::Cursor::new(vec![0u8; 1024]);
64/// let credential = Credential::new("abcdefghklmnopq", "1234567890");
65/// let bucket_name = "test-bucket";
66/// let object_name = "test-key";
67/// let provider = UploadPolicy::new_for_object(bucket_name, object_name, Duration::from_secs(3600))
68///     .build()
69///     .into_dynamic_upload_token_provider(&credential);
70/// let upload_token = provider
71///     .async_to_token_string(Default::default())
72///     .await?;
73/// let value: Value = HttpClient::default()
74///     .async_post(
75///         &[ServiceName::Up],
76///         RegionsProviderEndpoints::new(
77///             BucketRegionsQueryer::new().query(credential.access_key().to_owned(), bucket_name),
78///         ),
79///     )
80///     .idempotent(Idempotent::Always)
81///     .accept_json()
82///     .multipart(
83///         AsyncMultipart::new()
84///             .add_part("token", AsyncPart::text(upload_token))
85///             .add_part("key", AsyncPart::text(object_name))
86///             .add_part(
87///                 "file",
88///                 AsyncPart::stream(file).metadata(PartMetadata::default().file_name("fakefilename.bin")),
89///             ),
90///     )
91///     .await?
92///     .call()
93///     .await?
94///     .parse_json()
95///     .await?
96///     .into_body();
97/// # Ok(())
98/// # }
99/// ```
100#[derive(Debug)]
101pub struct Multipart<P> {
102    boundary: Boundary,
103    fields: VecDeque<(FieldName, P)>,
104}
105
106/// Multipart 表单组件
107#[derive(Debug)]
108pub struct Part<B> {
109    meta: PartMetadata,
110    body: B,
111}
112
113impl<P> Default for Multipart<P> {
114    #[inline]
115    fn default() -> Self {
116        Self::new()
117    }
118}
119
120impl<P> Multipart<P> {
121    /// 创建 Multipart 表单
122    #[inline]
123    pub fn new() -> Self {
124        Self {
125            boundary: gen_boundary(),
126            fields: Default::default(),
127        }
128    }
129
130    pub(super) fn boundary(&self) -> &str {
131        &self.boundary
132    }
133
134    /// 添加 Multipart 表单组件
135    #[inline]
136    #[must_use]
137    pub fn add_part(mut self, name: impl Into<FieldName>, part: P) -> Self {
138        self.fields.push_back((name.into(), part));
139        self
140    }
141}
142
143impl<P: Sync + Send> Multipart<P> {
144    #[allow(dead_code)]
145    fn ignore() {
146        assert_impl!(Send: Self);
147        assert_impl!(Sync: Self);
148    }
149}
150
151/// Multipart 表单组件元信息
152#[derive(Default, Debug)]
153pub struct PartMetadata {
154    headers: HeaderMap,
155    file_name: Option<FileName>,
156}
157
158impl PartMetadata {
159    /// 设置表单组件的 MIME 类型
160    #[inline]
161    #[must_use]
162    pub fn mime(self, mime: Mime) -> Self {
163        self.add_header(CONTENT_TYPE, HeaderValue::from_str(mime.as_ref()).unwrap())
164    }
165
166    /// 添加表单组件的 HTTP 头
167    #[inline]
168    #[must_use]
169    pub fn add_header(mut self, name: impl IntoHeaderName, value: impl Into<HeaderValue>) -> Self {
170        self.headers.insert(name, value.into());
171        self
172    }
173
174    /// 设置表单组件的文件名
175    #[inline]
176    #[must_use]
177    pub fn file_name(mut self, file_name: impl Into<FileName>) -> Self {
178        self.file_name = Some(file_name.into());
179        self
180    }
181}
182
183impl Extend<(HeaderName, HeaderValue)> for PartMetadata {
184    #[inline]
185    fn extend<T: IntoIterator<Item = (HeaderName, HeaderValue)>>(&mut self, iter: T) {
186        self.headers.extend(iter)
187    }
188}
189
190impl Extend<(Option<HeaderName>, HeaderValue)> for PartMetadata {
191    #[inline]
192    fn extend<T: IntoIterator<Item = (Option<HeaderName>, HeaderValue)>>(&mut self, iter: T) {
193        self.headers.extend(iter)
194    }
195}
196
197impl<B> Part<B> {
198    /// 设置 Multipart 表单组件的元信息
199    #[inline]
200    #[must_use]
201    pub fn metadata(mut self, metadata: PartMetadata) -> Self {
202        self.meta = metadata;
203        self
204    }
205}
206
207impl<B: Sync + Send> Part<B> {
208    #[allow(dead_code)]
209    fn ignore() {
210        assert_impl!(Send: Self);
211        assert_impl!(Sync: Self);
212    }
213}
214
215mod sync_part {
216    use super::*;
217    use std::{
218        fmt::{self, Debug},
219        fs::File,
220        io::{Cursor, Read, Result as IoResult},
221        mem::take,
222        path::Path,
223    };
224
225    enum SyncPartBodyInner<'a> {
226        Bytes(Cursor<Cow<'a, [u8]>>),
227        Stream(Box<dyn Read + 'a>),
228    }
229
230    impl Debug for SyncPartBodyInner<'_> {
231        #[inline]
232        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233            match self {
234                Self::Bytes(bytes) => f.debug_tuple("Bytes").field(bytes).finish(),
235                Self::Stream(_) => f.debug_tuple("Stream").finish(),
236            }
237        }
238    }
239
240    /// 阻塞 Multipart 表单组件请求体
241    #[derive(Debug)]
242    pub struct SyncPartBody<'a>(SyncPartBodyInner<'a>);
243
244    /// 阻塞 Multipart 表单组件
245    pub type SyncPart<'a> = Part<SyncPartBody<'a>>;
246
247    impl<'a> SyncPart<'a> {
248        /// 设置阻塞 Multipart 的请求体为字符串
249        #[inline]
250        #[must_use]
251        pub fn text(value: impl Into<Cow<'a, str>>) -> Self {
252            let bytes = match value.into() {
253                Cow::Borrowed(str) => Cow::Borrowed(str.as_bytes()),
254                Cow::Owned(string) => Cow::Owned(string.into_bytes()),
255            };
256            Self {
257                body: SyncPartBody(SyncPartBodyInner::Bytes(Cursor::new(bytes))),
258                meta: Default::default(),
259            }
260        }
261
262        /// 设置阻塞 Multipart 的请求体为内存数据
263        #[inline]
264        #[must_use]
265        pub fn bytes(value: impl Into<Cow<'a, [u8]>>) -> Self {
266            Self {
267                body: SyncPartBody(SyncPartBodyInner::Bytes(Cursor::new(value.into()))),
268                meta: Default::default(),
269            }
270        }
271
272        /// 设置阻塞 Multipart 的请求体为输入流
273        #[inline]
274        #[must_use]
275        pub fn stream(value: impl Read + 'a) -> Self {
276            Self {
277                body: SyncPartBody(SyncPartBodyInner::Stream(Box::new(value))),
278                meta: Default::default(),
279            }
280        }
281
282        /// 设置阻塞 Multipart 的请求体为文件
283        pub fn file_path<S: AsRef<OsStr> + ?Sized>(path: &S) -> IoResult<Self> {
284            let path = Path::new(path);
285            let file = File::open(path)?;
286            let mut metadata = PartMetadata::default().mime(mime_guess::from_path(path).first_or_octet_stream());
287            if let Some(file_name) = path.file_name() {
288                let file_name = match file_name.to_string_lossy() {
289                    Cow::Borrowed(str) => FileName::from(str),
290                    Cow::Owned(string) => FileName::from(string),
291                };
292                metadata = metadata.file_name(file_name);
293            }
294            Ok(SyncPart::stream(file).metadata(metadata))
295        }
296    }
297
298    /// 阻塞 Multipart
299    pub type SyncMultipart<'a> = Multipart<SyncPart<'a>>;
300
301    impl<'a> SyncMultipart<'a> {
302        pub(in super::super) fn into_read(mut self) -> Box<dyn Read + 'a> {
303            if self.fields.is_empty() {
304                return Box::new(Cursor::new([]));
305            }
306
307            let (name, part) = self.fields.pop_front().unwrap();
308            let chain = Box::new(self.part_stream(&name, part)) as Box<dyn Read + 'a>;
309            let fields = take(&mut self.fields);
310            Box::new(
311                fields
312                    .into_iter()
313                    .fold(chain, |readable, (name, part)| {
314                        Box::new(readable.chain(self.part_stream(&name, part))) as Box<dyn Read + 'a>
315                    })
316                    .chain(Cursor::new(b"--"))
317                    .chain(Cursor::new(self.boundary.to_owned()))
318                    .chain(Cursor::new(b"--\r\n")),
319            )
320        }
321
322        fn part_stream(&self, name: &str, part: SyncPart<'a>) -> impl Read + 'a {
323            Cursor::new(b"--")
324                .chain(Cursor::new(self.boundary.to_owned()))
325                .chain(Cursor::new(b"\r\n"))
326                .chain(Cursor::new(encode_headers(name, &part.meta)))
327                .chain(Cursor::new(b"\r\n\r\n"))
328                .chain(part.body)
329                .chain(Cursor::new(b"\r\n"))
330        }
331    }
332
333    impl Read for SyncPartBody<'_> {
334        #[inline]
335        fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> {
336            match &mut self.0 {
337                SyncPartBodyInner::Bytes(bytes) => bytes.read(buf),
338                SyncPartBodyInner::Stream(stream) => stream.read(buf),
339            }
340        }
341    }
342}
343pub use sync_part::{SyncMultipart, SyncPart, SyncPartBody};
344
345#[cfg(feature = "async")]
346mod async_part {
347    use super::*;
348    use async_std::{fs::File, path::Path};
349    use futures::io::{AsyncRead, AsyncReadExt, Cursor};
350    use std::{
351        fmt::{self, Debug},
352        io::Result as IoResult,
353        mem::take,
354        pin::Pin,
355        task::{Context, Poll},
356    };
357
358    type AsyncStream<'a> = Box<dyn AsyncRead + Send + Unpin + 'a>;
359
360    enum AsyncPartBodyInner<'a> {
361        Bytes(Cursor<Cow<'a, [u8]>>),
362        Stream(AsyncStream<'a>),
363    }
364
365    impl Debug for AsyncPartBodyInner<'_> {
366        #[inline]
367        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
368            match self {
369                Self::Bytes(bytes) => f.debug_tuple("Bytes").field(bytes).finish(),
370                Self::Stream(_) => f.debug_tuple("Stream").finish(),
371            }
372        }
373    }
374
375    /// 异步 Multipart 表单组件请求体
376    #[derive(Debug)]
377    #[cfg_attr(feature = "docs", doc(cfg(feature = "async")))]
378    pub struct AsyncPartBody<'a>(AsyncPartBodyInner<'a>);
379
380    /// 异步 Multipart 表单组件
381    #[cfg_attr(feature = "docs", doc(cfg(feature = "async")))]
382    pub type AsyncPart<'a> = Part<AsyncPartBody<'a>>;
383
384    impl<'a> AsyncPart<'a> {
385        /// 设置异步 Multipart 的请求体为字符串
386        #[inline]
387        #[must_use]
388        pub fn text(value: impl Into<Cow<'a, str>>) -> Self {
389            let bytes = match value.into() {
390                Cow::Borrowed(slice) => Cow::Borrowed(slice.as_bytes()),
391                Cow::Owned(string) => Cow::Owned(string.into_bytes()),
392            };
393            Self {
394                body: AsyncPartBody(AsyncPartBodyInner::Bytes(Cursor::new(bytes))),
395                meta: Default::default(),
396            }
397        }
398
399        /// 设置异步 Multipart 的请求体为内存数据
400        #[inline]
401        #[must_use]
402        pub fn bytes(value: impl Into<Cow<'a, [u8]>>) -> Self {
403            Self {
404                body: AsyncPartBody(AsyncPartBodyInner::Bytes(Cursor::new(value.into()))),
405                meta: Default::default(),
406            }
407        }
408
409        /// 设置异步 Multipart 的请求体为异步输入流
410        #[inline]
411        #[must_use]
412        pub fn stream(value: impl AsyncRead + Send + Unpin + 'a) -> Self {
413            Self {
414                body: AsyncPartBody(AsyncPartBodyInner::Stream(Box::new(value))),
415                meta: Default::default(),
416            }
417        }
418
419        /// 设置异步 Multipart 的请求体为文件
420        pub async fn file_path<S: AsRef<OsStr> + ?Sized>(path: &S) -> IoResult<AsyncPart<'a>> {
421            let path = Path::new(path);
422            let file = File::open(&path).await?;
423            let mut metadata = PartMetadata::default().mime(mime_guess::from_path(path).first_or_octet_stream());
424            if let Some(file_name) = path.file_name() {
425                let file_name = match file_name.to_string_lossy() {
426                    Cow::Borrowed(str) => FileName::from(str),
427                    Cow::Owned(string) => FileName::from(string),
428                };
429                metadata = metadata.file_name(file_name);
430            }
431            Ok(AsyncPart::stream(file).metadata(metadata))
432        }
433    }
434
435    /// 异步 Multipart
436    #[cfg_attr(feature = "docs", doc(cfg(feature = "async")))]
437    pub type AsyncMultipart<'a> = Multipart<AsyncPart<'a>>;
438
439    impl<'a> AsyncMultipart<'a> {
440        pub(in super::super) fn into_async_read(mut self) -> Box<dyn AsyncRead + Send + Unpin + 'a> {
441            if self.fields.is_empty() {
442                return Box::new(Cursor::new([]));
443            }
444
445            let (name, part) = self.fields.pop_front().unwrap();
446            let chain = Box::new(self.part_stream(&name, part)) as Box<dyn AsyncRead + Send + Unpin + 'a>;
447            let fields = take(&mut self.fields);
448            Box::new(
449                fields
450                    .into_iter()
451                    .fold(chain, |readable, (name, part)| {
452                        Box::new(readable.chain(self.part_stream(&name, part)))
453                            as Box<dyn AsyncRead + Send + Unpin + 'a>
454                    })
455                    .chain(Cursor::new(b"--"))
456                    .chain(Cursor::new(self.boundary.to_owned()))
457                    .chain(Cursor::new(b"--\r\n")),
458            )
459        }
460
461        fn part_stream(&self, name: &str, part: AsyncPart<'a>) -> impl AsyncRead + Send + Unpin + 'a {
462            Cursor::new(b"--")
463                .chain(Cursor::new(self.boundary.to_owned()))
464                .chain(Cursor::new(b"\r\n"))
465                .chain(Cursor::new(encode_headers(name, &part.meta)))
466                .chain(Cursor::new(b"\r\n\r\n"))
467                .chain(part.body)
468                .chain(Cursor::new(b"\r\n"))
469        }
470    }
471
472    impl AsyncRead for AsyncPartBody<'_> {
473        #[inline]
474        fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll<IoResult<usize>> {
475            match &mut self.0 {
476                AsyncPartBodyInner::Bytes(bytes) => Pin::new(bytes).poll_read(cx, buf),
477                AsyncPartBodyInner::Stream(stream) => Pin::new(stream).poll_read(cx, buf),
478            }
479        }
480    }
481}
482
483#[cfg(feature = "async")]
484pub use async_part::{AsyncMultipart, AsyncPart, AsyncPartBody};
485
486fn gen_boundary() -> Boundary {
487    use std::fmt::Write;
488
489    let mut b = Boundary::with_capacity(32);
490    write!(b, "{:016x}{:016x}", random::<u64>(), random::<u64>()).unwrap();
491    b
492}
493
494fn encode_headers(name: &str, field: &PartMetadata) -> HeaderBuffer {
495    let mut buf = HeaderBuffer::from_slice(b"content-disposition: form-data; ");
496    buf.extend_from_slice(&format_parameter("name", name));
497    if let Some(file_name) = field.file_name.as_ref() {
498        buf.extend_from_slice(b"; ");
499        buf.extend_from_slice(format_file_name(file_name).as_bytes());
500    }
501    for (name, value) in field.headers.iter() {
502        buf.extend_from_slice(b"\r\n");
503        buf.extend_from_slice(name.as_str().as_bytes());
504        buf.extend_from_slice(b": ");
505        buf.extend_from_slice(value.as_bytes());
506    }
507    buf
508}
509
510fn format_file_name(filename: &str) -> FileName {
511    static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("\\\\|\"|\r|\n").unwrap());
512    let mut formatted = FileName::from("filename=\"");
513    let mut last_match = 0;
514    for m in REGEX.find_iter(filename) {
515        let begin = m.start();
516        let end = m.end();
517        formatted.push_str(&filename[last_match..begin]);
518        match &filename[begin..end] {
519            "\\" => formatted.push_str("\\\\"),
520            "\"" => formatted.push_str("\\\""),
521            "\r" => formatted.push_str("\\\r"),
522            "\n" => formatted.push_str("\\\n"),
523            _ => unreachable!(),
524        }
525        last_match = end;
526    }
527    formatted.push_str(&filename[last_match..]);
528    formatted.push_str("\"");
529    formatted
530}
531
532const PATH_SEGMENT_ENCODE_SET: &AsciiSet = &CONTROLS
533    .add(b' ')
534    .add(b'"')
535    .add(b'<')
536    .add(b'>')
537    .add(b'`')
538    .add(b'#')
539    .add(b'?')
540    .add(b'{')
541    .add(b'}')
542    .add(b'/')
543    .add(b'%');
544
545fn format_parameter(name: &str, value: &str) -> HeaderBuffer {
546    let legal_value = {
547        let mut buf = HeaderBuffer::new();
548        for chunk in utf8_percent_encode(value, PATH_SEGMENT_ENCODE_SET) {
549            buf.extend_from_slice(chunk.as_bytes());
550        }
551        buf
552    };
553    let mut formatted = HeaderBuffer::from_slice(name.as_bytes());
554    if value.len() == legal_value.len() {
555        formatted.extend_from_slice(b"=\"");
556        formatted.extend_from_slice(value.as_bytes());
557        formatted.extend_from_slice(b"\"");
558    } else {
559        formatted.extend_from_slice(b"*=utf-8''");
560        formatted.extend_from_slice(&legal_value);
561    };
562    formatted
563}
564
565#[cfg(test)]
566mod tests {
567    use super::*;
568    use mime::{APPLICATION_JSON, IMAGE_BMP};
569    use std::{
570        fs::File,
571        io::{Cursor, Result as IoResult, Write},
572    };
573    use tempfile::tempdir;
574
575    #[test]
576    fn test_gen_boundary() {
577        env_logger::builder().is_test(true).try_init().ok();
578
579        for _ in 0..5 {
580            assert_eq!(gen_boundary().len(), 32);
581        }
582    }
583
584    #[test]
585    fn test_header_percent_encoding() {
586        env_logger::builder().is_test(true).try_init().ok();
587
588        let name = "start%'\"\r\nßend";
589        let metadata = PartMetadata {
590            headers: {
591                let mut headers = HeaderMap::default();
592                headers.insert(CONTENT_TYPE, HeaderValue::from_str(APPLICATION_JSON.as_ref()).unwrap());
593                headers
594            },
595            file_name: Some(name.into()),
596        };
597
598        assert_eq!(
599            encode_headers(name, &metadata).as_ref(),
600            "content-disposition: form-data; name*=utf-8''start%25'%22%0D%0A%C3%9Fend; filename=\"start%'\\\"\\\r\\\nßend\"\r\ncontent-type: application/json".as_bytes()
601        );
602    }
603
604    #[test]
605    fn test_multipart_into_read() -> IoResult<()> {
606        env_logger::builder().is_test(true).try_init().ok();
607
608        let tempdir = tempdir()?;
609        let temp_file_path = tempdir.path().join("fake-file.json");
610        let mut file = File::create(&temp_file_path)?;
611        file.write_all(b"{\"a\":\"b\"}\n")?;
612        drop(file);
613
614        let mut multipart = SyncMultipart::new()
615            .add_part("bytes1", SyncPart::bytes(b"part1".as_slice()))
616            .add_part("text1", SyncPart::text("value1"))
617            .add_part(
618                "text2",
619                SyncPart::text("value1").metadata(PartMetadata::default().mime(IMAGE_BMP)),
620            )
621            .add_part("reader1", SyncPart::stream(Cursor::new(b"value1")))
622            .add_part("reader2", SyncPart::file_path(&temp_file_path)?);
623        multipart.boundary = "boundary".into();
624
625        const EXPECTED: &str = "--boundary\r\n\
626        content-disposition: form-data; name=\"bytes1\"\r\n\r\n\
627        part1\r\n\
628        --boundary\r\n\
629        content-disposition: form-data; name=\"text1\"\r\n\r\n\
630        value1\r\n\
631        --boundary\r\n\
632        content-disposition: form-data; name=\"text2\"\r\n\
633        content-type: image/bmp\r\n\r\n\
634        value1\r\n\
635        --boundary\r\n\
636        content-disposition: form-data; name=\"reader1\"\r\n\r\n\
637        value1\r\n\
638        --boundary\r\n\
639        content-disposition: form-data; name=\"reader2\"; filename=\"fake-file.json\"\r\n\
640        content-type: application/json\r\n\r\n\
641        {\"a\":\"b\"}\n\r\n\
642        --boundary--\
643        \r\n";
644
645        let mut actual = String::new();
646        multipart.into_read().read_to_string(&mut actual)?;
647        assert_eq!(EXPECTED, actual);
648
649        tempdir.close()?;
650        Ok(())
651    }
652
653    #[cfg(feature = "async")]
654    #[async_std::test]
655    async fn test_multipart_into_async_read() -> IoResult<()> {
656        use async_std::{
657            fs::File,
658            io::{Cursor as AsyncCursor, ReadExt, WriteExt},
659        };
660
661        env_logger::builder().is_test(true).try_init().ok();
662
663        let tempdir = tempdir()?;
664        let temp_file_path = tempdir.path().join("fake-file.json");
665        let mut file = File::create(&temp_file_path).await?;
666        file.write_all(b"{\"a\":\"b\"}\n").await?;
667        file.flush().await?;
668        drop(file);
669
670        let mut multipart = AsyncMultipart::new()
671            .add_part("bytes1", AsyncPart::bytes(b"part1".as_slice()))
672            .add_part("text1", AsyncPart::text("value1"))
673            .add_part(
674                "text2",
675                AsyncPart::text("value1").metadata(PartMetadata::default().mime(IMAGE_BMP)),
676            )
677            .add_part("reader1", AsyncPart::stream(AsyncCursor::new(b"value1")))
678            .add_part("reader2", AsyncPart::file_path(&temp_file_path).await?);
679        multipart.boundary = "boundary".into();
680
681        const EXPECTED: &str = "--boundary\r\n\
682        content-disposition: form-data; name=\"bytes1\"\r\n\r\n\
683        part1\r\n\
684        --boundary\r\n\
685        content-disposition: form-data; name=\"text1\"\r\n\r\n\
686        value1\r\n\
687        --boundary\r\n\
688        content-disposition: form-data; name=\"text2\"\r\n\
689        content-type: image/bmp\r\n\r\n\
690        value1\r\n\
691        --boundary\r\n\
692        content-disposition: form-data; name=\"reader1\"\r\n\r\n\
693        value1\r\n\
694        --boundary\r\n\
695        content-disposition: form-data; name=\"reader2\"; filename=\"fake-file.json\"\r\n\
696        content-type: application/json\r\n\r\n\
697        {\"a\":\"b\"}\n\r\n\
698        --boundary--\
699        \r\n";
700
701        let mut actual = String::new();
702        multipart.into_async_read().read_to_string(&mut actual).await?;
703        assert_eq!(EXPECTED, actual);
704
705        tempdir.close()?;
706        Ok(())
707    }
708}