From b62e1711acc654ee1eb60eadb953adc6d4005749 Mon Sep 17 00:00:00 2001 From: Ferris Tseng Date: Sun, 19 Nov 2017 16:17:00 -0500 Subject: initial implementation of multipart --- ipfs-api/Cargo.toml | 1 + ipfs-api/src/client.rs | 2 + ipfs-api/src/form.rs | 246 +++++++++++++++++++++++++++++++++++++++++++++++++ ipfs-api/src/lib.rs | 2 + 4 files changed, 251 insertions(+) create mode 100644 ipfs-api/src/form.rs (limited to 'ipfs-api') diff --git a/ipfs-api/Cargo.toml b/ipfs-api/Cargo.toml index 2bb7a7e..9c85c7a 100644 --- a/ipfs-api/Cargo.toml +++ b/ipfs-api/Cargo.toml @@ -9,6 +9,7 @@ bytes = "0.4" error-chain = "0.11" futures = "0.1" hyper = "0.11" +rand = "0.3" serde = "1.0" serde_derive = "1.0" serde_json = "1.0" diff --git a/ipfs-api/src/client.rs b/ipfs-api/src/client.rs index 9640a54..221a1d2 100644 --- a/ipfs-api/src/client.rs +++ b/ipfs-api/src/client.rs @@ -6,6 +6,7 @@ // copied, modified, or distributed except according to those terms. // +use form::Form; use futures::{stream, Stream}; use futures::future::{Future, IntoFuture}; use header::Trailer; @@ -223,6 +224,7 @@ impl IpfsClient { for<'de> Res: 'static + Deserialize<'de>, R: 'static + Read + Send, { + let form = Form::default(); let res = self.build_base_request(req) .map(|req| self.send_request_json(req)) .into_future() diff --git a/ipfs-api/src/form.rs b/ipfs-api/src/form.rs new file mode 100644 index 0000000..6bf0442 --- /dev/null +++ b/ipfs-api/src/form.rs @@ -0,0 +1,246 @@ +// Copyright 2017 rust-ipfs-api Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +// + +use hyper::header::{ContentDisposition, ContentType, DispositionParam, DispositionType}; +use hyper::mime::{self, Mime}; +use rand::{self, Rng}; +use std::borrow::{Borrow, Cow}; +use std::fs::File; +use std::io::{self, Read}; +use std::iter::FromIterator; +use std::path::Path; +use std::str::FromStr; + + +/// Random boundary string provider. +/// +pub trait BoundaryGenerator { + /// Generates a String to use as a boundary. + /// + fn generate_boundary() -> String; +} + + +struct RandomAsciiGenerator; + +impl BoundaryGenerator for RandomAsciiGenerator { + /// Creates a boundary of 6 ascii characters. + /// + fn generate_boundary() -> String { + let mut rng = rand::weak_rng(); + let ascii = rng.gen_ascii_chars(); + + String::from_iter(ascii.take(6)) + } +} + + +/// Implements the multipart/form-data media type as described by +/// RFC 7578. +/// +/// [See](https://tools.ietf.org/html/rfc7578#section-1). +/// +pub struct Form { + parts: Vec, + + /// The auto-generated boundary as described by 4.1. + /// + /// [See](https://tools.ietf.org/html/rfc7578#section-4.1). + /// + boundary: String, +} + +impl Default for Form { + /// Creates a new form with the default boundary generator. + /// + #[inline] + fn default() -> Form { + Form::new::() + } +} + +impl Form { + /// Creates a new form with the specified boundary generator function. + /// + #[inline] + pub fn new() -> Form + where + G: BoundaryGenerator, + { + Form { + parts: vec![], + boundary: G::generate_boundary(), + } + } + + /// Implements section 4.1. + /// + /// [See](https://tools.ietf.org/html/rfc7578#section-4.1). + /// + fn write_boundary(&self) -> io::Result<()> { + Ok(()) + } + + /// Adds a file, and attempts to derive the mime type. + /// + #[inline] + pub fn add_file(&mut self, name: F, path: P) -> io::Result<()> + where + P: AsRef, + F: Into, + { + self.add_file_with_mime(name, path, None) + } + + /// Adds a file with the specified mime type to the form. + /// If the mime type isn't specified, a mime type will try to + /// be derived. + /// + fn add_file_with_mime(&mut self, name: F, path: P, mime: Option) -> io::Result<()> + where + P: AsRef, + F: Into, + { + let f = File::open(&path)?; + let mime = if let Some(ext) = path.as_ref().extension() { + Mime::from_str(ext.to_string_lossy().borrow()).ok() + } else { + mime + }; + let len = match f.metadata() { + // If the path is not a file, it can't be uploaded because there + // is no content. + // + Ok(ref meta) if !meta.is_file() => Err(io::Error::new( + io::ErrorKind::InvalidInput, + "expected a file not directory", + )), + + // If there is some metadata on the file, try to derive some + // header values. + // + Ok(ref meta) => Ok(meta.len()), + + // The file metadata could not be accessed. This MIGHT not be an + // error, if the file could be opened. + // + Err(e) => Err(e), + }?; + + let read = Box::new(f); + + self.parts.push(Part::new( + Inner::Read(read, len), + name, + mime, + Some(path.as_ref().as_os_str().to_string_lossy()), + )); + + Ok(()) + } +} + + +pub struct Part { + /// Each part can include a Content-Type header field. If this + /// is not specified, it defaults to "text/plain", or + /// "application/octet-stream" for file data. + /// + /// [See](https://tools.ietf.org/html/rfc7578#section-4.4) + /// + content_type: ContentType, + + /// Each part must contain a Content-Disposition header field. + /// + /// [See](https://tools.ietf.org/html/rfc7578#section-4.2). + /// + content_disposition: ContentDisposition, +} + +impl Part { + /// Internal method to build a new Part instance. Sets the disposition type, + /// content-type, and the disposition parameters for name, and optionally + /// for filename. + /// + /// Per [4.3](https://tools.ietf.org/html/rfc7578#section-4.3), if multiple + /// files need to be specified for one form field, they can all be specified + /// with the same name parameter. + /// + fn new(inner: Inner, name: N, mime: Option, filename: Option) -> Part + where + N: Into, + F: Into, + { + // `name` disposition parameter is required. It should correspond to the + // name of a form field. + // + // [See 4.2](https://tools.ietf.org/html/rfc7578#section-4.2) + // + let mut disposition_params = vec![DispositionParam::Ext("name".into(), name.into())]; + + // `filename` can be supplied for files, but is totally optional. + // + // [See 4.2](https://tools.ietf.org/html/rfc7578#section-4.2) + // + if let Some(filename) = filename { + disposition_params.push(DispositionParam::Ext("filename".into(), filename.into())); + } + + Part { + content_type: ContentType(mime.unwrap_or_else(|| inner.default_content_type())), + content_disposition: ContentDisposition { + disposition: DispositionType::Ext("form-data".into()), + parameters: disposition_params, + }, + } + } +} + + +enum Inner { + /// The `Read` variant captures multiple cases. + /// + /// * The first is it supports uploading a file, which is explicitly + /// described in RFC 7578. + /// + /// * The second (which is not described by RFC 7578), is it can handle + /// arbitrary input streams (for example, a server response). + /// Any arbitrary input stream is automatically considered a file, + /// and assigned the corresponding content type if not explicitly + /// specified. + /// + Read(Box, u64), + + /// The `String` variant handles "text/plain" form data payloads. + /// + String(Cow<'static, str>), +} + +impl Inner { + /// Returns the default Content-Type header value as described in section 4.4. + /// + /// [See](https://tools.ietf.org/html/rfc7578#section-4.4) + /// + #[inline] + fn default_content_type(&self) -> Mime { + match self { + &Inner::Read(_, _) => mime::TEXT_PLAIN, + &Inner::String(_) => mime::APPLICATION_OCTET_STREAM, + } + } + + /// Returns the length of the inner type. + /// + #[inline] + fn len(&self) -> u64 { + match self { + &Inner::Read(_, len) => len, + &Inner::String(ref s) => s.len() as u64, + } + } +} diff --git a/ipfs-api/src/lib.rs b/ipfs-api/src/lib.rs index ad8aee7..7c0c9f1 100644 --- a/ipfs-api/src/lib.rs +++ b/ipfs-api/src/lib.rs @@ -11,6 +11,7 @@ extern crate bytes; extern crate error_chain; extern crate futures; extern crate hyper; +extern crate rand; extern crate serde; #[macro_use] extern crate serde_derive; @@ -25,5 +26,6 @@ pub use request::{KeyType, Logger, LoggingLevel}; mod request; pub mod response; mod client; +mod form; mod header; mod read; -- cgit v1.2.3