diff options
author | Ferris Tseng <ferristseng@fastmail.fm> | 2017-11-24 13:53:29 -0500 |
---|---|---|
committer | Ferris Tseng <ferristseng@fastmail.fm> | 2017-11-24 13:53:29 -0500 |
commit | 8afe22320bee8c28d820dafc5fe25ae4343e95a4 (patch) | |
tree | e226953b6a83a6d6f21019644693399134fce9a6 | |
parent | 5d08948eb496e7e98add3845b0d9e9c529334421 (diff) |
move multipart to crate
-rw-r--r-- | ipfs-api/Cargo.toml | 32 | ||||
-rw-r--r-- | ipfs-api/examples/server.rs | 58 | ||||
-rw-r--r-- | ipfs-api/src/client.rs | 4 | ||||
-rw-r--r-- | ipfs-api/src/lib.rs | 3 | ||||
-rw-r--r-- | ipfs-api/src/multipart.rs | 441 |
5 files changed, 19 insertions, 519 deletions
diff --git a/ipfs-api/Cargo.toml b/ipfs-api/Cargo.toml index 9c85c7a..53fc690 100644 --- a/ipfs-api/Cargo.toml +++ b/ipfs-api/Cargo.toml @@ -1,21 +1,21 @@ [package] -name = "ipfs-api" -version = "0.4.0" -license = "MIT OR Apache-2.0" -authors = ["Ferris Tseng <ferristseng@fastmail.fm>"] +name = "ipfs-api" +version = "0.4.0" +license = "MIT OR Apache-2.0" +authors = ["Ferris Tseng <ferristseng@fastmail.fm>"] [dependencies] -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" -serde_urlencoded = "0.5" -tokio-core = "0.1" -tokio-io = "0.1" +bytes = "0.4" +error-chain = "0.11" +futures = "0.1" +hyper = "0.11" +hyper-multipart-rfc7578 = { git = "https://github.com/ferristseng/rust-hyper-multipart-rfc7578" } +serde = "1.0" +serde_derive = "1.0" +serde_json = "1.0" +serde_urlencoded = "0.5" +tokio-core = "0.1" +tokio-io = "0.1" [dev-dependencies] -tokio-timer = "0.1" +tokio-timer = "0.1" diff --git a/ipfs-api/examples/server.rs b/ipfs-api/examples/server.rs deleted file mode 100644 index bc02c90..0000000 --- a/ipfs-api/examples/server.rs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2017 rust-ipfs-api Developers -// -// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or -// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or -// http://opensource.org/licenses/MIT>, at your option. This file may not be -// copied, modified, or distributed except according to those terms. -// - -extern crate futures; -extern crate hyper; - -use futures::future::Future; -use futures::stream::Stream; -use hyper::StatusCode; -use hyper::server::{Http, Service, Request, Response}; - -struct Debug; - -impl Service for Debug { - type Request = Request; - - type Response = Response; - - type Error = hyper::Error; - - type Future = Box<Future<Item = Response, Error = hyper::Error>>; - - - fn call(&self, req: Request) -> Self::Future { - println!("{:?}", req); - - let res = req.body().concat2().map(|bod| { - println!("{}", String::from_utf8_lossy(&bod)); - - Response::new().with_status(StatusCode::Ok) - }); - - Box::new(res) - } -} - - -/// This example runs a server on the default Ipfs port. All it does is -/// print requests as it gets them. It is useful for debugging. -/// -fn main() { - let addr = "127.0.0.1:5001".parse().unwrap(); - let mut server = Http::new().bind(&addr, || Ok(Debug)).unwrap(); - - server.no_proto(); - - println!( - "Listening on http://{} with 1 thread.", - server.local_addr().unwrap() - ); - - server.run().unwrap(); -} diff --git a/ipfs-api/src/client.rs b/ipfs-api/src/client.rs index a0c8ec1..04e1f99 100644 --- a/ipfs-api/src/client.rs +++ b/ipfs-api/src/client.rs @@ -9,12 +9,12 @@ use futures::{stream, Stream}; use futures::future::{Future, IntoFuture}; use header::Trailer; -use multipart; use read::{JsonLineDecoder, LineDecoder, StreamReader}; use request::{self, ApiRequest}; use response::{self, Error, ErrorKind}; use hyper::{self, Chunk, Request, Response, Uri, Method, StatusCode}; use hyper::client::{Client, Config, HttpConnector}; +use hyper_multipart::client::multipart; use serde::{Deserialize, Serialize}; use serde_json; use std::io::Read; @@ -92,7 +92,7 @@ impl IpfsClient { .map(|url| { let mut req = Request::new(Method::Get, url); - if let Some(mut form) = form { + if let Some(form) = form { form.set_body(&mut req); } diff --git a/ipfs-api/src/lib.rs b/ipfs-api/src/lib.rs index f693965..e2770ac 100644 --- a/ipfs-api/src/lib.rs +++ b/ipfs-api/src/lib.rs @@ -11,7 +11,7 @@ extern crate bytes; extern crate error_chain; extern crate futures; extern crate hyper; -extern crate rand; +extern crate hyper_multipart_rfc7578 as hyper_multipart; extern crate serde; #[macro_use] extern crate serde_derive; @@ -27,5 +27,4 @@ mod request; pub mod response; mod client; mod header; -mod multipart; mod read; diff --git a/ipfs-api/src/multipart.rs b/ipfs-api/src/multipart.rs deleted file mode 100644 index 36b0c5a..0000000 --- a/ipfs-api/src/multipart.rs +++ /dev/null @@ -1,441 +0,0 @@ -// Copyright 2017 rust-ipfs-api Developers -// -// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or -// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or -// http://opensource.org/licenses/MIT>, at your option. This file may not be -// copied, modified, or distributed except according to those terms. -// - -use bytes::{BufMut, BytesMut}; -use futures::{Poll, Async}; -use futures::stream::Stream; -use hyper::{self, Request}; -use hyper::header::{ContentDisposition, ContentType, DispositionParam, DispositionType, Header}; -use hyper::mime::{self, Mime}; -use rand::{self, Rng}; -use std::borrow::Borrow; -use std::fmt::Display; -use std::fs::File; -use std::io::{self, Cursor, Read, Write}; -use std::iter::{FromIterator, Peekable}; -use std::path::Path; -use std::str::FromStr; -use std::vec::IntoIter; - - -/// Converts a hyper Header into a String. -/// -fn header_to_string<H>(header: &H) -> String -where - H: Header + Display, -{ - format!("{}: {}", H::header_name(), header) -} - - -/// Writes a CLRF. -/// -fn write_crlf<W>(write: &mut W) -> io::Result<()> -where - W: Write, -{ - write.write_all(&[b'\r', b'\n']) -} - - -/// Multipart body that is compatible with Hyper. -/// -pub struct Body { - /// The amount of data to write with each chunk. - /// - buf_size: usize, - - /// The active reader. - /// - current: Option<Box<'static + Read + Send>>, - - /// The parts as an iterator. When the iterator stops - /// yielding, the body is fully written. - /// - parts: Peekable<IntoIter<Part>>, - - /// The multipart boundary. - /// - boundary: String, -} - -impl Body { - /// Implements section 4.1. - /// - /// [See](https://tools.ietf.org/html/rfc7578#section-4.1). - /// - fn write_boundary<W>(&self, write: &mut W) -> io::Result<()> - where - W: Write, - { - write_crlf(write)?; - write.write_all(&[b'-', b'-'])?; - write.write_all(self.boundary.as_bytes()) - } - - fn write_final_boundary<W>(&self, write: &mut W) -> io::Result<()> - where - W: Write, - { - self.write_boundary(write)?; - write.write_all(&[b'-', b'-']) - } - - /// Writes the Content-Disposition, and Content-Type headers. - /// - fn write_headers<W>(&self, write: &mut W, part: &Part) -> io::Result<()> - where - W: Write, - { - write_crlf(write)?; - write.write_all( - header_to_string(&part.content_type).as_bytes(), - )?; - write_crlf(write)?; - write.write_all( - header_to_string(&part.content_disposition).as_bytes(), - )?; - write_crlf(write)?; - write_crlf(write) - } -} - -impl Stream for Body { - type Item = BytesMut; - - type Error = hyper::Error; - - fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> { - let bytes = BytesMut::with_capacity(self.buf_size); - let mut writer = bytes.writer(); - - if self.current.is_none() { - if let Some(part) = self.parts.next() { - self.write_boundary(&mut writer)?; - self.write_headers(&mut writer, &part)?; - - let read = match part.inner { - Inner::Read(read, _) => read, - Inner::Text(s) => Box::new(Cursor::new(s.into_bytes())), - }; - - self.current = Some(read); - } else { - // No current part, and no parts left means there is nothing - // left to write. - // - return Ok(Async::Ready(None)); - } - } - - let num = if let Some(ref mut read) = self.current { - let mut buf = writer.get_mut(); - unsafe { - let num = read.read(&mut buf.bytes_mut())?; - - buf.advance_mut(num); - - num - } - } else { - 0 - }; - - if num == 0 { - // Wrote 0 bytes from the reader, so we reached the EOF for the - // current item. - // - self.current = None; - - // Peek to check if there are are any parts not yet written. - // If there is nothing, the final boundary can be written. - // - if self.parts.peek().is_none() { - self.write_final_boundary(&mut writer)?; - - Ok(Async::Ready(Some(writer.into_inner()))) - } else { - self.poll() - } - } else { - Ok(Async::Ready(Some(writer.into_inner()))) - } - } -} - - -/// 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<Part>, - - /// 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::<RandomAsciiGenerator>() - } -} - -impl Form { - /// Creates a new form with the specified boundary generator function. - /// - #[inline] - pub fn new<G>() -> Form - where - G: BoundaryGenerator, - { - Form { - parts: vec![], - boundary: G::generate_boundary(), - } - } - - /// Updates a request instance with the multipart Content-Type header - /// and the payload data. - /// - pub fn set_body(self, req: &mut Request<Body>) { - let header = format!("multipart/form-data; boundary=\"{}\"", &self.boundary); - - { - let headers = req.headers_mut(); - - headers.set(ContentType(Mime::from_str(&header).expect( - "multipart mime type should parse", - ))); - } - - req.set_body(self); - } - - /// Adds a struct that implements Read. - /// - pub fn add_reader<F, R>(&mut self, name: F, read: R) - where - F: Into<String>, - R: 'static + Read + Send, - { - let read = Box::new(read); - - self.parts.push(Part::new::<_, String>( - Inner::Read(read, None), - name, - None, - None, - )); - } - - /// Adds a file, and attempts to derive the mime type. - /// - #[inline] - pub fn add_file<P, F>(&mut self, name: F, path: P) -> io::Result<()> - where - P: AsRef<Path>, - F: Into<String>, - { - 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<P, F>(&mut self, name: F, path: P, mime: Option<Mime>) -> io::Result<()> - where - P: AsRef<Path>, - F: Into<String>, - { - 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(Some(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(()) - } -} - -impl Into<Body> for Form { - #[inline] - fn into(self) -> Body { - Body { - buf_size: 2048, - current: None, - parts: self.parts.into_iter().peekable(), - boundary: self.boundary, - } - } -} - - -pub struct Part { - inner: Inner, - - /// 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<N, F>(inner: Inner, name: N, mime: Option<Mime>, filename: Option<F>) -> Part - where - N: Into<String>, - F: Into<String>, - { - // `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())); - } - - let content_type = ContentType(mime.unwrap_or_else(|| inner.default_content_type())); - - Part { - inner: inner, - content_type: 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<'static + Read + Send>, Option<u64>), - - /// The `String` variant handles "text/plain" form data payloads. - /// - Text(String), -} - -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::APPLICATION_OCTET_STREAM, - &Inner::Text(_) => mime::TEXT_PLAIN, - } - } - - /// Returns the length of the inner type. - /// - #[inline] - fn len(&self) -> Option<u64> { - match self { - &Inner::Read(_, len) => len, - &Inner::Text(ref s) => Some(s.len() as u64), - } - } -} - - -/// 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)) - } -} |