multipart_form_data/
lib.rs

1// Ref https://github.com/abonander/multipart/blob/0.7.0/src/client/mod.rs#L193
2
3use std::io::{BufWriter, Error as IoError, Write};
4
5#[derive(Debug)]
6pub struct MultipartFormDataWriter<W>
7where
8    W: Write,
9{
10    buf_writer: BufWriter<W>,
11    pub boundary: String,
12}
13
14impl<W> MultipartFormDataWriter<W>
15where
16    W: Write,
17{
18    pub fn new(writer: W) -> Self {
19        Self {
20            buf_writer: BufWriter::new(writer),
21            boundary: multipart_boundary::generate(),
22        }
23    }
24
25    pub fn with_boundary(writer: W, boundary: impl AsRef<str>) -> Self {
26        Self {
27            buf_writer: BufWriter::new(writer),
28            boundary: boundary.as_ref().to_owned(),
29        }
30    }
31
32    pub fn write_text_field(
33        &mut self,
34        name: impl AsRef<str>,
35        value: impl AsRef<str>,
36    ) -> Result<(), IoError> {
37        self.write_field(name, value.as_ref(), None, None, None)
38    }
39
40    pub fn write_field<'a>(
41        &mut self,
42        name: impl AsRef<str>,
43        value: impl AsRef<[u8]>,
44        filename: impl Into<Option<&'a str>>,
45        content_type: impl Into<Option<&'a str>>,
46        headers: impl Into<Option<Vec<(&'a str, &'a str)>>>,
47    ) -> Result<(), IoError> {
48        self.buf_writer.write_all(b"--")?;
49        self.buf_writer.write_all(self.boundary.as_bytes())?;
50        self.buf_writer.write_all(b"\r\n")?;
51
52        self.buf_writer
53            .write_all(br#"Content-Disposition: form-data; name=""#)?;
54        self.buf_writer.write_all(name.as_ref().as_bytes())?;
55        self.buf_writer.write_all(br#"""#)?;
56
57        if let Some(filename) = filename.into() {
58            self.buf_writer.write_all(br#"; filename=""#)?;
59            self.buf_writer.write_all(filename.as_bytes())?;
60            self.buf_writer.write_all(br#"""#)?;
61        }
62
63        self.buf_writer.write_all(b"\r\n")?;
64
65        if let Some(content_type) = content_type.into() {
66            self.buf_writer.write_all(b"Content-Type: ")?;
67            self.buf_writer.write_all(content_type.as_bytes())?;
68            self.buf_writer.write_all(b"\r\n")?;
69        }
70        if let Some(headers) = headers.into() {
71            for (k, v) in headers.into_iter() {
72                self.buf_writer.write_all(k.as_bytes())?;
73                self.buf_writer.write_all(b": ")?;
74                self.buf_writer.write_all(v.as_bytes())?;
75                self.buf_writer.write_all(b"\r\n")?;
76            }
77        }
78
79        self.buf_writer.write_all(b"\r\n")?;
80
81        self.buf_writer.write_all(value.as_ref())?;
82        self.buf_writer.write_all(b"\r\n")?;
83
84        Ok(())
85    }
86
87    pub fn finish(mut self) -> Result<W, IoError> {
88        self.buf_writer.write_all(b"--")?;
89        self.buf_writer.write_all(self.boundary.as_bytes())?;
90        self.buf_writer.write_all(b"--")?;
91
92        self.buf_writer.write_all(b"\r\n")?;
93
94        Ok(self.buf_writer.into_inner()?)
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn test_writer() {
104        //
105        let mut writer = MultipartFormDataWriter::with_boundary(
106            vec![],
107            "------------------------afb08437765cfecd",
108        );
109        writer.write_text_field("foo", "bar").unwrap();
110        let buf = writer.finish().unwrap();
111        println!("{}", String::from_utf8(buf.clone()).unwrap());
112        assert_eq!(buf, include_bytes!("../tests/curl_F_body_files/case1.txt"));
113
114        //
115        let mut writer = MultipartFormDataWriter::with_boundary(
116            vec![],
117            "------------------------8cde15cb2484c740",
118        );
119        writer
120            .write_field(
121                "foo",
122                "bar",
123                "foo.txt",
124                "text/plain",
125                vec![("X-A", "1"), ("X-B", "2")],
126            )
127            .unwrap();
128        let buf = writer.finish().unwrap();
129        println!("{}", String::from_utf8(buf.clone()).unwrap());
130        assert_eq!(buf, include_bytes!("../tests/curl_F_body_files/case2.txt"));
131    }
132
133    #[test]
134    fn test_httpbin() {
135        use std::{collections::HashMap, time::Duration};
136
137        use isahc::{
138            config::Configurable as _,
139            http::{header::CONTENT_TYPE, Method},
140            HttpClient, ReadResponseExt as _, Request,
141        };
142        use serde::Deserialize;
143
144        #[derive(Deserialize)]
145        struct Body {
146            form: HashMap<String, String>,
147            headers: HashMap<String, String>,
148        }
149
150        //
151        let mut writer = MultipartFormDataWriter::new(vec![]);
152        let boundary = writer.boundary.to_owned();
153        writer.write_text_field("foo", "bar").unwrap();
154        let buf = writer.finish().unwrap();
155
156        //
157        let request = Request::builder()
158            .method(Method::POST)
159            .uri("http://httpbin.org/post")
160            .header(
161                CONTENT_TYPE,
162                format!("multipart/form-data; boundary={}", boundary),
163            )
164            .body(buf)
165            .unwrap();
166
167        let client = HttpClient::builder()
168            .timeout(Duration::from_secs(5))
169            .build()
170            .unwrap();
171
172        let mut response = match client.send(request) {
173            Ok(x) => x,
174            Err(err) => {
175                eprintln!("client.send failed, err: {}", err);
176                return;
177            }
178        };
179
180        let body = response.bytes().unwrap();
181        let body: Body = serde_json::from_slice(&body).unwrap();
182
183        assert_eq!(
184            body.form,
185            vec![("foo".to_owned(), "bar".to_owned())]
186                .into_iter()
187                .collect(),
188        );
189        assert_eq!(
190            body.headers.get("Content-Type").cloned().unwrap(),
191            format!("multipart/form-data; boundary={}", boundary)
192        );
193        assert_eq!(body.headers.get("Content-Length").cloned().unwrap(), "141");
194    }
195}