multipart_form_data/
lib.rs1use 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 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 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 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 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}