rig/http_client/
multipart.rs1use bytes::Bytes;
2use mime::Mime;
3use std::borrow::Cow;
4
5#[derive(Clone, Debug)]
7pub struct Part {
8 name: String,
9 content: PartContent,
10 filename: Option<String>,
11 content_type: Option<Mime>,
12}
13
14#[derive(Clone, Debug)]
15enum PartContent {
16 Text(String),
17 Binary(Bytes),
18}
19
20impl Part {
21 pub fn text(name: impl Into<String>, value: impl Into<String>) -> Self {
23 Self {
24 name: name.into(),
25 content: PartContent::Text(value.into()),
26 filename: None,
27 content_type: None,
28 }
29 }
30
31 pub fn bytes(name: impl Into<String>, data: impl Into<Bytes>) -> Self {
33 Self {
34 name: name.into(),
35 content: PartContent::Binary(data.into()),
36 filename: None,
37 content_type: None,
38 }
39 }
40
41 pub fn filename(mut self, filename: impl Into<String>) -> Self {
43 self.filename = Some(filename.into());
44 self
45 }
46
47 pub fn content_type(mut self, content_type: Mime) -> Self {
49 self.content_type = Some(content_type);
50 self
51 }
52
53 pub fn name(&self) -> &str {
55 &self.name
56 }
57
58 pub fn get_filename(&self) -> Option<&str> {
60 self.filename.as_deref()
61 }
62
63 pub fn get_content_type(&self) -> Option<&Mime> {
65 self.content_type.as_ref()
66 }
67}
68
69#[derive(Clone, Debug, Default)]
71pub struct MultipartForm {
72 parts: Vec<Part>,
73 boundary: Option<String>,
74}
75
76impl MultipartForm {
77 pub fn new() -> Self {
79 Self::default()
80 }
81
82 pub fn part(mut self, part: Part) -> Self {
84 self.parts.push(part);
85 self
86 }
87
88 pub fn text(self, name: impl Into<String>, value: impl Into<String>) -> Self {
90 self.part(Part::text(name, value))
91 }
92
93 pub fn file(
95 self,
96 name: impl Into<String>,
97 filename: impl Into<String>,
98 content_type: Mime,
99 data: impl Into<Bytes>,
100 ) -> Self {
101 self.part(
102 Part::bytes(name, data)
103 .filename(filename)
104 .content_type(content_type),
105 )
106 }
107
108 pub fn boundary(mut self, boundary: impl Into<String>) -> Self {
110 self.boundary = Some(boundary.into());
111 self
112 }
113
114 pub fn parts(&self) -> &[Part] {
116 &self.parts
117 }
118
119 fn generate_boundary() -> String {
121 use std::time::{SystemTime, UNIX_EPOCH};
122 let timestamp = SystemTime::now()
123 .duration_since(UNIX_EPOCH)
124 .unwrap()
125 .as_nanos();
126 format!("----boundary{}", timestamp)
127 }
128
129 fn get_boundary(&self) -> Cow<'_, str> {
131 match &self.boundary {
132 Some(b) => Cow::Borrowed(b),
133 None => Cow::Owned(Self::generate_boundary()),
134 }
135 }
136
137 pub fn encode(&self) -> (String, Bytes) {
139 let boundary = self.get_boundary();
140 let mut body = Vec::new();
141
142 for part in &self.parts {
143 body.extend_from_slice(b"--");
144 body.extend_from_slice(boundary.as_bytes());
145 body.extend_from_slice(b"\r\n");
146
147 body.extend_from_slice(b"Content-Disposition: form-data; name=\"");
149 body.extend_from_slice(part.name.as_bytes());
150 body.extend_from_slice(b"\"");
151
152 if let Some(filename) = &part.filename {
153 body.extend_from_slice(b"; filename=\"");
154 body.extend_from_slice(filename.as_bytes());
155 body.extend_from_slice(b"\"");
156 }
157 body.extend_from_slice(b"\r\n");
158
159 if let Some(content_type) = &part.content_type {
161 body.extend_from_slice(b"Content-Type: ");
162 body.extend_from_slice(content_type.as_ref().as_bytes());
163 body.extend_from_slice(b"\r\n");
164 }
165
166 body.extend_from_slice(b"\r\n");
167
168 match &part.content {
170 PartContent::Text(text) => body.extend_from_slice(text.as_bytes()),
171 PartContent::Binary(bytes) => body.extend_from_slice(bytes),
172 }
173
174 body.extend_from_slice(b"\r\n");
175 }
176
177 body.extend_from_slice(b"--");
179 body.extend_from_slice(boundary.as_bytes());
180 body.extend_from_slice(b"--\r\n");
181
182 (boundary.into_owned(), Bytes::from(body))
183 }
184}
185
186impl From<MultipartForm> for reqwest::multipart::Form {
187 fn from(value: MultipartForm) -> Self {
188 let mut form = reqwest::multipart::Form::new();
189
190 for part in value.parts {
191 match part.content {
192 PartContent::Text(text) => {
193 form = form.text(part.name, text);
194 }
195 PartContent::Binary(bytes) => {
196 let mut req_part = reqwest::multipart::Part::bytes(bytes.to_vec());
197
198 if let Some(filename) = part.filename {
199 req_part = req_part.file_name(filename);
200 }
201 if let Some(content_type) = part.content_type {
202 req_part = req_part.mime_str(content_type.as_ref()).unwrap();
203 }
204
205 form = form.part(part.name, req_part);
206 }
207 }
208 }
209
210 form
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn test_multipart_encoding() {
220 let form = MultipartForm::new()
221 .text("field1", "value1")
222 .text("field2", "value2");
223
224 let (boundary, body) = form.encode();
225 let body_str = String::from_utf8_lossy(&body);
226
227 assert!(body_str.contains("field1"));
228 assert!(body_str.contains("value1"));
229 assert!(body_str.contains(&boundary));
230 }
231
232 #[test]
233 fn test_file_part() {
234 let form = MultipartForm::new().file(
235 "upload",
236 "test.txt",
237 "text/plain".parse().unwrap(),
238 Bytes::from("file contents"),
239 );
240
241 let (_, body) = form.encode();
242 let body_str = String::from_utf8_lossy(&body);
243
244 assert!(body_str.contains("filename=\"test.txt\""));
245 assert!(body_str.contains("Content-Type: text/plain"));
246 assert!(body_str.contains("file contents"));
247 }
248}