Add mime sniffing to Body::from_file

This commit is contained in:
Yoshua Wuyts 2020-05-16 13:18:17 +02:00
parent 9e1808a46b
commit 568251d18f
8 changed files with 180 additions and 4 deletions

View File

@ -42,3 +42,4 @@ serde_urlencoded = "0.6.1"
[dev-dependencies]
http = "0.2.0"
async-std = { version = "1.4.0", features = ["unstable", "attributes"] }

View File

@ -358,12 +358,26 @@ impl Body {
/// # Ok(()) }) }
/// ```
#[cfg(feature = "async_std")]
pub async fn from_file<P>(file: P) -> io::Result<Self>
pub async fn from_file<P>(path: P) -> io::Result<Self>
where
P: AsRef<Path>,
{
let file = fs::read(file.as_ref()).await?;
Ok(file.into())
let path = path.as_ref();
let mut file = fs::File::open(path).await?;
let len = file.metadata().await?.len();
// Look at magic bytes first, look at extension second, fall back to
// octet stream.
let mime = peek_mime(&mut file)
.await?
.or_else(|| guess_ext(path))
.unwrap_or(mime::BYTE_STREAM);
Ok(Self {
mime,
length: Some(len as usize),
reader: Box::new(io::BufReader::new(file)),
})
}
/// Get the length of the body in bytes.
@ -448,3 +462,30 @@ impl BufRead for Body {
Pin::new(&mut self.reader).consume(amt)
}
}
/// Look at first few bytes of a file to determine the mime type.
/// This is used for various binary formats such as images and videos.
#[cfg(feature = "async_std")]
async fn peek_mime(file: &mut async_std::fs::File) -> io::Result<Option<Mime>> {
let mut buf = [0_u8; 8];
file.read_exact(&mut buf).await?;
let mime = Mime::sniff(&buf).ok();
file.seek(io::SeekFrom::Start(0)).await?;
Ok(mime)
}
/// Look at the extension of a file to determine the mime type.
/// This is useful for plain-text formats such as HTML and CSS.
#[cfg(feature = "async_std")]
fn guess_ext(path: &Path) -> Option<Mime> {
let ext = path.extension().map(|p| p.to_str()).flatten();
match ext {
Some("html") => Some(mime::HTML),
Some("js") | Some("mjs") => Some(mime::JAVASCRIPT),
Some("json") => Some(mime::JSON),
Some("css") => Some(mime::CSS),
Some("svg") => Some(mime::SVG),
Some("wasm") => Some(mime::WASM),
None | Some(_) => None,
}
}

View File

@ -86,6 +86,77 @@ pub const HTML: Mime = Mime {
static_subtype: Some("html"),
};
/// Content-Type for SVG.
///
/// # Mime Type
///
/// ```txt
/// image/svg+xml
/// ```
pub const SVG: Mime = Mime {
static_essence: Some("image/svg+xml"),
essence: String::new(),
basetype: String::new(),
subtype: String::new(),
params: None,
static_basetype: Some("image"),
static_subtype: Some("svg+xml"),
};
/// Content-Type for ICO icons.
///
/// # Mime Type
///
/// ```txt
/// image/x-icon
/// ```
// There are multiple `.ico` mime types known, but `image/x-icon`
// is what most browser use. See:
// https://en.wikipedia.org/wiki/ICO_%28file_format%29#MIME_type
pub const ICO: Mime = Mime {
static_essence: Some("image/x-icon"),
essence: String::new(),
basetype: String::new(),
subtype: String::new(),
params: None,
static_basetype: Some("image"),
static_subtype: Some("x-icon"),
};
/// Content-Type for PNG images.
///
/// # Mime Type
///
/// ```txt
/// image/png
/// ```
pub const PNG: Mime = Mime {
static_essence: Some("image/png"),
essence: String::new(),
basetype: String::new(),
subtype: String::new(),
params: None,
static_basetype: Some("image"),
static_subtype: Some("png"),
};
/// Content-Type for JPEG images.
///
/// # Mime Type
///
/// ```txt
/// image/jpeg
/// ```
pub const JPEG: Mime = Mime {
static_essence: Some("image/jpeg"),
essence: String::new(),
basetype: String::new(),
subtype: String::new(),
params: None,
static_basetype: Some("image"),
static_subtype: Some("jpeg"),
};
/// Content-Type for Server Sent Events
///
/// # Mime Type
@ -170,3 +241,20 @@ pub const MULTIPART_FORM: Mime = Mime {
static_subtype: Some("form-data"),
params: None,
};
/// Content-Type for webassembly.
///
/// # Mime Type
///
/// ```txt
/// application/wasm
/// ```
pub const WASM: Mime = Mime {
static_essence: Some("application/wasm"),
essence: String::new(),
basetype: String::new(),
subtype: String::new(),
static_basetype: Some("application"),
static_subtype: Some("wasm"),
params: None,
};

View File

@ -98,6 +98,20 @@ impl Mime {
}
}
impl PartialEq<Mime> for Mime {
fn eq(&self, other: &Mime) -> bool {
let left = match self.static_essence {
Some(essence) => essence,
None => &self.essence,
};
let right = match other.static_essence {
Some(essence) => essence,
None => &other.essence,
};
left == right
}
}
impl Display for Mime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
parse::format(self, f)
@ -188,7 +202,7 @@ impl PartialEq<str> for ParamValue {
/// This is a hack that allows us to mark a trait as utf8 during compilation. We
/// can remove this once we can construct HashMap during compilation.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ParamKind {
Utf8,
Vec(Vec<(ParamName, ParamValue)>),

View File

@ -367,6 +367,11 @@ impl Response {
}
}
/// Get the current content type
pub fn content_type(&self) -> Option<Mime> {
self.header(CONTENT_TYPE)?.last().as_str().parse().ok()
}
/// Get the length of the body stream, if it has been set.
///
/// This value is set when passing a fixed-size object into as the body. E.g. a string, or a

7
tests/fixtures/index.html vendored Normal file
View File

@ -0,0 +1,7 @@
<html>
<head></head>
<body>This is a fixture!</body>
</html>

BIN
tests/fixtures/nori.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

20
tests/mime.rs Normal file
View File

@ -0,0 +1,20 @@
use http_types::{Response, Body, mime};
use async_std::io;
#[async_std::test]
async fn guess_plain_text_mime() -> io::Result<()> {
let body = Body::from_file("tests/fixtures/index.html").await?;
let mut res = Response::new(200);
res.set_body(body);
assert_eq!(res.content_type(), Some(mime::HTML));
Ok(())
}
#[async_std::test]
async fn guess_binary_mime() -> io::Result<()> {
let body = Body::from_file("tests/fixtures/nori.png").await?;
let mut res = Response::new(200);
res.set_body(body);
assert_eq!(res.content_type(), Some(mime::PNG));
Ok(())
}