use mime::Mime;
use rand::Rng;
use std::io::{self, Read, Write};
const BOUNDARY_LEN: usize = 29;
fn random_alphanumeric(len: usize) -> String {
rand::thread_rng()
.sample_iter(&rand::distributions::Uniform::from(0..=9))
.take(len)
.map(|num| num.to_string())
.collect()
}
#[derive(Debug)]
pub struct MultipartBuilder {
boundary: String,
inner: Vec<u8>,
data_written: bool,
}
impl Default for MultipartBuilder {
fn default() -> Self {
Self::new()
}
}
#[allow(dead_code)]
impl MultipartBuilder {
pub fn new() -> Self {
Self {
boundary: random_alphanumeric(BOUNDARY_LEN),
inner: Vec::new(),
data_written: false,
}
}
pub fn add_text(mut self, name: &str, text: &str) -> Self {
self.write_field_headers(name, None, None);
self.inner.extend(text.as_bytes());
self
}
#[cfg(not(target_arch = "wasm32"))]
pub fn add_file<P: AsRef<std::path::Path>>(self, name: &str, path: P) -> io::Result<Self> {
fn mime_filename(path: &std::path::Path) -> (Mime, Option<&str>) {
let content_type = mime_guess::from_path(path);
let filename = path.file_name().and_then(|filename| filename.to_str());
(content_type.first_or_octet_stream(), filename)
}
let path = path.as_ref();
let (content_type, filename) = mime_filename(path);
let mut file = std::fs::File::open(path)?;
self.add_stream(&mut file, name, filename, Some(content_type))
}
pub fn add_stream<S: Read>(
mut self,
stream: &mut S,
name: &str,
filename: Option<&str>,
content_type: Option<Mime>,
) -> io::Result<Self> {
let content_type = Some(content_type.unwrap_or(mime::APPLICATION_OCTET_STREAM));
self.write_field_headers(name, filename, content_type);
io::copy(stream, &mut self.inner)?;
Ok(self)
}
fn write_boundary(&mut self) {
if self.data_written {
self.inner.write_all(b"\r\n").unwrap();
}
write!(
self.inner,
"-----------------------------{}\r\n",
self.boundary
)
.unwrap()
}
fn write_field_headers(
&mut self,
name: &str,
filename: Option<&str>,
content_type: Option<Mime>,
) {
self.write_boundary();
if !self.data_written {
self.data_written = true;
}
write!(
self.inner,
"Content-Disposition: form-data; name=\"{name}\""
)
.unwrap();
if let Some(filename) = filename {
write!(self.inner, "; filename=\"{filename}\"").unwrap();
}
if let Some(content_type) = content_type {
write!(self.inner, "\r\nContent-Type: {content_type}").unwrap();
}
self.inner.write_all(b"\r\n\r\n").unwrap();
}
pub fn finish(mut self) -> (String, Vec<u8>) {
if self.data_written {
self.inner.write_all(b"\r\n").unwrap();
}
write!(
self.inner,
"-----------------------------{}--\r\n",
self.boundary
)
.unwrap();
(
format!(
"multipart/form-data; boundary=---------------------------{}",
self.boundary
),
self.inner,
)
}
}