rig_http/http_client/
multipart.rs1use bytes::Bytes;
12use mime::Mime;
13use std::borrow::Cow;
14
15#[derive(Clone, Debug)]
17pub struct Part {
18 name: String,
19 content: PartContent,
20 filename: Option<String>,
21 content_type: Option<Mime>,
22}
23
24#[derive(Clone, Debug)]
26pub enum PartContent {
27 Text(String),
29 Binary(Bytes),
31}
32
33impl Part {
34 pub fn text(name: impl Into<String>, value: impl Into<String>) -> Self {
36 Self {
37 name: name.into(),
38 content: PartContent::Text(value.into()),
39 filename: None,
40 content_type: None,
41 }
42 }
43
44 pub fn bytes(name: impl Into<String>, data: impl Into<Bytes>) -> Self {
46 Self {
47 name: name.into(),
48 content: PartContent::Binary(data.into()),
49 filename: None,
50 content_type: None,
51 }
52 }
53
54 pub fn filename(mut self, filename: impl Into<String>) -> Self {
56 self.filename = Some(filename.into());
57 self
58 }
59
60 pub fn content_type(mut self, content_type: Mime) -> Self {
62 self.content_type = Some(content_type);
63 self
64 }
65
66 pub fn name(&self) -> &str {
68 &self.name
69 }
70
71 pub fn get_filename(&self) -> Option<&str> {
73 self.filename.as_deref()
74 }
75
76 pub fn get_content_type(&self) -> Option<&Mime> {
78 self.content_type.as_ref()
79 }
80
81 pub fn content(&self) -> &PartContent {
83 &self.content
84 }
85
86 pub fn into_pieces(self) -> (String, PartContent, Option<String>, Option<Mime>) {
89 (self.name, self.content, self.filename, self.content_type)
90 }
91}
92
93#[derive(Clone, Debug, Default)]
95pub struct MultipartForm {
96 parts: Vec<Part>,
97 boundary: Option<String>,
98}
99
100impl MultipartForm {
101 pub fn new() -> Self {
103 Self::default()
104 }
105
106 pub fn part(mut self, part: Part) -> Self {
108 self.parts.push(part);
109 self
110 }
111
112 pub fn text(self, name: impl Into<String>, value: impl Into<String>) -> Self {
114 self.part(Part::text(name, value))
115 }
116
117 pub fn file(
119 self,
120 name: impl Into<String>,
121 filename: impl Into<String>,
122 content_type: Mime,
123 data: impl Into<Bytes>,
124 ) -> Self {
125 self.part(
126 Part::bytes(name, data)
127 .filename(filename)
128 .content_type(content_type),
129 )
130 }
131
132 pub fn boundary(mut self, boundary: impl Into<String>) -> Self {
134 self.boundary = Some(boundary.into());
135 self
136 }
137
138 pub fn parts(&self) -> &[Part] {
140 &self.parts
141 }
142
143 pub fn into_parts(self) -> Vec<Part> {
146 self.parts
147 }
148
149 fn generate_boundary() -> String {
151 use std::time::{SystemTime, UNIX_EPOCH};
152 let timestamp = SystemTime::now()
153 .duration_since(UNIX_EPOCH)
154 .unwrap_or_default()
155 .as_nanos();
156 format!("----boundary{timestamp}")
157 }
158
159 fn get_boundary(&self) -> Cow<'_, str> {
161 match &self.boundary {
162 Some(b) => Cow::Borrowed(b),
163 None => Cow::Owned(Self::generate_boundary()),
164 }
165 }
166
167 pub fn encode(&self) -> (String, Bytes) {
172 let boundary = self.get_boundary();
173 let mut body = Vec::new();
174
175 for part in &self.parts {
176 body.extend_from_slice(b"--");
177 body.extend_from_slice(boundary.as_bytes());
178 body.extend_from_slice(b"\r\n");
179
180 body.extend_from_slice(b"Content-Disposition: form-data; name=\"");
181 body.extend_from_slice(part.name.as_bytes());
182 body.extend_from_slice(b"\"");
183
184 if let Some(filename) = &part.filename {
185 body.extend_from_slice(b"; filename=\"");
186 body.extend_from_slice(filename.as_bytes());
187 body.extend_from_slice(b"\"");
188 }
189 body.extend_from_slice(b"\r\n");
190
191 if let Some(content_type) = &part.content_type {
192 body.extend_from_slice(b"Content-Type: ");
193 body.extend_from_slice(content_type.as_ref().as_bytes());
194 body.extend_from_slice(b"\r\n");
195 }
196
197 body.extend_from_slice(b"\r\n");
198
199 match &part.content {
200 PartContent::Text(text) => body.extend_from_slice(text.as_bytes()),
201 PartContent::Binary(bytes) => body.extend_from_slice(bytes),
202 }
203
204 body.extend_from_slice(b"\r\n");
205 }
206
207 body.extend_from_slice(b"--");
208 body.extend_from_slice(boundary.as_bytes());
209 body.extend_from_slice(b"--\r\n");
210
211 (boundary.into_owned(), Bytes::from(body))
212 }
213}
214
215#[cfg(test)]
216mod tests;