1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-09-01 01:16:59 +02:00

port hyper header

This commit is contained in:
Nikolay Kim
2018-03-06 00:43:25 -08:00
parent e182ed33b1
commit 32b5544ad9
36 changed files with 3639 additions and 166 deletions

164
src/fs.rs
View File

@@ -4,15 +4,21 @@
use std::io;
use std::io::Read;
use std::fmt::Write;
use std::fs::{File, DirEntry};
use std::fs::{File, DirEntry, Metadata};
use std::path::{Path, PathBuf};
use std::ops::{Deref, DerefMut};
use std::time::{SystemTime, UNIX_EPOCH};
use http::{header, Method};
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
use http::{Method, StatusCode};
use mime_guess::get_mime_type;
use header;
use param::FromParam;
use handler::{Handler, Responder};
use httpmessage::HttpMessage;
use httprequest::HttpRequest;
use httpresponse::HttpResponse;
use httpcodes::{HttpOk, HttpFound, HttpMethodNotAllowed};
@@ -20,7 +26,12 @@ use httpcodes::{HttpOk, HttpFound, HttpMethodNotAllowed};
/// A file with an associated name; responds with the Content-Type based on the
/// file extension.
#[derive(Debug)]
pub struct NamedFile(PathBuf, File);
pub struct NamedFile {
path: PathBuf,
file: File,
md: Metadata,
modified: Option<SystemTime>,
}
impl NamedFile {
/// Attempts to open a file in read-only mode.
@@ -30,18 +41,20 @@ impl NamedFile {
/// ```rust
/// use actix_web::fs::NamedFile;
///
/// # #[allow(unused_variables)]
/// let file = NamedFile::open("foo.txt");
/// ```
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
let file = File::open(path.as_ref())?;
Ok(NamedFile(path.as_ref().to_path_buf(), file))
let md = file.metadata()?;
let path = path.as_ref().to_path_buf();
let modified = md.modified().ok();
Ok(NamedFile{path, file, md, modified})
}
/// Returns reference to the underlying `File` object.
#[inline]
pub fn file(&self) -> &File {
&self.1
&self.file
}
/// Retrieve the path of this file.
@@ -52,7 +65,6 @@ impl NamedFile {
/// # use std::io;
/// use actix_web::fs::NamedFile;
///
/// # #[allow(dead_code)]
/// # fn path() -> io::Result<()> {
/// let file = NamedFile::open("test.txt")?;
/// assert_eq!(file.path().as_os_str(), "foo.txt");
@@ -61,7 +73,30 @@ impl NamedFile {
/// ```
#[inline]
pub fn path(&self) -> &Path {
self.0.as_path()
self.path.as_path()
}
fn etag(&self) -> Option<header::EntityTag> {
// This etag format is similar to Apache's.
self.modified.as_ref().map(|mtime| {
let ino = {
#[cfg(unix)]
{ self.md.ino() }
#[cfg(not(unix))]
{ 0 }
};
let dur = mtime.duration_since(UNIX_EPOCH)
.expect("modification time must be after epoch");
header::EntityTag::strong(
format!("{:x}:{:x}:{:x}:{:x}",
ino, self.md.len(), dur.as_secs(),
dur.subsec_nanos()))
})
}
fn last_modified(&self) -> Option<header::HttpDate> {
self.modified.map(|mtime| mtime.into())
}
}
@@ -69,35 +104,112 @@ impl Deref for NamedFile {
type Target = File;
fn deref(&self) -> &File {
&self.1
&self.file
}
}
impl DerefMut for NamedFile {
fn deref_mut(&mut self) -> &mut File {
&mut self.1
&mut self.file
}
}
/// Returns true if `req` has no `If-Match` header or one which matches `etag`.
fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
match req.get_header::<header::IfMatch>() {
Err(_) | Ok(header::IfMatch::Any) => true,
Ok(header::IfMatch::Items(ref items)) => {
if let Some(some_etag) = etag {
for item in items {
if item.strong_eq(some_etag) {
return true;
}
}
}
false
}
}
}
/// Returns true if `req` doesn't have an `If-None-Match` header matching `req`.
fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
match req.get_header::<header::IfNoneMatch>() {
Ok(header::IfNoneMatch::Any) => false,
Ok(header::IfNoneMatch::Items(ref items)) => {
if let Some(some_etag) = etag {
for item in items {
if item.weak_eq(some_etag) {
return false;
}
}
}
true
}
Err(_) => true,
}
}
impl Responder for NamedFile {
type Item = HttpResponse;
type Error = io::Error;
fn respond_to(mut self, req: HttpRequest) -> Result<HttpResponse, io::Error> {
if *req.method() != Method::GET && *req.method() != Method::HEAD {
Ok(HttpMethodNotAllowed.build()
.header(header::CONTENT_TYPE, "text/plain")
.header(header::ALLOW, "GET, HEAD")
.body("This resource only supports GET and HEAD.").unwrap())
return Ok(HttpMethodNotAllowed.build()
.header(header::http::CONTENT_TYPE, "text/plain")
.header(header::http::ALLOW, "GET, HEAD")
.body("This resource only supports GET and HEAD.").unwrap())
}
let etag = self.etag();
let last_modified = self.last_modified();
// check preconditions
let precondition_failed = if !any_match(etag.as_ref(), &req) {
true
} else if let (Some(ref m), Ok(header::IfUnmodifiedSince(ref since))) =
(last_modified, req.get_header())
{
m > since
} else {
let mut resp = HttpOk.build();
if let Some(ext) = self.path().extension() {
let mime = get_mime_type(&ext.to_string_lossy());
resp.content_type(format!("{}", mime).as_str());
}
false
};
// check last modified
let not_modified = if !none_match(etag.as_ref(), &req) {
true
} else if let (Some(ref m), Ok(header::IfModifiedSince(ref since))) =
(last_modified, req.get_header())
{
m <= since
} else {
false
};
let mut resp = HttpOk.build();
resp
.if_some(self.path().extension(), |ext, resp| {
resp.set(header::ContentType(get_mime_type(&ext.to_string_lossy())));
})
.if_some(last_modified, |lm, resp| {resp.set(header::LastModified(lm));})
.if_some(etag, |etag, resp| {resp.set(header::ETag(etag));});
if precondition_failed {
return Ok(resp.status(StatusCode::PRECONDITION_FAILED).finish().unwrap())
} else if not_modified {
return Ok(resp.status(StatusCode::NOT_MODIFIED).finish().unwrap())
}
resp.content_length(self.md.len());
if *req.method() == Method::GET {
let mut data = Vec::new();
let _ = self.1.read_to_end(&mut data);
let _ = self.file.read_to_end(&mut data);
Ok(resp.body(data).unwrap())
} else {
Ok(resp.finish().unwrap())
}
}
}
@@ -314,7 +426,8 @@ impl<S> Handler<S> for StaticFiles {
#[cfg(test)]
mod tests {
use super::*;
use http::{header, StatusCode};
use test::TestRequest;
use http::{header, Method, StatusCode};
#[test]
fn test_named_file() {
@@ -328,6 +441,15 @@ mod tests {
assert_eq!(resp.headers().get(header::CONTENT_TYPE).unwrap(), "text/x-toml")
}
#[test]
fn test_named_file_not_allowed() {
let req = TestRequest::default().method(Method::POST).finish();
let file = NamedFile::open("Cargo.toml").unwrap();
let resp = file.respond_to(req).unwrap();
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
}
#[test]
fn test_static_files() {
let mut st = StaticFiles::new(".", true);