Skip to content

Commit

Permalink
multipart & json feature (#47)
Browse files Browse the repository at this point in the history
  • Loading branch information
JustFrederik authored Feb 1, 2024
1 parent 809f330 commit 3913c2c
Show file tree
Hide file tree
Showing 5 changed files with 313 additions and 2 deletions.
56 changes: 56 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 20 additions & 2 deletions ehttp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,34 @@ include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
all-features = true

[features]
"default" = []
default = []

## Support `fetch_async` on native
"native-async" = ["async-channel"]
native-async = ["async-channel"]

## Support streaming fetch
streaming = ["dep:wasm-streams", "dep:futures-util"]

## Support json fetch
json = ["dep:serde", "dep:serde_json"]

## Support multipart fetch
multipart = ["dep:getrandom", "dep:mime", "dep:mime_guess", "dep:rand" ]

[lib]

[dependencies]
document-features = "0.2"

# Multipart request
mime = { version = "0.3", optional = true }
mime_guess = { version = "2.0", optional = true }
rand = { version = "0.8.5", optional = true }

# Json request
serde = { version = "1.0", optional = true }
serde_json = { version = "1.0", optional = true }

# For compiling natively:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
# ureq = { version = "2.0", default-features = false, features = ["gzip", "tls_native_certs"] }
Expand All @@ -42,6 +57,9 @@ js-sys = "0.3"
wasm-bindgen = "0.2.89"
wasm-bindgen-futures = "0.4"

# Multipart request
getrandom = { version = "0.2.10", features = ["js"], optional = true }

# Streaming response
futures-util = { version = "0.3", optional = true }
wasm-streams = { version = "0.4", optional = true }
Expand Down
3 changes: 3 additions & 0 deletions ehttp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ pub use web::spawn_future;
#[cfg(feature = "streaming")]
pub mod streaming;

#[cfg(feature = "multipart")]
pub mod multipart;

#[deprecated = "Use ehttp::Headers::new"]
pub fn headers(headers: &[(&str, &str)]) -> Headers {
Headers::new(headers)
Expand Down
180 changes: 180 additions & 0 deletions ehttp/src/multipart.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
//! Multipart HTTP request for both native and WASM.
//!
//! Requires the `multipart` feature to be enabled.
//!
//! Example:
//! ```
//! use std::io::Cursor;
//! use ehttp::multipart::MultipartBuilder;
//! let url = "https://www.example.com";
//! let request = ehttp::Request::multipart(
//! url,
//! MultipartBuilder::new()
//! .add_text("label", "lorem ipsum")
//! .add_stream(
//! &mut Cursor::new(vec![0, 0, 0, 0]),
//! "4_empty_bytes",
//! Some("4_empty_bytes.png"),
//! None,
//! )
//! .unwrap(),
//! );
//! ehttp::fetch(request, |result| {});
//! ```
//! Taken from ureq_multipart 1.1.1
//!
use mime::Mime;
use rand::Rng;

use std::fs::File;

Check warning on line 30 in ehttp/src/multipart.rs

View workflow job for this annotation

GitHub Actions / cargo check web --all-features

unused import: `std::fs::File`
use std::io::{self, Read, Write};
use std::path::Path;

const BOUNDARY_LEN: usize = 29;

fn opt_filename(path: &Path) -> Option<&str> {

Check warning on line 36 in ehttp/src/multipart.rs

View workflow job for this annotation

GitHub Actions / cargo check web --all-features

function `opt_filename` is never used
path.file_name().and_then(|filename| filename.to_str())
}

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()
}

fn mime_filename(path: &Path) -> (Mime, Option<&str>) {

Check warning on line 48 in ehttp/src/multipart.rs

View workflow job for this annotation

GitHub Actions / cargo check web --all-features

function `mime_filename` is never used
let content_type = mime_guess::from_path(path);
let filename = opt_filename(path);
(content_type.first_or_octet_stream(), filename)
}

#[derive(Debug)]
/// The Builder for the multipart
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 {
/// creates a new MultipartBuilder with empty inner
pub fn new() -> Self {
Self {
boundary: random_alphanumeric(BOUNDARY_LEN),
inner: Vec::new(),
data_written: false,
}
}

/// add text field
///
/// * name field name
/// * text field text value
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
}

/// add file
///
/// * name file field name
/// * path the sending file path
#[cfg(not(target_arch = "wasm32"))]
pub fn add_file<P: AsRef<Path>>(self, name: &str, path: P) -> io::Result<Self> {
let path = path.as_ref();
let (content_type, filename) = mime_filename(path);
let mut file = File::open(path)?;
self.add_stream(&mut file, name, filename, Some(content_type))
}

/// add some stream
pub fn add_stream<S: Read>(
mut self,
stream: &mut S,
name: &str,
filename: Option<&str>,
content_type: Option<Mime>,
) -> io::Result<Self> {
// This is necessary to make sure it is interpreted as a file on the server end.
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();
}

/// general multipart data
///
/// # Return
/// * (content_type,post_data)
/// * content_type http header content type
/// * post_data ureq.req.send_send_bytes(&post_data)
///
pub fn finish(mut self) -> (String, Vec<u8>) {
if self.data_written {
self.inner.write_all(b"\r\n").unwrap();
}

// always write the closing boundary, even for empty bodies
write!(
self.inner,
"-----------------------------{}--\r\n",
self.boundary
)
.unwrap();
(
format!(
"multipart/form-data; boundary=---------------------------{}",
self.boundary
),
self.inner,
)
}
}
Loading

0 comments on commit 3913c2c

Please sign in to comment.