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:
164
src/fs.rs
164
src/fs.rs
@@ -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);
|
||||
|
Reference in New Issue
Block a user