mirror of
https://github.com/actix/actix-web.git
synced 2024-11-26 03:21:08 +00:00
refactor and enable some tests for staticfiles
This commit is contained in:
parent
1a80b70868
commit
6efc3438b8
18 changed files with 2198 additions and 2063 deletions
|
@ -35,7 +35,7 @@ before_script:
|
|||
|
||||
script:
|
||||
- cargo clean
|
||||
- cargo test -- --nocapture
|
||||
- cargo test --all -- --nocapture
|
||||
|
||||
# Upload docs
|
||||
after_success:
|
||||
|
@ -49,7 +49,7 @@ after_success:
|
|||
fi
|
||||
- |
|
||||
if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-03-02" ]]; then
|
||||
cargo tarpaulin --out Xml
|
||||
cargo tarpaulin --out Xml --all
|
||||
bash <(curl -s https://codecov.io/bash)
|
||||
echo "Uploaded code coverage"
|
||||
fi
|
||||
|
|
|
@ -28,7 +28,7 @@ path = "src/lib.rs"
|
|||
members = [
|
||||
".",
|
||||
"actix-session",
|
||||
"staticfiles",
|
||||
"actix-staticfiles",
|
||||
]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
|
|
|
@ -20,7 +20,8 @@ path = "src/lib.rs"
|
|||
[dependencies]
|
||||
actix-web = { path=".." }
|
||||
actix-http = { git = "https://github.com/actix/actix-http.git" }
|
||||
actix-service = "0.3.0"
|
||||
actix-service = { git = "https://github.com/actix/actix-net.git" }
|
||||
#actix-service = "0.3.0"
|
||||
|
||||
bytes = "0.4"
|
||||
futures = "0.1"
|
70
actix-staticfiles/src/config.rs
Normal file
70
actix-staticfiles/src/config.rs
Normal file
|
@ -0,0 +1,70 @@
|
|||
use actix_http::http::header::DispositionType;
|
||||
use actix_web::http::Method;
|
||||
use mime;
|
||||
|
||||
/// Describes `StaticFiles` configiration
|
||||
///
|
||||
/// To configure actix's static resources you need
|
||||
/// to define own configiration type and implement any method
|
||||
/// you wish to customize.
|
||||
/// As trait implements reasonable defaults for Actix.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// extern crate mime;
|
||||
/// extern crate actix_web;
|
||||
/// use actix_web::http::header::DispositionType;
|
||||
/// use actix_web::fs::{StaticFileConfig, NamedFile};
|
||||
///
|
||||
/// #[derive(Default)]
|
||||
/// struct MyConfig;
|
||||
///
|
||||
/// impl StaticFileConfig for MyConfig {
|
||||
/// fn content_disposition_map(typ: mime::Name) -> DispositionType {
|
||||
/// DispositionType::Attachment
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let file = NamedFile::open_with_config("foo.txt", MyConfig);
|
||||
/// ```
|
||||
pub trait StaticFileConfig: Default {
|
||||
///Describes mapping for mime type to content disposition header
|
||||
///
|
||||
///By default `IMAGE`, `TEXT` and `VIDEO` are mapped to Inline.
|
||||
///Others are mapped to Attachment
|
||||
fn content_disposition_map(typ: mime::Name) -> DispositionType {
|
||||
match typ {
|
||||
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
|
||||
_ => DispositionType::Attachment,
|
||||
}
|
||||
}
|
||||
|
||||
///Describes whether Actix should attempt to calculate `ETag`
|
||||
///
|
||||
///Defaults to `true`
|
||||
fn is_use_etag() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
///Describes whether Actix should use last modified date of file.
|
||||
///
|
||||
///Defaults to `true`
|
||||
fn is_use_last_modifier() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
///Describes allowed methods to access static resources.
|
||||
///
|
||||
///By default all methods are allowed
|
||||
fn is_method_allowed(_method: &Method) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
///Default content disposition as described in
|
||||
///[StaticFileConfig](trait.StaticFileConfig.html)
|
||||
#[derive(Default)]
|
||||
pub struct DefaultConfig;
|
||||
|
||||
impl StaticFileConfig for DefaultConfig {}
|
41
actix-staticfiles/src/error.rs
Normal file
41
actix-staticfiles/src/error.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
use actix_web::{http::StatusCode, HttpResponse, ResponseError};
|
||||
use derive_more::Display;
|
||||
|
||||
/// Errors which can occur when serving static files.
|
||||
#[derive(Display, Debug, PartialEq)]
|
||||
pub enum StaticFilesError {
|
||||
/// Path is not a directory
|
||||
#[display(fmt = "Path is not a directory. Unable to serve static files")]
|
||||
IsNotDirectory,
|
||||
|
||||
/// Cannot render directory
|
||||
#[display(fmt = "Unable to render directory without index file")]
|
||||
IsDirectory,
|
||||
}
|
||||
|
||||
/// Return `NotFound` for `StaticFilesError`
|
||||
impl ResponseError for StaticFilesError {
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::new(StatusCode::NOT_FOUND)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Display, Debug, PartialEq)]
|
||||
pub enum UriSegmentError {
|
||||
/// The segment started with the wrapped invalid character.
|
||||
#[display(fmt = "The segment started with the wrapped invalid character")]
|
||||
BadStart(char),
|
||||
/// The segment contained the wrapped invalid character.
|
||||
#[display(fmt = "The segment contained the wrapped invalid character")]
|
||||
BadChar(char),
|
||||
/// The segment ended with the wrapped invalid character.
|
||||
#[display(fmt = "The segment ended with the wrapped invalid character")]
|
||||
BadEnd(char),
|
||||
}
|
||||
|
||||
/// Return `BadRequest` for `UriSegmentError`
|
||||
impl ResponseError for UriSegmentError {
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::new(StatusCode::BAD_REQUEST)
|
||||
}
|
||||
}
|
1482
actix-staticfiles/src/lib.rs
Normal file
1482
actix-staticfiles/src/lib.rs
Normal file
File diff suppressed because it is too large
Load diff
438
actix-staticfiles/src/named.rs
Normal file
438
actix-staticfiles/src/named.rs
Normal file
|
@ -0,0 +1,438 @@
|
|||
use std::fs::{File, Metadata};
|
||||
use std::io;
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
use mime;
|
||||
use mime_guess::guess_mime_type;
|
||||
|
||||
use actix_http::error::Error;
|
||||
use actix_http::http::header::{self, ContentDisposition, DispositionParam};
|
||||
use actix_web::http::{ContentEncoding, Method, StatusCode};
|
||||
use actix_web::{HttpMessage, HttpRequest, HttpResponse, Responder};
|
||||
|
||||
use crate::config::{DefaultConfig, StaticFileConfig};
|
||||
use crate::{ChunkedReadFile, HttpRange};
|
||||
|
||||
/// A file with an associated name.
|
||||
#[derive(Debug)]
|
||||
pub struct NamedFile<C = DefaultConfig> {
|
||||
path: PathBuf,
|
||||
file: File,
|
||||
pub(crate) content_type: mime::Mime,
|
||||
pub(crate) content_disposition: header::ContentDisposition,
|
||||
pub(crate) md: Metadata,
|
||||
modified: Option<SystemTime>,
|
||||
encoding: Option<ContentEncoding>,
|
||||
pub(crate) status_code: StatusCode,
|
||||
_cd_map: PhantomData<C>,
|
||||
}
|
||||
|
||||
impl NamedFile {
|
||||
/// Creates an instance from a previously opened file.
|
||||
///
|
||||
/// The given `path` need not exist and is only used to determine the `ContentType` and
|
||||
/// `ContentDisposition` headers.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// extern crate actix_web;
|
||||
///
|
||||
/// use actix_web::fs::NamedFile;
|
||||
/// use std::io::{self, Write};
|
||||
/// use std::env;
|
||||
/// use std::fs::File;
|
||||
///
|
||||
/// fn main() -> io::Result<()> {
|
||||
/// let mut file = File::create("foo.txt")?;
|
||||
/// file.write_all(b"Hello, world!")?;
|
||||
/// let named_file = NamedFile::from_file(file, "bar.txt")?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub fn from_file<P: AsRef<Path>>(file: File, path: P) -> io::Result<NamedFile> {
|
||||
Self::from_file_with_config(file, path, DefaultConfig)
|
||||
}
|
||||
|
||||
/// Attempts to open a file in read-only mode.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use actix_web::fs::NamedFile;
|
||||
///
|
||||
/// let file = NamedFile::open("foo.txt");
|
||||
/// ```
|
||||
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
|
||||
Self::open_with_config(path, DefaultConfig)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: StaticFileConfig> NamedFile<C> {
|
||||
/// Creates an instance from a previously opened file using the provided configuration.
|
||||
///
|
||||
/// The given `path` need not exist and is only used to determine the `ContentType` and
|
||||
/// `ContentDisposition` headers.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// extern crate actix_web;
|
||||
///
|
||||
/// use actix_web::fs::{DefaultConfig, NamedFile};
|
||||
/// use std::io::{self, Write};
|
||||
/// use std::env;
|
||||
/// use std::fs::File;
|
||||
///
|
||||
/// fn main() -> io::Result<()> {
|
||||
/// let mut file = File::create("foo.txt")?;
|
||||
/// file.write_all(b"Hello, world!")?;
|
||||
/// let named_file = NamedFile::from_file_with_config(file, "bar.txt", DefaultConfig)?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub fn from_file_with_config<P: AsRef<Path>>(
|
||||
file: File,
|
||||
path: P,
|
||||
_: C,
|
||||
) -> io::Result<NamedFile<C>> {
|
||||
let path = path.as_ref().to_path_buf();
|
||||
|
||||
// Get the name of the file and use it to construct default Content-Type
|
||||
// and Content-Disposition values
|
||||
let (content_type, content_disposition) = {
|
||||
let filename = match path.file_name() {
|
||||
Some(name) => name.to_string_lossy(),
|
||||
None => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Provided path has no filename",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let ct = guess_mime_type(&path);
|
||||
let disposition_type = C::content_disposition_map(ct.type_());
|
||||
let cd = ContentDisposition {
|
||||
disposition: disposition_type,
|
||||
parameters: vec![DispositionParam::Filename(filename.into_owned())],
|
||||
};
|
||||
(ct, cd)
|
||||
};
|
||||
|
||||
let md = file.metadata()?;
|
||||
let modified = md.modified().ok();
|
||||
let encoding = None;
|
||||
Ok(NamedFile {
|
||||
path,
|
||||
file,
|
||||
content_type,
|
||||
content_disposition,
|
||||
md,
|
||||
modified,
|
||||
encoding,
|
||||
status_code: StatusCode::OK,
|
||||
_cd_map: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
/// Attempts to open a file in read-only mode using provided configuration.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use actix_web::fs::{DefaultConfig, NamedFile};
|
||||
///
|
||||
/// let file = NamedFile::open_with_config("foo.txt", DefaultConfig);
|
||||
/// ```
|
||||
pub fn open_with_config<P: AsRef<Path>>(
|
||||
path: P,
|
||||
config: C,
|
||||
) -> io::Result<NamedFile<C>> {
|
||||
Self::from_file_with_config(File::open(&path)?, path, config)
|
||||
}
|
||||
|
||||
/// Returns reference to the underlying `File` object.
|
||||
#[inline]
|
||||
pub fn file(&self) -> &File {
|
||||
&self.file
|
||||
}
|
||||
|
||||
/// Retrieve the path of this file.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// # use std::io;
|
||||
/// use actix_web::fs::NamedFile;
|
||||
///
|
||||
/// # fn path() -> io::Result<()> {
|
||||
/// let file = NamedFile::open("test.txt")?;
|
||||
/// assert_eq!(file.path().as_os_str(), "foo.txt");
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn path(&self) -> &Path {
|
||||
self.path.as_path()
|
||||
}
|
||||
|
||||
/// Set response **Status Code**
|
||||
pub fn set_status_code(mut self, status: StatusCode) -> Self {
|
||||
self.status_code = status;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the MIME Content-Type for serving this file. By default
|
||||
/// the Content-Type is inferred from the filename extension.
|
||||
#[inline]
|
||||
pub fn set_content_type(mut self, mime_type: mime::Mime) -> Self {
|
||||
self.content_type = mime_type;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the Content-Disposition for serving this file. This allows
|
||||
/// changing the inline/attachment disposition as well as the filename
|
||||
/// sent to the peer. By default the disposition is `inline` for text,
|
||||
/// image, and video content types, and `attachment` otherwise, and
|
||||
/// the filename is taken from the path provided in the `open` method
|
||||
/// after converting it to UTF-8 using
|
||||
/// [to_string_lossy](https://doc.rust-lang.org/std/ffi/struct.OsStr.html#method.to_string_lossy).
|
||||
#[inline]
|
||||
pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self {
|
||||
self.content_disposition = cd;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set content encoding for serving this file
|
||||
#[inline]
|
||||
pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self {
|
||||
self.encoding = Some(enc);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) 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()
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn last_modified(&self) -> Option<header::HttpDate> {
|
||||
self.modified.map(|mtime| mtime.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Deref for NamedFile<C> {
|
||||
type Target = File;
|
||||
|
||||
fn deref(&self) -> &File {
|
||||
&self.file
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> DerefMut for NamedFile<C> {
|
||||
fn deref_mut(&mut self) -> &mut File {
|
||||
&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>() {
|
||||
None | Some(header::IfMatch::Any) => true,
|
||||
Some(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>() {
|
||||
Some(header::IfNoneMatch::Any) => false,
|
||||
Some(header::IfNoneMatch::Items(ref items)) => {
|
||||
if let Some(some_etag) = etag {
|
||||
for item in items {
|
||||
if item.weak_eq(some_etag) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: StaticFileConfig> Responder for NamedFile<C> {
|
||||
type Error = Error;
|
||||
type Future = Result<HttpResponse, Error>;
|
||||
|
||||
fn respond_to(self, req: &HttpRequest) -> Self::Future {
|
||||
if self.status_code != StatusCode::OK {
|
||||
let mut resp = HttpResponse::build(self.status_code);
|
||||
resp.set(header::ContentType(self.content_type.clone()))
|
||||
.header(
|
||||
header::CONTENT_DISPOSITION,
|
||||
self.content_disposition.to_string(),
|
||||
);
|
||||
// TODO blocking by compressing
|
||||
// if let Some(current_encoding) = self.encoding {
|
||||
// resp.content_encoding(current_encoding);
|
||||
// }
|
||||
let reader = ChunkedReadFile {
|
||||
size: self.md.len(),
|
||||
offset: 0,
|
||||
file: Some(self.file),
|
||||
fut: None,
|
||||
counter: 0,
|
||||
};
|
||||
return Ok(resp.streaming(reader));
|
||||
}
|
||||
|
||||
if !C::is_method_allowed(req.method()) {
|
||||
return Ok(HttpResponse::MethodNotAllowed()
|
||||
.header(header::CONTENT_TYPE, "text/plain")
|
||||
.header(header::ALLOW, "GET, HEAD")
|
||||
.body("This resource only supports GET and HEAD."));
|
||||
}
|
||||
|
||||
let etag = if C::is_use_etag() { self.etag() } else { None };
|
||||
let last_modified = if C::is_use_last_modifier() {
|
||||
self.last_modified()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// check preconditions
|
||||
let precondition_failed = if !any_match(etag.as_ref(), req) {
|
||||
true
|
||||
} else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) =
|
||||
(last_modified, req.get_header())
|
||||
{
|
||||
m > since
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// check last modified
|
||||
let not_modified = if !none_match(etag.as_ref(), req) {
|
||||
true
|
||||
} else if req.headers().contains_key(header::IF_NONE_MATCH) {
|
||||
false
|
||||
} else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) =
|
||||
(last_modified, req.get_header())
|
||||
{
|
||||
m <= since
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let mut resp = HttpResponse::build(self.status_code);
|
||||
resp.set(header::ContentType(self.content_type.clone()))
|
||||
.header(
|
||||
header::CONTENT_DISPOSITION,
|
||||
self.content_disposition.to_string(),
|
||||
);
|
||||
// TODO blocking by compressing
|
||||
// if let Some(current_encoding) = self.encoding {
|
||||
// resp.content_encoding(current_encoding);
|
||||
// }
|
||||
|
||||
resp.if_some(last_modified, |lm, resp| {
|
||||
resp.set(header::LastModified(lm));
|
||||
})
|
||||
.if_some(etag, |etag, resp| {
|
||||
resp.set(header::ETag(etag));
|
||||
});
|
||||
|
||||
resp.header(header::ACCEPT_RANGES, "bytes");
|
||||
|
||||
let mut length = self.md.len();
|
||||
let mut offset = 0;
|
||||
|
||||
// check for range header
|
||||
if let Some(ranges) = req.headers().get(header::RANGE) {
|
||||
if let Ok(rangesheader) = ranges.to_str() {
|
||||
if let Ok(rangesvec) = HttpRange::parse(rangesheader, length) {
|
||||
length = rangesvec[0].length;
|
||||
offset = rangesvec[0].start;
|
||||
// TODO blocking by compressing
|
||||
// resp.content_encoding(ContentEncoding::Identity);
|
||||
resp.header(
|
||||
header::CONTENT_RANGE,
|
||||
format!(
|
||||
"bytes {}-{}/{}",
|
||||
offset,
|
||||
offset + length - 1,
|
||||
self.md.len()
|
||||
),
|
||||
);
|
||||
} else {
|
||||
resp.header(header::CONTENT_RANGE, format!("bytes */{}", length));
|
||||
return Ok(resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish());
|
||||
};
|
||||
} else {
|
||||
return Ok(resp.status(StatusCode::BAD_REQUEST).finish());
|
||||
};
|
||||
};
|
||||
|
||||
resp.header(header::CONTENT_LENGTH, format!("{}", length));
|
||||
|
||||
if precondition_failed {
|
||||
return Ok(resp.status(StatusCode::PRECONDITION_FAILED).finish());
|
||||
} else if not_modified {
|
||||
return Ok(resp.status(StatusCode::NOT_MODIFIED).finish());
|
||||
}
|
||||
|
||||
if *req.method() == Method::HEAD {
|
||||
Ok(resp.finish())
|
||||
} else {
|
||||
let reader = ChunkedReadFile {
|
||||
offset,
|
||||
size: length,
|
||||
file: Some(self.file),
|
||||
fut: None,
|
||||
counter: 0,
|
||||
};
|
||||
if offset != 0 || length != self.md.len() {
|
||||
return Ok(resp.status(StatusCode::PARTIAL_CONTENT).streaming(reader));
|
||||
};
|
||||
Ok(resp.streaming(reader))
|
||||
}
|
||||
}
|
||||
}
|
1
actix-staticfiles/tests/test space.binary
Normal file
1
actix-staticfiles/tests/test space.binary
Normal file
|
@ -0,0 +1 @@
|
|||
ÂTÇ‘É‚Vù2þvI ª–\ÇRË™–ˆæeÞ<04>vDØ:è—½¬RVÖYpíÿ;ÍÏGñùp!2÷CŒ.–<0C>û®õpA!ûߦÙx j+Uc÷±©X”c%Û;ï"yìAI
|
1
actix-staticfiles/tests/test.binary
Normal file
1
actix-staticfiles/tests/test.binary
Normal file
|
@ -0,0 +1 @@
|
|||
ÂTÇ‘É‚Vù2þvI ª–\ÇRË™–ˆæeÞ<04>vDØ:è—½¬RVÖYpíÿ;ÍÏGñùp!2÷CŒ.–<0C>û®õpA!ûߦÙx j+Uc÷±©X”c%Û;ï"yìAI
|
BIN
actix-staticfiles/tests/test.png
Normal file
BIN
actix-staticfiles/tests/test.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 168 B |
|
@ -2,7 +2,6 @@ use futures::IntoFuture;
|
|||
|
||||
use actix_web::{
|
||||
http::Method, middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer,
|
||||
Resource, Route,
|
||||
};
|
||||
|
||||
fn index(req: HttpRequest) -> &'static str {
|
||||
|
@ -29,19 +28,17 @@ fn main() -> std::io::Result<()> {
|
|||
.middleware(middleware::DefaultHeaders::new().header("X-Version", "0.2"))
|
||||
.middleware(middleware::Compress::default())
|
||||
.resource("/resource1/index.html", |r| r.route(web::get().to(index)))
|
||||
.service(
|
||||
"/resource2/index.html",
|
||||
Resource::new()
|
||||
.middleware(
|
||||
middleware::DefaultHeaders::new().header("X-Version-R2", "0.3"),
|
||||
)
|
||||
.default_resource(|r| {
|
||||
r.route(Route::new().to(|| HttpResponse::MethodNotAllowed()))
|
||||
})
|
||||
.route(web::method(Method::GET).to_async(index_async)),
|
||||
)
|
||||
.service("/test1.html", Resource::new().to(|| "Test\r\n"))
|
||||
.service("/", Resource::new().to(no_params))
|
||||
.resource("/resource2/index.html", |r| {
|
||||
r.middleware(
|
||||
middleware::DefaultHeaders::new().header("X-Version-R2", "0.3"),
|
||||
)
|
||||
.default_resource(|r| {
|
||||
r.route(web::route().to(|| HttpResponse::MethodNotAllowed()))
|
||||
})
|
||||
.route(web::method(Method::GET).to_async(index_async))
|
||||
})
|
||||
.resource("/test1.html", |r| r.to(|| "Test\r\n"))
|
||||
.resource("/", |r| r.to(no_params))
|
||||
})
|
||||
.bind("127.0.0.1:8080")?
|
||||
.workers(1)
|
||||
|
|
45
src/app.rs
45
src/app.rs
|
@ -24,8 +24,13 @@ type HttpService<P> = BoxedService<ServiceRequest<P>, ServiceResponse, ()>;
|
|||
type HttpNewService<P> = BoxedNewService<(), ServiceRequest<P>, ServiceResponse, (), ()>;
|
||||
type BoxedResponse = Box<Future<Item = ServiceResponse, Error = ()>>;
|
||||
|
||||
pub trait HttpServiceFactory<Request> {
|
||||
type Factory: NewService<Request>;
|
||||
pub trait HttpServiceFactory<P> {
|
||||
type Factory: NewService<
|
||||
ServiceRequest<P>,
|
||||
Response = ServiceResponse,
|
||||
Error = (),
|
||||
InitError = (),
|
||||
>;
|
||||
|
||||
fn rdef(&self) -> &ResourceDef;
|
||||
|
||||
|
@ -293,6 +298,29 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// Register resource handler service.
|
||||
pub fn service<F>(self, service: F) -> AppRouter<T, P, Body, AppEntry<P>>
|
||||
where
|
||||
F: HttpServiceFactory<P> + 'static,
|
||||
{
|
||||
let fref = Rc::new(RefCell::new(None));
|
||||
AppRouter {
|
||||
chain: self.chain,
|
||||
services: vec![(
|
||||
service.rdef().clone(),
|
||||
boxed::new_service(service.create().map_init_err(|_| ())),
|
||||
None,
|
||||
)],
|
||||
default: None,
|
||||
defaults: vec![],
|
||||
endpoint: AppEntry::new(fref.clone()),
|
||||
factory_ref: fref,
|
||||
extensions: self.extensions,
|
||||
state: self.state,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set server host name.
|
||||
///
|
||||
/// Host name is used by application router aa a hostname for url
|
||||
|
@ -445,16 +473,15 @@ where
|
|||
}
|
||||
|
||||
/// Register resource handler service.
|
||||
pub fn service<R, F, U>(mut self, rdef: R, factory: F) -> Self
|
||||
pub fn service<F>(mut self, factory: F) -> Self
|
||||
where
|
||||
R: Into<ResourceDef>,
|
||||
F: IntoNewService<U, ServiceRequest<P>>,
|
||||
U: NewService<ServiceRequest<P>, Response = ServiceResponse, Error = ()>
|
||||
+ 'static,
|
||||
F: HttpServiceFactory<P> + 'static,
|
||||
{
|
||||
let rdef = factory.rdef().clone();
|
||||
|
||||
self.services.push((
|
||||
rdef.into(),
|
||||
boxed::new_service(factory.into_new_service().map_init_err(|_| ())),
|
||||
rdef,
|
||||
boxed::new_service(factory.create().map_init_err(|_| ())),
|
||||
None,
|
||||
));
|
||||
self
|
||||
|
|
24
src/lib.rs
24
src/lib.rs
|
@ -19,9 +19,9 @@ pub mod test;
|
|||
|
||||
// re-export for convenience
|
||||
pub use actix_http::Response as HttpResponse;
|
||||
pub use actix_http::{error, http, Error, HttpMessage, ResponseError, Result};
|
||||
pub use actix_http::{body, error, http, Error, HttpMessage, ResponseError, Result};
|
||||
|
||||
pub use crate::app::{App, AppRouter};
|
||||
pub use crate::app::App;
|
||||
pub use crate::extract::{FromRequest, Json};
|
||||
pub use crate::request::HttpRequest;
|
||||
pub use crate::resource::Resource;
|
||||
|
@ -32,6 +32,26 @@ pub use crate::server::HttpServer;
|
|||
pub use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse};
|
||||
pub use crate::state::State;
|
||||
|
||||
pub mod dev {
|
||||
//! The `actix-web` prelude for library developers
|
||||
//!
|
||||
//! The purpose of this module is to alleviate imports of many common actix
|
||||
//! traits by adding a glob import to the top of actix heavy modules:
|
||||
//!
|
||||
//! ```
|
||||
//! # #![allow(unused_imports)]
|
||||
//! use actix_web::dev::*;
|
||||
//! ```
|
||||
|
||||
pub use crate::app::{AppRouter, HttpServiceFactory};
|
||||
pub use actix_http::body::{Body, MessageBody, ResponseBody};
|
||||
pub use actix_http::dev::ResponseBuilder as HttpResponseBuilder;
|
||||
pub use actix_http::{
|
||||
Extensions, Payload, PayloadStream, RequestHead, ResponseHead,
|
||||
};
|
||||
pub use actix_router::{Path, ResourceDef, Url};
|
||||
}
|
||||
|
||||
pub mod web {
|
||||
use actix_http::{http::Method, Error, Response};
|
||||
use futures::IntoFuture;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
use std::borrow::Cow;
|
||||
use std::cell::{Ref, RefMut};
|
||||
use std::fmt;
|
||||
use std::rc::Rc;
|
||||
|
||||
use actix_http::body::{Body, ResponseBody};
|
||||
use actix_http::body::{Body, MessageBody, ResponseBody};
|
||||
use actix_http::http::{HeaderMap, Method, Uri, Version};
|
||||
use actix_http::{
|
||||
Error, Extensions, HttpMessage, Payload, PayloadStream, Request, RequestHead,
|
||||
|
@ -123,6 +124,11 @@ impl<P> ServiceRequest<P> {
|
|||
pub fn app_extensions(&self) -> &Extensions {
|
||||
self.req.app_extensions()
|
||||
}
|
||||
|
||||
/// Deconstruct request into parts
|
||||
pub fn into_parts(self) -> (HttpRequest, Payload<P>) {
|
||||
(self.req, self.payload)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> Resource<Url> for ServiceRequest<P> {
|
||||
|
@ -172,6 +178,29 @@ impl<P> std::ops::DerefMut for ServiceRequest<P> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<P> fmt::Debug for ServiceRequest<P> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
writeln!(
|
||||
f,
|
||||
"\nServiceRequest {:?} {}:{}",
|
||||
self.head().version,
|
||||
self.head().method,
|
||||
self.path()
|
||||
)?;
|
||||
if !self.query_string().is_empty() {
|
||||
writeln!(f, " query: ?{:?}", self.query_string())?;
|
||||
}
|
||||
if !self.match_info().is_empty() {
|
||||
writeln!(f, " params: {:?}", self.match_info())?;
|
||||
}
|
||||
writeln!(f, " headers:")?;
|
||||
for (key, val) in self.headers().iter() {
|
||||
writeln!(f, " {:?}: {:?}", key, val)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ServiceFromRequest<P> {
|
||||
req: HttpRequest,
|
||||
payload: Payload<P>,
|
||||
|
@ -259,6 +288,16 @@ impl<B> ServiceResponse<B> {
|
|||
ServiceResponse { request, response }
|
||||
}
|
||||
|
||||
/// Create service response from the error
|
||||
pub fn from_err<E: Into<Error>>(err: E, request: HttpRequest) -> Self {
|
||||
let e: Error = err.into();
|
||||
let res: Response = e.into();
|
||||
ServiceResponse {
|
||||
request,
|
||||
response: res.into_body(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get reference to original request
|
||||
#[inline]
|
||||
pub fn request(&self) -> &HttpRequest {
|
||||
|
@ -303,6 +342,11 @@ impl<B> ServiceResponse<B> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract response body
|
||||
pub fn take_body(&mut self) -> ResponseBody<B> {
|
||||
self.response.take_body()
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> ServiceResponse<B> {
|
||||
|
@ -349,3 +393,21 @@ impl<B> IntoFuture for ServiceResponse<B> {
|
|||
ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: MessageBody> fmt::Debug for ServiceResponse<B> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let res = writeln!(
|
||||
f,
|
||||
"\nServiceResponse {:?} {}{}",
|
||||
self.response.head().version,
|
||||
self.response.head().status,
|
||||
self.response.head().reason.unwrap_or(""),
|
||||
);
|
||||
let _ = writeln!(f, " headers:");
|
||||
for (key, val) in self.response.head().headers.iter() {
|
||||
let _ = writeln!(f, " {:?}: {:?}", key, val);
|
||||
}
|
||||
let _ = writeln!(f, " body: {:?}", self.response.body().length());
|
||||
res
|
||||
}
|
||||
}
|
||||
|
|
28
src/test.rs
28
src/test.rs
|
@ -70,6 +70,34 @@ where
|
|||
block_on(app.into_new_service().new_service(&())).unwrap()
|
||||
}
|
||||
|
||||
/// Calls service and waits for response future completion.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use actix_web::{test, App, HttpResponse, http::StatusCode};
|
||||
/// use actix_service::Service;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let mut app = test::init_service(
|
||||
/// App::new()
|
||||
/// .resource("/test", |r| r.to(|| HttpResponse::Ok()))
|
||||
/// );
|
||||
///
|
||||
/// // Create request object
|
||||
/// let req = test::TestRequest::with_uri("/test").to_request();
|
||||
///
|
||||
/// // Call application
|
||||
/// let resp = test::call_succ_service(&mut app, req);
|
||||
/// assert_eq!(resp.status(), StatusCode::OK);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn call_success<S, R, B, E>(app: &mut S, req: R) -> S::Response
|
||||
where
|
||||
S: Service<R, Response = ServiceResponse<B>, Error = E>,
|
||||
E: std::fmt::Debug,
|
||||
{
|
||||
block_on(app.call(req)).unwrap()
|
||||
}
|
||||
|
||||
/// Test `Request` builder.
|
||||
///
|
||||
/// For unit testing, actix provides a request builder type and a simple handler runner. TestRequest implements a builder-like pattern.
|
||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue