mirror of
https://github.com/fafhrd91/actix-web
synced 2025-06-25 06:39:22 +02:00
rename to Files
This commit is contained in:
5
actix-files/CHANGES.md
Normal file
5
actix-files/CHANGES.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Changes
|
||||
|
||||
## [0.1.0] - 2018-10-x
|
||||
|
||||
* Initial impl
|
44
actix-files/Cargo.toml
Normal file
44
actix-files/Cargo.toml
Normal file
@ -0,0 +1,44 @@
|
||||
[package]
|
||||
name = "actix-files"
|
||||
version = "0.1.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Static files support for Actix web."
|
||||
readme = "README.md"
|
||||
keywords = ["actix", "http", "async", "futures"]
|
||||
homepage = "https://actix.rs"
|
||||
repository = "https://github.com/actix/actix-web.git"
|
||||
documentation = "https://actix.rs/api/actix-web/stable/actix_web/"
|
||||
categories = ["asynchronous", "web-programming::http-server"]
|
||||
license = "MIT/Apache-2.0"
|
||||
edition = "2018"
|
||||
workspace = ".."
|
||||
|
||||
[lib]
|
||||
name = "actix_staticfiles"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
actix-web = { path=".." }
|
||||
actix-http = { git = "https://github.com/actix/actix-http.git" }
|
||||
actix-service = { git = "https://github.com/actix/actix-net.git" }
|
||||
#actix-service = "0.3.0"
|
||||
|
||||
bytes = "0.4"
|
||||
futures = "0.1"
|
||||
derive_more = "0.14"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
mime_guess = "2.0.0-alpha"
|
||||
percent-encoding = "1.0"
|
||||
v_htmlescape = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "0.1.0"
|
||||
#actix-server = { version="0.2", features=["ssl"] }
|
||||
actix-web = { path="..", features=["ssl"] }
|
||||
actix-server = { git = "https://github.com/actix/actix-net.git", features=["ssl"] }
|
||||
actix-http = { git = "https://github.com/actix/actix-http.git", features=["ssl"] }
|
||||
actix-http-test = { git = "https://github.com/actix/actix-http.git", features=["ssl"] }
|
||||
rand = "0.6"
|
||||
env_logger = "0.6"
|
||||
serde_derive = "1.0"
|
82
actix-files/README.md
Normal file
82
actix-files/README.md
Normal file
@ -0,0 +1,82 @@
|
||||
# Actix web [](https://travis-ci.org/actix/actix-web) [](https://ci.appveyor.com/project/fafhrd91/actix-web-hdy9d/branch/master) [](https://codecov.io/gh/actix/actix-web) [](https://crates.io/crates/actix-web) [](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
Actix web is a simple, pragmatic and extremely fast web framework for Rust.
|
||||
|
||||
* Supported *HTTP/1.x* and [*HTTP/2.0*](https://actix.rs/docs/http2/) protocols
|
||||
* Streaming and pipelining
|
||||
* Keep-alive and slow requests handling
|
||||
* Client/server [WebSockets](https://actix.rs/docs/websockets/) support
|
||||
* Transparent content compression/decompression (br, gzip, deflate)
|
||||
* Configurable [request routing](https://actix.rs/docs/url-dispatch/)
|
||||
* Multipart streams
|
||||
* Static assets
|
||||
* SSL support with OpenSSL or `native-tls`
|
||||
* Middlewares ([Logger, Session, CORS, CSRF, etc](https://actix.rs/docs/middleware/))
|
||||
* Includes an asynchronous [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html)
|
||||
* Built on top of [Actix actor framework](https://github.com/actix/actix)
|
||||
* Experimental [Async/Await](https://github.com/mehcode/actix-web-async-await) support.
|
||||
|
||||
## Documentation & community resources
|
||||
|
||||
* [User Guide](https://actix.rs/docs/)
|
||||
* [API Documentation (Development)](https://actix.rs/actix-web/actix_web/)
|
||||
* [API Documentation (Releases)](https://actix.rs/api/actix-web/stable/actix_web/)
|
||||
* [Chat on gitter](https://gitter.im/actix/actix)
|
||||
* Cargo package: [actix-web](https://crates.io/crates/actix-web)
|
||||
* Minimum supported Rust version: 1.31 or later
|
||||
|
||||
## Example
|
||||
|
||||
```rust
|
||||
extern crate actix_web;
|
||||
use actix_web::{http, server, App, Path, Responder};
|
||||
|
||||
fn index(info: Path<(u32, String)>) -> impl Responder {
|
||||
format!("Hello {}! id:{}", info.1, info.0)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
server::new(
|
||||
|| App::new()
|
||||
.route("/{id}/{name}/index.html", http::Method::GET, index))
|
||||
.bind("127.0.0.1:8080").unwrap()
|
||||
.run();
|
||||
}
|
||||
```
|
||||
|
||||
### More examples
|
||||
|
||||
* [Basics](https://github.com/actix/examples/tree/master/basics/)
|
||||
* [Stateful](https://github.com/actix/examples/tree/master/state/)
|
||||
* [Protobuf support](https://github.com/actix/examples/tree/master/protobuf/)
|
||||
* [Multipart streams](https://github.com/actix/examples/tree/master/multipart/)
|
||||
* [Simple websocket](https://github.com/actix/examples/tree/master/websocket/)
|
||||
* [Tera](https://github.com/actix/examples/tree/master/template_tera/) /
|
||||
[Askama](https://github.com/actix/examples/tree/master/template_askama/) templates
|
||||
* [Diesel integration](https://github.com/actix/examples/tree/master/diesel/)
|
||||
* [r2d2](https://github.com/actix/examples/tree/master/r2d2/)
|
||||
* [SSL / HTTP/2.0](https://github.com/actix/examples/tree/master/tls/)
|
||||
* [Tcp/Websocket chat](https://github.com/actix/examples/tree/master/websocket-chat/)
|
||||
* [Json](https://github.com/actix/examples/tree/master/json/)
|
||||
|
||||
You may consider checking out
|
||||
[this directory](https://github.com/actix/examples/tree/master/) for more examples.
|
||||
|
||||
## Benchmarks
|
||||
|
||||
* [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r16&hw=ph&test=plaintext)
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under either of
|
||||
|
||||
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0))
|
||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT))
|
||||
|
||||
at your option.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Contribution to the actix-web crate is organized under the terms of the
|
||||
Contributor Covenant, the maintainer of actix-web, @fafhrd91, promises to
|
||||
intervene to uphold that code of conduct.
|
70
actix-files/src/config.rs
Normal file
70
actix-files/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-files/src/error.rs
Normal file
41
actix-files/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 FilesError {
|
||||
/// 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 `FilesError`
|
||||
impl ResponseError for FilesError {
|
||||
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)
|
||||
}
|
||||
}
|
1097
actix-files/src/lib.rs
Normal file
1097
actix-files/src/lib.rs
Normal file
File diff suppressed because it is too large
Load Diff
441
actix-files/src/named.rs
Normal file
441
actix-files/src/named.rs
Normal file
@ -0,0 +1,441 @@
|
||||
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::range::HttpRange;
|
||||
use crate::ChunkedReadFile;
|
||||
|
||||
/// 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 {
|
||||
println!("RESP: {:?}", req);
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
375
actix-files/src/range.rs
Normal file
375
actix-files/src/range.rs
Normal file
@ -0,0 +1,375 @@
|
||||
/// HTTP Range header representation.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct HttpRange {
|
||||
pub start: u64,
|
||||
pub length: u64,
|
||||
}
|
||||
|
||||
static PREFIX: &'static str = "bytes=";
|
||||
const PREFIX_LEN: usize = 6;
|
||||
|
||||
impl HttpRange {
|
||||
/// Parses Range HTTP header string as per RFC 2616.
|
||||
///
|
||||
/// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`).
|
||||
/// `size` is full size of response (file).
|
||||
pub fn parse(header: &str, size: u64) -> Result<Vec<HttpRange>, ()> {
|
||||
if header.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if !header.starts_with(PREFIX) {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
let size_sig = size as i64;
|
||||
let mut no_overlap = false;
|
||||
|
||||
let all_ranges: Vec<Option<HttpRange>> = header[PREFIX_LEN..]
|
||||
.split(',')
|
||||
.map(|x| x.trim())
|
||||
.filter(|x| !x.is_empty())
|
||||
.map(|ra| {
|
||||
let mut start_end_iter = ra.split('-');
|
||||
|
||||
let start_str = start_end_iter.next().ok_or(())?.trim();
|
||||
let end_str = start_end_iter.next().ok_or(())?.trim();
|
||||
|
||||
if start_str.is_empty() {
|
||||
// If no start is specified, end specifies the
|
||||
// range start relative to the end of the file.
|
||||
let mut length: i64 = end_str.parse().map_err(|_| ())?;
|
||||
|
||||
if length > size_sig {
|
||||
length = size_sig;
|
||||
}
|
||||
|
||||
Ok(Some(HttpRange {
|
||||
start: (size_sig - length) as u64,
|
||||
length: length as u64,
|
||||
}))
|
||||
} else {
|
||||
let start: i64 = start_str.parse().map_err(|_| ())?;
|
||||
|
||||
if start < 0 {
|
||||
return Err(());
|
||||
}
|
||||
if start >= size_sig {
|
||||
no_overlap = true;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let length = if end_str.is_empty() {
|
||||
// If no end is specified, range extends to end of the file.
|
||||
size_sig - start
|
||||
} else {
|
||||
let mut end: i64 = end_str.parse().map_err(|_| ())?;
|
||||
|
||||
if start > end {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
if end >= size_sig {
|
||||
end = size_sig - 1;
|
||||
}
|
||||
|
||||
end - start + 1
|
||||
};
|
||||
|
||||
Ok(Some(HttpRange {
|
||||
start: start as u64,
|
||||
length: length as u64,
|
||||
}))
|
||||
}
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
let ranges: Vec<HttpRange> = all_ranges.into_iter().filter_map(|x| x).collect();
|
||||
|
||||
if no_overlap && ranges.is_empty() {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
Ok(ranges)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
struct T(&'static str, u64, Vec<HttpRange>);
|
||||
|
||||
#[test]
|
||||
fn test_parse() {
|
||||
let tests = vec![
|
||||
T("", 0, vec![]),
|
||||
T("", 1000, vec![]),
|
||||
T("foo", 0, vec![]),
|
||||
T("bytes=", 0, vec![]),
|
||||
T("bytes=7", 10, vec![]),
|
||||
T("bytes= 7 ", 10, vec![]),
|
||||
T("bytes=1-", 0, vec![]),
|
||||
T("bytes=5-4", 10, vec![]),
|
||||
T("bytes=0-2,5-4", 10, vec![]),
|
||||
T("bytes=2-5,4-3", 10, vec![]),
|
||||
T("bytes=--5,4--3", 10, vec![]),
|
||||
T("bytes=A-", 10, vec![]),
|
||||
T("bytes=A- ", 10, vec![]),
|
||||
T("bytes=A-Z", 10, vec![]),
|
||||
T("bytes= -Z", 10, vec![]),
|
||||
T("bytes=5-Z", 10, vec![]),
|
||||
T("bytes=Ran-dom, garbage", 10, vec![]),
|
||||
T("bytes=0x01-0x02", 10, vec![]),
|
||||
T("bytes= ", 10, vec![]),
|
||||
T("bytes= , , , ", 10, vec![]),
|
||||
T(
|
||||
"bytes=0-9",
|
||||
10,
|
||||
vec![HttpRange {
|
||||
start: 0,
|
||||
length: 10,
|
||||
}],
|
||||
),
|
||||
T(
|
||||
"bytes=0-",
|
||||
10,
|
||||
vec![HttpRange {
|
||||
start: 0,
|
||||
length: 10,
|
||||
}],
|
||||
),
|
||||
T(
|
||||
"bytes=5-",
|
||||
10,
|
||||
vec![HttpRange {
|
||||
start: 5,
|
||||
length: 5,
|
||||
}],
|
||||
),
|
||||
T(
|
||||
"bytes=0-20",
|
||||
10,
|
||||
vec![HttpRange {
|
||||
start: 0,
|
||||
length: 10,
|
||||
}],
|
||||
),
|
||||
T(
|
||||
"bytes=15-,0-5",
|
||||
10,
|
||||
vec![HttpRange {
|
||||
start: 0,
|
||||
length: 6,
|
||||
}],
|
||||
),
|
||||
T(
|
||||
"bytes=1-2,5-",
|
||||
10,
|
||||
vec![
|
||||
HttpRange {
|
||||
start: 1,
|
||||
length: 2,
|
||||
},
|
||||
HttpRange {
|
||||
start: 5,
|
||||
length: 5,
|
||||
},
|
||||
],
|
||||
),
|
||||
T(
|
||||
"bytes=-2 , 7-",
|
||||
11,
|
||||
vec![
|
||||
HttpRange {
|
||||
start: 9,
|
||||
length: 2,
|
||||
},
|
||||
HttpRange {
|
||||
start: 7,
|
||||
length: 4,
|
||||
},
|
||||
],
|
||||
),
|
||||
T(
|
||||
"bytes=0-0 ,2-2, 7-",
|
||||
11,
|
||||
vec![
|
||||
HttpRange {
|
||||
start: 0,
|
||||
length: 1,
|
||||
},
|
||||
HttpRange {
|
||||
start: 2,
|
||||
length: 1,
|
||||
},
|
||||
HttpRange {
|
||||
start: 7,
|
||||
length: 4,
|
||||
},
|
||||
],
|
||||
),
|
||||
T(
|
||||
"bytes=-5",
|
||||
10,
|
||||
vec![HttpRange {
|
||||
start: 5,
|
||||
length: 5,
|
||||
}],
|
||||
),
|
||||
T(
|
||||
"bytes=-15",
|
||||
10,
|
||||
vec![HttpRange {
|
||||
start: 0,
|
||||
length: 10,
|
||||
}],
|
||||
),
|
||||
T(
|
||||
"bytes=0-499",
|
||||
10000,
|
||||
vec![HttpRange {
|
||||
start: 0,
|
||||
length: 500,
|
||||
}],
|
||||
),
|
||||
T(
|
||||
"bytes=500-999",
|
||||
10000,
|
||||
vec![HttpRange {
|
||||
start: 500,
|
||||
length: 500,
|
||||
}],
|
||||
),
|
||||
T(
|
||||
"bytes=-500",
|
||||
10000,
|
||||
vec![HttpRange {
|
||||
start: 9500,
|
||||
length: 500,
|
||||
}],
|
||||
),
|
||||
T(
|
||||
"bytes=9500-",
|
||||
10000,
|
||||
vec![HttpRange {
|
||||
start: 9500,
|
||||
length: 500,
|
||||
}],
|
||||
),
|
||||
T(
|
||||
"bytes=0-0,-1",
|
||||
10000,
|
||||
vec![
|
||||
HttpRange {
|
||||
start: 0,
|
||||
length: 1,
|
||||
},
|
||||
HttpRange {
|
||||
start: 9999,
|
||||
length: 1,
|
||||
},
|
||||
],
|
||||
),
|
||||
T(
|
||||
"bytes=500-600,601-999",
|
||||
10000,
|
||||
vec![
|
||||
HttpRange {
|
||||
start: 500,
|
||||
length: 101,
|
||||
},
|
||||
HttpRange {
|
||||
start: 601,
|
||||
length: 399,
|
||||
},
|
||||
],
|
||||
),
|
||||
T(
|
||||
"bytes=500-700,601-999",
|
||||
10000,
|
||||
vec![
|
||||
HttpRange {
|
||||
start: 500,
|
||||
length: 201,
|
||||
},
|
||||
HttpRange {
|
||||
start: 601,
|
||||
length: 399,
|
||||
},
|
||||
],
|
||||
),
|
||||
// Match Apache laxity:
|
||||
T(
|
||||
"bytes= 1 -2 , 4- 5, 7 - 8 , ,,",
|
||||
11,
|
||||
vec![
|
||||
HttpRange {
|
||||
start: 1,
|
||||
length: 2,
|
||||
},
|
||||
HttpRange {
|
||||
start: 4,
|
||||
length: 2,
|
||||
},
|
||||
HttpRange {
|
||||
start: 7,
|
||||
length: 2,
|
||||
},
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
for t in tests {
|
||||
let header = t.0;
|
||||
let size = t.1;
|
||||
let expected = t.2;
|
||||
|
||||
let res = HttpRange::parse(header, size);
|
||||
|
||||
if res.is_err() {
|
||||
if expected.is_empty() {
|
||||
continue;
|
||||
} else {
|
||||
assert!(
|
||||
false,
|
||||
"parse({}, {}) returned error {:?}",
|
||||
header,
|
||||
size,
|
||||
res.unwrap_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let got = res.unwrap();
|
||||
|
||||
if got.len() != expected.len() {
|
||||
assert!(
|
||||
false,
|
||||
"len(parseRange({}, {})) = {}, want {}",
|
||||
header,
|
||||
size,
|
||||
got.len(),
|
||||
expected.len()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
for i in 0..expected.len() {
|
||||
if got[i].start != expected[i].start {
|
||||
assert!(
|
||||
false,
|
||||
"parseRange({}, {})[{}].start = {}, want {}",
|
||||
header, size, i, got[i].start, expected[i].start
|
||||
)
|
||||
}
|
||||
if got[i].length != expected[i].length {
|
||||
assert!(
|
||||
false,
|
||||
"parseRange({}, {})[{}].length = {}, want {}",
|
||||
header, size, i, got[i].length, expected[i].length
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1
actix-files/tests/test space.binary
Normal file
1
actix-files/tests/test space.binary
Normal file
@ -0,0 +1 @@
|
||||
<EFBFBD>TǑɂV<EFBFBD>2<EFBFBD>vI<EFBFBD><EFBFBD><EFBFBD>\<5C>R˙<52><CB99><EFBFBD>e<EFBFBD><04>vD<76>:藽<>RV<03>Yp<59><70>;<3B><>G<><47>p!2<7F>C<EFBFBD>.<2E><0C><><EFBFBD><EFBFBD>pA!<21>ߦ<EFBFBD>x j+Uc<55><63><EFBFBD>X<13>c%<17>;<3B>"y<10><>AI
|
1
actix-files/tests/test.binary
Normal file
1
actix-files/tests/test.binary
Normal file
@ -0,0 +1 @@
|
||||
<EFBFBD>TǑɂV<EFBFBD>2<EFBFBD>vI<EFBFBD><EFBFBD><EFBFBD>\<5C>R˙<52><CB99><EFBFBD>e<EFBFBD><04>vD<76>:藽<>RV<03>Yp<59><70>;<3B><>G<><47>p!2<7F>C<EFBFBD>.<2E><0C><><EFBFBD><EFBFBD>pA!<21>ߦ<EFBFBD>x j+Uc<55><63><EFBFBD>X<13>c%<17>;<3B>"y<10><>AI
|
BIN
actix-files/tests/test.png
Normal file
BIN
actix-files/tests/test.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 168 B |
Reference in New Issue
Block a user