mirror of
https://github.com/actix/actix-extras.git
synced 2024-11-28 01:32:57 +01:00
simplify StaticFile constructor, move HttpRange to separate module
This commit is contained in:
parent
6efc3438b8
commit
3fc28c5d07
@ -27,10 +27,12 @@ use futures::future::{ok, FutureResult};
|
|||||||
mod config;
|
mod config;
|
||||||
mod error;
|
mod error;
|
||||||
mod named;
|
mod named;
|
||||||
|
mod range;
|
||||||
|
|
||||||
use self::error::{StaticFilesError, UriSegmentError};
|
use self::error::{StaticFilesError, UriSegmentError};
|
||||||
pub use crate::config::{DefaultConfig, StaticFileConfig};
|
pub use crate::config::{DefaultConfig, StaticFileConfig};
|
||||||
pub use crate::named::NamedFile;
|
pub use crate::named::NamedFile;
|
||||||
|
pub use crate::range::HttpRange;
|
||||||
|
|
||||||
type HttpNewService<P> = BoxedNewService<(), ServiceRequest<P>, ServiceResponse, (), ()>;
|
type HttpNewService<P> = BoxedNewService<(), ServiceRequest<P>, ServiceResponse, (), ()>;
|
||||||
|
|
||||||
@ -212,17 +214,15 @@ fn directory_listing(
|
|||||||
|
|
||||||
/// Static files handling
|
/// Static files handling
|
||||||
///
|
///
|
||||||
/// `StaticFile` handler must be registered with `App::handler()` method,
|
/// `StaticFile` handler must be registered with `App::service()` method.
|
||||||
/// because `StaticFile` handler requires access sub-path information.
|
|
||||||
///
|
///
|
||||||
/// ```rust,ignore
|
/// ```rust
|
||||||
/// # extern crate actix_web;
|
/// use actix_web::App;
|
||||||
/// use actix_web::{fs, App};
|
/// use actix_staticfiles as fs;
|
||||||
///
|
///
|
||||||
/// fn main() {
|
/// fn main() {
|
||||||
/// let app = App::new()
|
/// let app = App::new()
|
||||||
/// .handler("/static", fs::StaticFiles::new(".").unwrap())
|
/// .service(fs::StaticFiles::new("/static", "."));
|
||||||
/// .finish();
|
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub struct StaticFiles<S, C = DefaultConfig> {
|
pub struct StaticFiles<S, C = DefaultConfig> {
|
||||||
@ -243,7 +243,7 @@ impl<S: 'static> StaticFiles<S> {
|
|||||||
/// `StaticFile` uses `ThreadPool` for blocking filesystem operations.
|
/// `StaticFile` uses `ThreadPool` for blocking filesystem operations.
|
||||||
/// By default pool with 5x threads of available cpus is used.
|
/// By default pool with 5x threads of available cpus is used.
|
||||||
/// Pool size can be changed by setting ACTIX_CPU_POOL environment variable.
|
/// Pool size can be changed by setting ACTIX_CPU_POOL environment variable.
|
||||||
pub fn new<T: Into<PathBuf>>(path: &str, dir: T) -> Result<StaticFiles<S>, Error> {
|
pub fn new<T: Into<PathBuf>>(path: &str, dir: T) -> StaticFiles<S> {
|
||||||
Self::with_config(path, dir, DefaultConfig)
|
Self::with_config(path, dir, DefaultConfig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -252,18 +252,13 @@ impl<S: 'static, C: StaticFileConfig> StaticFiles<S, C> {
|
|||||||
/// Create new `StaticFiles` instance for specified base directory.
|
/// Create new `StaticFiles` instance for specified base directory.
|
||||||
///
|
///
|
||||||
/// Identical with `new` but allows to specify configiration to use.
|
/// Identical with `new` but allows to specify configiration to use.
|
||||||
pub fn with_config<T: Into<PathBuf>>(
|
pub fn with_config<T: Into<PathBuf>>(path: &str, dir: T, _: C) -> StaticFiles<S, C> {
|
||||||
path: &str,
|
let dir = dir.into().canonicalize().unwrap_or_else(|_| PathBuf::new());
|
||||||
dir: T,
|
|
||||||
_: C,
|
|
||||||
) -> Result<StaticFiles<S, C>, Error> {
|
|
||||||
let dir = dir.into().canonicalize()?;
|
|
||||||
|
|
||||||
if !dir.is_dir() {
|
if !dir.is_dir() {
|
||||||
return Err(StaticFilesError::IsNotDirectory.into());
|
log::error!("Specified path is not a directory");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(StaticFiles {
|
StaticFiles {
|
||||||
path: ResourceDef::root_prefix(path),
|
path: ResourceDef::root_prefix(path),
|
||||||
directory: dir,
|
directory: dir,
|
||||||
index: None,
|
index: None,
|
||||||
@ -273,7 +268,7 @@ impl<S: 'static, C: StaticFileConfig> StaticFiles<S, C> {
|
|||||||
_chunk_size: 0,
|
_chunk_size: 0,
|
||||||
_follow_symlinks: false,
|
_follow_symlinks: false,
|
||||||
_cd_map: PhantomData,
|
_cd_map: PhantomData,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show files listing for directories.
|
/// Show files listing for directories.
|
||||||
@ -452,101 +447,6 @@ impl<P> FromRequest<P> for PathBufWrp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// HTTP Range header representation.
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
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).
|
|
||||||
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@ -800,11 +700,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_named_file_ranges_status_code() {
|
fn test_named_file_ranges_status_code() {
|
||||||
let mut srv = test::init_service(
|
let mut srv = test::init_service(
|
||||||
App::new().service(
|
App::new().service(StaticFiles::new("/test", ".").index_file("Cargo.toml")),
|
||||||
StaticFiles::new("/test", ".")
|
|
||||||
.unwrap()
|
|
||||||
.index_file("Cargo.toml"),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Valid range header
|
// Valid range header
|
||||||
@ -828,11 +724,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_named_file_content_range_headers() {
|
fn test_named_file_content_range_headers() {
|
||||||
let mut srv = test::init_service(
|
let mut srv = test::init_service(
|
||||||
App::new().service(
|
App::new()
|
||||||
StaticFiles::new("/test", ".")
|
.service(StaticFiles::new("/test", ".").index_file("tests/test.binary")),
|
||||||
.unwrap()
|
|
||||||
.index_file("tests/test.binary"),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Valid range header
|
// Valid range header
|
||||||
@ -871,11 +764,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_named_file_content_length_headers() {
|
fn test_named_file_content_length_headers() {
|
||||||
let mut srv = test::init_service(
|
let mut srv = test::init_service(
|
||||||
App::new().service(
|
App::new()
|
||||||
StaticFiles::new("test", ".")
|
.service(StaticFiles::new("test", ".").index_file("tests/test.binary")),
|
||||||
.unwrap()
|
|
||||||
.index_file("tests/test.binary"),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Valid range header
|
// Valid range header
|
||||||
@ -948,8 +838,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_static_files_with_spaces() {
|
fn test_static_files_with_spaces() {
|
||||||
let mut srv = test::init_service(
|
let mut srv = test::init_service(
|
||||||
App::new()
|
App::new().service(StaticFiles::new("/", ".").index_file("Cargo.toml")),
|
||||||
.service(StaticFiles::new("/", ".").unwrap().index_file("Cargo.toml")),
|
|
||||||
);
|
);
|
||||||
let request = TestRequest::get()
|
let request = TestRequest::get()
|
||||||
.uri("/tests/test%20space.binary")
|
.uri("/tests/test%20space.binary")
|
||||||
@ -1030,22 +919,21 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_static_files() {
|
fn test_static_files() {
|
||||||
let mut srv = test::init_service(
|
let mut srv = test::init_service(
|
||||||
App::new().service(StaticFiles::new("/", ".").unwrap().show_files_listing()),
|
App::new().service(StaticFiles::new("/", ".").show_files_listing()),
|
||||||
);
|
);
|
||||||
let req = TestRequest::with_uri("/missing").to_request();
|
let req = TestRequest::with_uri("/missing").to_request();
|
||||||
|
|
||||||
let resp = test::call_success(&mut srv, req);
|
let resp = test::call_success(&mut srv, req);
|
||||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||||
|
|
||||||
let mut srv =
|
let mut srv = test::init_service(App::new().service(StaticFiles::new("/", ".")));
|
||||||
test::init_service(App::new().service(StaticFiles::new("/", ".").unwrap()));
|
|
||||||
|
|
||||||
let req = TestRequest::default().to_request();
|
let req = TestRequest::default().to_request();
|
||||||
let resp = test::call_success(&mut srv, req);
|
let resp = test::call_success(&mut srv, req);
|
||||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||||
|
|
||||||
let mut srv = test::init_service(
|
let mut srv = test::init_service(
|
||||||
App::new().service(StaticFiles::new("/", ".").unwrap().show_files_listing()),
|
App::new().service(StaticFiles::new("/", ".").show_files_listing()),
|
||||||
);
|
);
|
||||||
let req = TestRequest::with_uri("/tests").to_request();
|
let req = TestRequest::with_uri("/tests").to_request();
|
||||||
let mut resp = test::call_success(&mut srv, req);
|
let mut resp = test::call_success(&mut srv, req);
|
||||||
@ -1065,17 +953,13 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_static_files_bad_directory() {
|
fn test_static_files_bad_directory() {
|
||||||
let st: Result<StaticFiles<()>, Error> = StaticFiles::new("/", "missing");
|
let _st: StaticFiles<()> = StaticFiles::new("/", "missing");
|
||||||
assert!(st.is_err());
|
let _st: StaticFiles<()> = StaticFiles::new("/", "Cargo.toml");
|
||||||
|
|
||||||
let st: Result<StaticFiles<()>, Error> = StaticFiles::new("/", "Cargo.toml");
|
|
||||||
assert!(st.is_err());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[test]
|
// #[test]
|
||||||
// fn test_default_handler_file_missing() {
|
// fn test_default_handler_file_missing() {
|
||||||
// let st = StaticFiles::new(".")
|
// let st = StaticFiles::new(".")
|
||||||
// .unwrap()
|
|
||||||
// .default_handler(|_: &_| "default content");
|
// .default_handler(|_: &_| "default content");
|
||||||
// let req = TestRequest::with_uri("/missing")
|
// let req = TestRequest::with_uri("/missing")
|
||||||
// .param("tail", "missing")
|
// .param("tail", "missing")
|
||||||
@ -1092,7 +976,7 @@ mod tests {
|
|||||||
|
|
||||||
// #[test]
|
// #[test]
|
||||||
// fn test_serve_index() {
|
// fn test_serve_index() {
|
||||||
// let st = StaticFiles::new(".").unwrap().index_file("test.binary");
|
// let st = StaticFiles::new(".").index_file("test.binary");
|
||||||
// let req = TestRequest::default().uri("/tests").finish();
|
// let req = TestRequest::default().uri("/tests").finish();
|
||||||
|
|
||||||
// let resp = st.handle(&req).respond_to(&req).unwrap();
|
// let resp = st.handle(&req).respond_to(&req).unwrap();
|
||||||
@ -1138,7 +1022,7 @@ mod tests {
|
|||||||
|
|
||||||
// #[test]
|
// #[test]
|
||||||
// fn test_serve_index_nested() {
|
// fn test_serve_index_nested() {
|
||||||
// let st = StaticFiles::new(".").unwrap().index_file("mod.rs");
|
// let st = StaticFiles::new(".").index_file("mod.rs");
|
||||||
// let req = TestRequest::default().uri("/src/client").finish();
|
// let req = TestRequest::default().uri("/src/client").finish();
|
||||||
// let resp = st.handle(&req).respond_to(&req).unwrap();
|
// let resp = st.handle(&req).respond_to(&req).unwrap();
|
||||||
// let resp = resp.as_msg();
|
// let resp = resp.as_msg();
|
||||||
@ -1158,7 +1042,7 @@ mod tests {
|
|||||||
// let mut srv = test::TestServer::with_factory(|| {
|
// let mut srv = test::TestServer::with_factory(|| {
|
||||||
// App::new().handler(
|
// App::new().handler(
|
||||||
// "test",
|
// "test",
|
||||||
// StaticFiles::new(".").unwrap().index_file("Cargo.toml"),
|
// StaticFiles::new(".").index_file("Cargo.toml"),
|
||||||
// )
|
// )
|
||||||
// });
|
// });
|
||||||
|
|
||||||
@ -1191,7 +1075,7 @@ mod tests {
|
|||||||
// let mut srv = test::TestServer::with_factory(|| {
|
// let mut srv = test::TestServer::with_factory(|| {
|
||||||
// App::new().handler(
|
// App::new().handler(
|
||||||
// "test",
|
// "test",
|
||||||
// StaticFiles::new(".").unwrap().index_file("Cargo.toml"),
|
// StaticFiles::new(".").index_file("Cargo.toml"),
|
||||||
// )
|
// )
|
||||||
// });
|
// });
|
||||||
|
|
||||||
@ -1204,279 +1088,4 @@ mod tests {
|
|||||||
// assert_eq!(response.status(), StatusCode::OK);
|
// assert_eq!(response.status(), StatusCode::OK);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,8 @@ use actix_web::http::{ContentEncoding, Method, StatusCode};
|
|||||||
use actix_web::{HttpMessage, HttpRequest, HttpResponse, Responder};
|
use actix_web::{HttpMessage, HttpRequest, HttpResponse, Responder};
|
||||||
|
|
||||||
use crate::config::{DefaultConfig, StaticFileConfig};
|
use crate::config::{DefaultConfig, StaticFileConfig};
|
||||||
use crate::{ChunkedReadFile, HttpRange};
|
use crate::range::HttpRange;
|
||||||
|
use crate::ChunkedReadFile;
|
||||||
|
|
||||||
/// A file with an associated name.
|
/// A file with an associated name.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -303,6 +304,8 @@ impl<C: StaticFileConfig> Responder for NamedFile<C> {
|
|||||||
type Future = Result<HttpResponse, Error>;
|
type Future = Result<HttpResponse, Error>;
|
||||||
|
|
||||||
fn respond_to(self, req: &HttpRequest) -> Self::Future {
|
fn respond_to(self, req: &HttpRequest) -> Self::Future {
|
||||||
|
println!("RESP: {:?}", req);
|
||||||
|
|
||||||
if self.status_code != StatusCode::OK {
|
if self.status_code != StatusCode::OK {
|
||||||
let mut resp = HttpResponse::build(self.status_code);
|
let mut resp = HttpResponse::build(self.status_code);
|
||||||
resp.set(header::ContentType(self.content_type.clone()))
|
resp.set(header::ContentType(self.content_type.clone()))
|
||||||
|
375
actix-staticfiles/src/range.rs
Normal file
375
actix-staticfiles/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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user