From 6efc3438b83497577b8d791b77c6600e95660f35 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 22:10:08 -0800 Subject: [PATCH] refactor and enable some tests for staticfiles --- .travis.yml | 4 +- Cargo.toml | 2 +- {staticfiles => actix-staticfiles}/CHANGES.md | 0 {staticfiles => actix-staticfiles}/Cargo.toml | 3 +- {staticfiles => actix-staticfiles}/README.md | 0 actix-staticfiles/src/config.rs | 70 + actix-staticfiles/src/error.rs | 41 + actix-staticfiles/src/lib.rs | 1482 ++++++++++++ actix-staticfiles/src/named.rs | 438 ++++ actix-staticfiles/tests/test space.binary | 1 + actix-staticfiles/tests/test.binary | 1 + actix-staticfiles/tests/test.png | Bin 0 -> 168 bytes examples/basic.rs | 25 +- src/app.rs | 45 +- src/lib.rs | 24 +- src/service.rs | 64 +- src/test.rs | 28 + staticfiles/src/lib.rs | 2033 ----------------- 18 files changed, 2198 insertions(+), 2063 deletions(-) rename {staticfiles => actix-staticfiles}/CHANGES.md (100%) rename {staticfiles => actix-staticfiles}/Cargo.toml (93%) rename {staticfiles => actix-staticfiles}/README.md (100%) create mode 100644 actix-staticfiles/src/config.rs create mode 100644 actix-staticfiles/src/error.rs create mode 100644 actix-staticfiles/src/lib.rs create mode 100644 actix-staticfiles/src/named.rs create mode 100644 actix-staticfiles/tests/test space.binary create mode 100644 actix-staticfiles/tests/test.binary create mode 100644 actix-staticfiles/tests/test.png delete mode 100644 staticfiles/src/lib.rs diff --git a/.travis.yml b/.travis.yml index 1d3c227a..55a03ec8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/Cargo.toml b/Cargo.toml index 2f50b210..acacb2f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ path = "src/lib.rs" members = [ ".", "actix-session", - "staticfiles", + "actix-staticfiles", ] [package.metadata.docs.rs] diff --git a/staticfiles/CHANGES.md b/actix-staticfiles/CHANGES.md similarity index 100% rename from staticfiles/CHANGES.md rename to actix-staticfiles/CHANGES.md diff --git a/staticfiles/Cargo.toml b/actix-staticfiles/Cargo.toml similarity index 93% rename from staticfiles/Cargo.toml rename to actix-staticfiles/Cargo.toml index 0aa58970..0a551792 100644 --- a/staticfiles/Cargo.toml +++ b/actix-staticfiles/Cargo.toml @@ -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" diff --git a/staticfiles/README.md b/actix-staticfiles/README.md similarity index 100% rename from staticfiles/README.md rename to actix-staticfiles/README.md diff --git a/actix-staticfiles/src/config.rs b/actix-staticfiles/src/config.rs new file mode 100644 index 00000000..da72da20 --- /dev/null +++ b/actix-staticfiles/src/config.rs @@ -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 {} diff --git a/actix-staticfiles/src/error.rs b/actix-staticfiles/src/error.rs new file mode 100644 index 00000000..f165a618 --- /dev/null +++ b/actix-staticfiles/src/error.rs @@ -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) + } +} diff --git a/actix-staticfiles/src/lib.rs b/actix-staticfiles/src/lib.rs new file mode 100644 index 00000000..01306d4b --- /dev/null +++ b/actix-staticfiles/src/lib.rs @@ -0,0 +1,1482 @@ +//! Static files support +use std::cell::RefCell; +use std::fmt::Write; +use std::fs::{DirEntry, File}; +use std::io::{Read, Seek}; +use std::marker::PhantomData; +use std::path::{Path, PathBuf}; +use std::rc::Rc; +use std::{cmp, io}; + +use bytes::Bytes; +use futures::{Async, Future, Poll, Stream}; +use mime; +use mime_guess::get_mime_type; +use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; +use v_htmlescape::escape as escape_html_entity; + +use actix_http::error::{Error, ErrorInternalServerError}; +use actix_service::{boxed::BoxedNewService, NewService, Service}; +use actix_web::dev::{self, HttpServiceFactory, ResourceDef, Url}; +use actix_web::{ + blocking, FromRequest, HttpRequest, HttpResponse, Responder, ServiceFromRequest, + ServiceRequest, ServiceResponse, +}; +use futures::future::{ok, FutureResult}; + +mod config; +mod error; +mod named; + +use self::error::{StaticFilesError, UriSegmentError}; +pub use crate::config::{DefaultConfig, StaticFileConfig}; +pub use crate::named::NamedFile; + +type HttpNewService

= BoxedNewService<(), ServiceRequest

, ServiceResponse, (), ()>; + +/// Return the MIME type associated with a filename extension (case-insensitive). +/// If `ext` is empty or no associated type for the extension was found, returns +/// the type `application/octet-stream`. +#[inline] +pub fn file_extension_to_mime(ext: &str) -> mime::Mime { + get_mime_type(ext) +} + +#[doc(hidden)] +/// A helper created from a `std::fs::File` which reads the file +/// chunk-by-chunk on a `ThreadPool`. +pub struct ChunkedReadFile { + size: u64, + offset: u64, + file: Option, + fut: Option>, + counter: u64, +} + +fn handle_error(err: blocking::BlockingError) -> Error { + match err { + blocking::BlockingError::Error(err) => err.into(), + blocking::BlockingError::Canceled => { + ErrorInternalServerError("Unexpected error").into() + } + } +} + +impl Stream for ChunkedReadFile { + type Item = Bytes; + type Error = Error; + + fn poll(&mut self) -> Poll, Error> { + if self.fut.is_some() { + return match self.fut.as_mut().unwrap().poll().map_err(handle_error)? { + Async::Ready((file, bytes)) => { + self.fut.take(); + self.file = Some(file); + self.offset += bytes.len() as u64; + self.counter += bytes.len() as u64; + Ok(Async::Ready(Some(bytes))) + } + Async::NotReady => Ok(Async::NotReady), + }; + } + + let size = self.size; + let offset = self.offset; + let counter = self.counter; + + if size == counter { + Ok(Async::Ready(None)) + } else { + let mut file = self.file.take().expect("Use after completion"); + self.fut = Some(blocking::run(move || { + let max_bytes: usize; + max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; + let mut buf = Vec::with_capacity(max_bytes); + file.seek(io::SeekFrom::Start(offset))?; + let nbytes = + file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?; + if nbytes == 0 { + return Err(io::ErrorKind::UnexpectedEof.into()); + } + Ok((file, Bytes::from(buf))) + })); + self.poll() + } + } +} + +type DirectoryRenderer = + Fn(&Directory, &HttpRequest) -> Result; + +/// A directory; responds with the generated directory listing. +#[derive(Debug)] +pub struct Directory { + /// Base directory + pub base: PathBuf, + /// Path of subdirectory to generate listing for + pub path: PathBuf, +} + +impl Directory { + /// Create a new directory + pub fn new(base: PathBuf, path: PathBuf) -> Directory { + Directory { base, path } + } + + /// Is this entry visible from this directory? + pub fn is_visible(&self, entry: &io::Result) -> bool { + if let Ok(ref entry) = *entry { + if let Some(name) = entry.file_name().to_str() { + if name.starts_with('.') { + return false; + } + } + if let Ok(ref md) = entry.metadata() { + let ft = md.file_type(); + return ft.is_dir() || ft.is_file() || ft.is_symlink(); + } + } + false + } +} + +// show file url as relative to static path +macro_rules! encode_file_url { + ($path:ident) => { + utf8_percent_encode(&$path.to_string_lossy(), DEFAULT_ENCODE_SET) + }; +} + +// " -- " & -- & ' -- ' < -- < > -- > / -- / +macro_rules! encode_file_name { + ($entry:ident) => { + escape_html_entity(&$entry.file_name().to_string_lossy()) + }; +} + +fn directory_listing( + dir: &Directory, + req: &HttpRequest, +) -> Result { + let index_of = format!("Index of {}", req.path()); + let mut body = String::new(); + let base = Path::new(req.path()); + + for entry in dir.path.read_dir()? { + if dir.is_visible(&entry) { + let entry = entry.unwrap(); + let p = match entry.path().strip_prefix(&dir.path) { + Ok(p) => base.join(p), + Err(_) => continue, + }; + + // if file is a directory, add '/' to the end of the name + if let Ok(metadata) = entry.metadata() { + if metadata.is_dir() { + let _ = write!( + body, + "

  • {}/
  • ", + encode_file_url!(p), + encode_file_name!(entry), + ); + } else { + let _ = write!( + body, + "
  • {}
  • ", + encode_file_url!(p), + encode_file_name!(entry), + ); + } + } else { + continue; + } + } + } + + let html = format!( + "\ + {}\ +

    {}

    \ + \n", + index_of, index_of, body + ); + Ok(ServiceResponse::new( + req.clone(), + HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(html), + )) +} + +/// Static files handling +/// +/// `StaticFile` handler must be registered with `App::handler()` method, +/// because `StaticFile` handler requires access sub-path information. +/// +/// ```rust,ignore +/// # extern crate actix_web; +/// use actix_web::{fs, App}; +/// +/// fn main() { +/// let app = App::new() +/// .handler("/static", fs::StaticFiles::new(".").unwrap()) +/// .finish(); +/// } +/// ``` +pub struct StaticFiles { + path: ResourceDef, + directory: PathBuf, + index: Option, + show_index: bool, + default: Rc>>>>, + renderer: Rc, + _chunk_size: usize, + _follow_symlinks: bool, + _cd_map: PhantomData, +} + +impl StaticFiles { + /// Create new `StaticFiles` instance for specified base directory. + /// + /// `StaticFile` uses `ThreadPool` for blocking filesystem operations. + /// By default pool with 5x threads of available cpus is used. + /// Pool size can be changed by setting ACTIX_CPU_POOL environment variable. + pub fn new>(path: &str, dir: T) -> Result, Error> { + Self::with_config(path, dir, DefaultConfig) + } +} + +impl StaticFiles { + /// Create new `StaticFiles` instance for specified base directory. + /// + /// Identical with `new` but allows to specify configiration to use. + pub fn with_config>( + path: &str, + dir: T, + _: C, + ) -> Result, Error> { + let dir = dir.into().canonicalize()?; + + if !dir.is_dir() { + return Err(StaticFilesError::IsNotDirectory.into()); + } + + Ok(StaticFiles { + path: ResourceDef::root_prefix(path), + directory: dir, + index: None, + show_index: false, + default: Rc::new(RefCell::new(None)), + renderer: Rc::new(directory_listing), + _chunk_size: 0, + _follow_symlinks: false, + _cd_map: PhantomData, + }) + } + + /// Show files listing for directories. + /// + /// By default show files listing is disabled. + pub fn show_files_listing(mut self) -> Self { + self.show_index = true; + self + } + + /// Set custom directory renderer + pub fn files_listing_renderer(mut self, f: F) -> Self + where + for<'r, 's> F: + Fn(&'r Directory, &'s HttpRequest) -> Result + + 'static, + { + self.renderer = Rc::new(f); + self + } + + /// Set index file + /// + /// Shows specific index file for directory "/" instead of + /// showing files listing. + pub fn index_file>(mut self, index: T) -> StaticFiles { + self.index = Some(index.into()); + self + } +} + +impl HttpServiceFactory

    for StaticFiles { + type Factory = Self; + + fn rdef(&self) -> &ResourceDef { + &self.path + } + + fn create(self) -> Self { + self + } +} + +impl NewService> + for StaticFiles +{ + type Response = ServiceResponse; + type Error = (); + type Service = StaticFilesService; + type InitError = (); + type Future = FutureResult; + + fn new_service(&self, _: &()) -> Self::Future { + ok(StaticFilesService { + directory: self.directory.clone(), + index: self.index.clone(), + show_index: self.show_index, + default: self.default.clone(), + renderer: self.renderer.clone(), + _chunk_size: self._chunk_size, + _follow_symlinks: self._follow_symlinks, + _cd_map: self._cd_map, + }) + } +} + +pub struct StaticFilesService { + directory: PathBuf, + index: Option, + show_index: bool, + default: Rc>>>>, + renderer: Rc, + _chunk_size: usize, + _follow_symlinks: bool, + _cd_map: PhantomData, +} + +impl Service> for StaticFilesService { + type Response = ServiceResponse; + type Error = (); + type Future = FutureResult; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, req: ServiceRequest

    ) -> Self::Future { + let (req, _) = req.into_parts(); + + let real_path = match PathBufWrp::get_pathbuf(req.match_info()) { + Ok(item) => item, + Err(e) => return ok(ServiceResponse::from_err(e, req.clone())), + }; + + // full filepath + let path = match self.directory.join(&real_path.0).canonicalize() { + Ok(path) => path, + Err(e) => return ok(ServiceResponse::from_err(e, req.clone())), + }; + + if path.is_dir() { + if let Some(ref redir_index) = self.index { + let path = path.join(redir_index); + + match NamedFile::open_with_config(path, C::default()) { + Ok(named_file) => match named_file.respond_to(&req) { + Ok(item) => ok(ServiceResponse::new(req.clone(), item)), + Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + }, + Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + } + } else if self.show_index { + let dir = Directory::new(self.directory.clone(), path); + let x = (self.renderer)(&dir, &req); + match x { + Ok(resp) => ok(resp), + Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + } + } else { + ok(ServiceResponse::from_err( + StaticFilesError::IsDirectory, + req.clone(), + )) + } + } else { + match NamedFile::open_with_config(path, C::default()) { + Ok(named_file) => match named_file.respond_to(&req) { + Ok(item) => ok(ServiceResponse::new(req.clone(), item)), + Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + }, + Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + } + } + } +} + +struct PathBufWrp(PathBuf); + +impl PathBufWrp { + fn get_pathbuf(path: &dev::Path) -> Result { + let path_str = path.path(); + let mut buf = PathBuf::new(); + for segment in path_str.split('/') { + if segment == ".." { + buf.pop(); + } else if segment.starts_with('.') { + return Err(UriSegmentError::BadStart('.')); + } else if segment.starts_with('*') { + return Err(UriSegmentError::BadStart('*')); + } else if segment.ends_with(':') { + return Err(UriSegmentError::BadEnd(':')); + } else if segment.ends_with('>') { + return Err(UriSegmentError::BadEnd('>')); + } else if segment.ends_with('<') { + return Err(UriSegmentError::BadEnd('<')); + } else if segment.is_empty() { + continue; + } else if cfg!(windows) && segment.contains('\\') { + return Err(UriSegmentError::BadChar('\\')); + } else { + buf.push(segment) + } + } + + Ok(PathBufWrp(buf)) + } +} + +impl

    FromRequest

    for PathBufWrp { + type Error = UriSegmentError; + type Future = Result; + type Config = (); + + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { + PathBufWrp::get_pathbuf(req.match_info()) + } +} + +/// 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, ()> { + 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> = 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::>()?; + + let ranges: Vec = all_ranges.into_iter().filter_map(|x| x).collect(); + + if no_overlap && ranges.is_empty() { + return Err(()); + } + + Ok(ranges) + } +} + +#[cfg(test)] +mod tests { + use std::fs; + use std::ops::Add; + use std::time::{Duration, SystemTime}; + + use bytes::BytesMut; + + use super::*; + use actix_web::http::{header, header::DispositionType, Method, StatusCode}; + use actix_web::test::{self, TestRequest}; + use actix_web::App; + + #[test] + fn test_file_extension_to_mime() { + let m = file_extension_to_mime("jpg"); + assert_eq!(m, mime::IMAGE_JPEG); + + let m = file_extension_to_mime("invalid extension!!"); + assert_eq!(m, mime::APPLICATION_OCTET_STREAM); + + let m = file_extension_to_mime(""); + assert_eq!(m, mime::APPLICATION_OCTET_STREAM); + } + + #[test] + fn test_if_modified_since_without_if_none_match() { + let file = NamedFile::open("Cargo.toml").unwrap(); + let since = + header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); + + let req = TestRequest::default() + .header(header::IF_MODIFIED_SINCE, since) + .to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_MODIFIED); + } + + #[test] + fn test_if_modified_since_with_if_none_match() { + let file = NamedFile::open("Cargo.toml").unwrap(); + let since = + header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); + + let req = TestRequest::default() + .header(header::IF_NONE_MATCH, "miss_etag") + .header(header::IF_MODIFIED_SINCE, since) + .to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_ne!(resp.status(), StatusCode::NOT_MODIFIED); + } + + #[test] + fn test_named_file_text() { + assert!(NamedFile::open("test--").is_err()); + let mut file = NamedFile::open("Cargo.toml").unwrap(); + { + file.file(); + let _f: &File = &file; + } + { + let _f: &mut File = &mut file; + } + + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "text/x-toml" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "inline; filename=\"Cargo.toml\"" + ); + } + + #[test] + fn test_named_file_set_content_type() { + let mut file = NamedFile::open("Cargo.toml") + .unwrap() + .set_content_type(mime::TEXT_XML); + { + file.file(); + let _f: &File = &file; + } + { + let _f: &mut File = &mut file; + } + + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "text/xml" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "inline; filename=\"Cargo.toml\"" + ); + } + + #[test] + fn test_named_file_image() { + let mut file = NamedFile::open("tests/test.png").unwrap(); + { + file.file(); + let _f: &File = &file; + } + { + let _f: &mut File = &mut file; + } + + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "image/png" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "inline; filename=\"test.png\"" + ); + } + + #[test] + fn test_named_file_image_attachment() { + use header::{ContentDisposition, DispositionParam, DispositionType}; + let cd = ContentDisposition { + disposition: DispositionType::Attachment, + parameters: vec![DispositionParam::Filename(String::from("test.png"))], + }; + let mut file = NamedFile::open("tests/test.png") + .unwrap() + .set_content_disposition(cd); + { + file.file(); + let _f: &File = &file; + } + { + let _f: &mut File = &mut file; + } + + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "image/png" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "attachment; filename=\"test.png\"" + ); + } + + #[derive(Default)] + pub struct AllAttachmentConfig; + impl StaticFileConfig for AllAttachmentConfig { + fn content_disposition_map(_typ: mime::Name) -> DispositionType { + DispositionType::Attachment + } + } + + #[derive(Default)] + pub struct AllInlineConfig; + impl StaticFileConfig for AllInlineConfig { + fn content_disposition_map(_typ: mime::Name) -> DispositionType { + DispositionType::Inline + } + } + + #[test] + fn test_named_file_image_attachment_and_custom_config() { + let file = + NamedFile::open_with_config("tests/test.png", AllAttachmentConfig).unwrap(); + + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "image/png" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "attachment; filename=\"test.png\"" + ); + + let file = + NamedFile::open_with_config("tests/test.png", AllInlineConfig).unwrap(); + + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "image/png" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "inline; filename=\"test.png\"" + ); + } + + #[test] + fn test_named_file_binary() { + let mut file = NamedFile::open("tests/test.binary").unwrap(); + { + file.file(); + let _f: &File = &file; + } + { + let _f: &mut File = &mut file; + } + + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "application/octet-stream" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "attachment; filename=\"test.binary\"" + ); + } + + #[test] + fn test_named_file_status_code_text() { + let mut file = NamedFile::open("Cargo.toml") + .unwrap() + .set_status_code(StatusCode::NOT_FOUND); + { + file.file(); + let _f: &File = &file; + } + { + let _f: &mut File = &mut file; + } + + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "text/x-toml" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "inline; filename=\"Cargo.toml\"" + ); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + #[test] + fn test_named_file_ranges_status_code() { + let mut srv = test::init_service( + App::new().service( + StaticFiles::new("/test", ".") + .unwrap() + .index_file("Cargo.toml"), + ), + ); + + // Valid range header + let request = TestRequest::get() + .uri("/t%65st/Cargo.toml") + .header(header::RANGE, "bytes=10-20") + .to_request(); + let response = test::call_success(&mut srv, request); + assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); + + // Invalid range header + let request = TestRequest::get() + .uri("/t%65st/Cargo.toml") + .header(header::RANGE, "bytes=1-0") + .to_request(); + let response = test::call_success(&mut srv, request); + + assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); + } + + #[test] + fn test_named_file_content_range_headers() { + let mut srv = test::init_service( + App::new().service( + StaticFiles::new("/test", ".") + .unwrap() + .index_file("tests/test.binary"), + ), + ); + + // Valid range header + let request = TestRequest::get() + .uri("/t%65st/tests/test.binary") + .header(header::RANGE, "bytes=10-20") + .to_request(); + + let response = test::call_success(&mut srv, request); + let contentrange = response + .headers() + .get(header::CONTENT_RANGE) + .unwrap() + .to_str() + .unwrap(); + + assert_eq!(contentrange, "bytes 10-20/100"); + + // Invalid range header + let request = TestRequest::get() + .uri("/t%65st/tests/test.binary") + .header(header::RANGE, "bytes=10-5") + .to_request(); + let response = test::call_success(&mut srv, request); + + let contentrange = response + .headers() + .get(header::CONTENT_RANGE) + .unwrap() + .to_str() + .unwrap(); + + assert_eq!(contentrange, "bytes */100"); + } + + #[test] + fn test_named_file_content_length_headers() { + let mut srv = test::init_service( + App::new().service( + StaticFiles::new("test", ".") + .unwrap() + .index_file("tests/test.binary"), + ), + ); + + // Valid range header + let request = TestRequest::get() + .uri("/t%65st/tests/test.binary") + .header(header::RANGE, "bytes=10-20") + .to_request(); + let response = test::call_success(&mut srv, request); + + let contentlength = response + .headers() + .get(header::CONTENT_LENGTH) + .unwrap() + .to_str() + .unwrap(); + + assert_eq!(contentlength, "11"); + + // Invalid range header + let request = TestRequest::get() + .uri("/t%65st/tests/test.binary") + .header(header::RANGE, "bytes=10-8") + .to_request(); + let response = test::call_success(&mut srv, request); + assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); + + // Without range header + let request = TestRequest::get() + .uri("/t%65st/tests/test.binary") + // .no_default_headers() + .to_request(); + let response = test::call_success(&mut srv, request); + + let contentlength = response + .headers() + .get(header::CONTENT_LENGTH) + .unwrap() + .to_str() + .unwrap(); + + assert_eq!(contentlength, "100"); + + // chunked + let request = TestRequest::get() + .uri("/t%65st/tests/test.binary") + .to_request(); + let mut response = test::call_success(&mut srv, request); + + // with enabled compression + // { + // let te = response + // .headers() + // .get(header::TRANSFER_ENCODING) + // .unwrap() + // .to_str() + // .unwrap(); + // assert_eq!(te, "chunked"); + // } + + let bytes = + test::block_on(response.take_body().fold(BytesMut::new(), |mut b, c| { + b.extend(c); + Ok::<_, Error>(b) + })) + .unwrap(); + let data = Bytes::from(fs::read("tests/test.binary").unwrap()); + assert_eq!(bytes.freeze(), data); + } + + #[test] + fn test_static_files_with_spaces() { + let mut srv = test::init_service( + App::new() + .service(StaticFiles::new("/", ".").unwrap().index_file("Cargo.toml")), + ); + let request = TestRequest::get() + .uri("/tests/test%20space.binary") + .to_request(); + let mut response = test::call_success(&mut srv, request); + assert_eq!(response.status(), StatusCode::OK); + + let bytes = + test::block_on(response.take_body().fold(BytesMut::new(), |mut b, c| { + b.extend(c); + Ok::<_, Error>(b) + })) + .unwrap(); + + let data = Bytes::from(fs::read("tests/test space.binary").unwrap()); + assert_eq!(bytes.freeze(), data); + } + + #[derive(Default)] + pub struct OnlyMethodHeadConfig; + impl StaticFileConfig for OnlyMethodHeadConfig { + fn is_method_allowed(method: &Method) -> bool { + match *method { + Method::HEAD => true, + _ => false, + } + } + } + + #[test] + fn test_named_file_not_allowed() { + let file = + NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); + let req = TestRequest::default() + .method(Method::POST) + .to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + + let file = + NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); + let req = TestRequest::default().method(Method::PUT).to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + + let file = + NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); + let req = TestRequest::default().method(Method::GET).to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + } + + // #[test] + // fn test_named_file_content_encoding() { + // let req = TestRequest::default().method(Method::GET).finish(); + // let file = NamedFile::open("Cargo.toml").unwrap(); + + // assert!(file.encoding.is_none()); + // let resp = file + // .set_content_encoding(ContentEncoding::Identity) + // .respond_to(&req) + // .unwrap(); + + // assert!(resp.content_encoding().is_some()); + // assert_eq!(resp.content_encoding().unwrap().as_str(), "identity"); + // } + + #[test] + fn test_named_file_any_method() { + let req = TestRequest::default() + .method(Method::POST) + .to_http_request(); + let file = NamedFile::open("Cargo.toml").unwrap(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + } + + #[test] + fn test_static_files() { + let mut srv = test::init_service( + App::new().service(StaticFiles::new("/", ".").unwrap().show_files_listing()), + ); + let req = TestRequest::with_uri("/missing").to_request(); + + let resp = test::call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + let mut srv = + test::init_service(App::new().service(StaticFiles::new("/", ".").unwrap())); + + let req = TestRequest::default().to_request(); + let resp = test::call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + let mut srv = test::init_service( + App::new().service(StaticFiles::new("/", ".").unwrap().show_files_listing()), + ); + let req = TestRequest::with_uri("/tests").to_request(); + let mut resp = test::call_success(&mut srv, req); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "text/html; charset=utf-8" + ); + + let bytes = + test::block_on(resp.take_body().fold(BytesMut::new(), |mut b, c| { + b.extend(c); + Ok::<_, Error>(b) + })) + .unwrap(); + assert!(format!("{:?}", bytes).contains("/tests/test.png")); + } + + #[test] + fn test_static_files_bad_directory() { + let st: Result, Error> = StaticFiles::new("/", "missing"); + assert!(st.is_err()); + + let st: Result, Error> = StaticFiles::new("/", "Cargo.toml"); + assert!(st.is_err()); + } + + // #[test] + // fn test_default_handler_file_missing() { + // let st = StaticFiles::new(".") + // .unwrap() + // .default_handler(|_: &_| "default content"); + // let req = TestRequest::with_uri("/missing") + // .param("tail", "missing") + // .finish(); + + // let resp = st.handle(&req).respond_to(&req).unwrap(); + // let resp = resp.as_msg(); + // assert_eq!(resp.status(), StatusCode::OK); + // assert_eq!( + // resp.body(), + // &Body::Binary(Binary::Slice(b"default content")) + // ); + // } + + // #[test] + // fn test_serve_index() { + // let st = StaticFiles::new(".").unwrap().index_file("test.binary"); + // let req = TestRequest::default().uri("/tests").finish(); + + // let resp = st.handle(&req).respond_to(&req).unwrap(); + // let resp = resp.as_msg(); + // assert_eq!(resp.status(), StatusCode::OK); + // assert_eq!( + // resp.headers() + // .get(header::CONTENT_TYPE) + // .expect("content type"), + // "application/octet-stream" + // ); + // assert_eq!( + // resp.headers() + // .get(header::CONTENT_DISPOSITION) + // .expect("content disposition"), + // "attachment; filename=\"test.binary\"" + // ); + + // let req = TestRequest::default().uri("/tests/").finish(); + // let resp = st.handle(&req).respond_to(&req).unwrap(); + // let resp = resp.as_msg(); + // assert_eq!(resp.status(), StatusCode::OK); + // assert_eq!( + // resp.headers().get(header::CONTENT_TYPE).unwrap(), + // "application/octet-stream" + // ); + // assert_eq!( + // resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + // "attachment; filename=\"test.binary\"" + // ); + + // // nonexistent index file + // let req = TestRequest::default().uri("/tests/unknown").finish(); + // let resp = st.handle(&req).respond_to(&req).unwrap(); + // let resp = resp.as_msg(); + // assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + // let req = TestRequest::default().uri("/tests/unknown/").finish(); + // let resp = st.handle(&req).respond_to(&req).unwrap(); + // let resp = resp.as_msg(); + // assert_eq!(resp.status(), StatusCode::NOT_FOUND); + // } + + // #[test] + // fn test_serve_index_nested() { + // let st = StaticFiles::new(".").unwrap().index_file("mod.rs"); + // let req = TestRequest::default().uri("/src/client").finish(); + // let resp = st.handle(&req).respond_to(&req).unwrap(); + // let resp = resp.as_msg(); + // assert_eq!(resp.status(), StatusCode::OK); + // assert_eq!( + // resp.headers().get(header::CONTENT_TYPE).unwrap(), + // "text/x-rust" + // ); + // assert_eq!( + // resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + // "inline; filename=\"mod.rs\"" + // ); + // } + + // #[test] + // fn integration_serve_index() { + // let mut srv = test::TestServer::with_factory(|| { + // App::new().handler( + // "test", + // StaticFiles::new(".").unwrap().index_file("Cargo.toml"), + // ) + // }); + + // let request = srv.get().uri(srv.url("/test")).finish().unwrap(); + // let response = srv.execute(request.send()).unwrap(); + // assert_eq!(response.status(), StatusCode::OK); + // let bytes = srv.execute(response.body()).unwrap(); + // let data = Bytes::from(fs::read("Cargo.toml").unwrap()); + // assert_eq!(bytes, data); + + // let request = srv.get().uri(srv.url("/test/")).finish().unwrap(); + // let response = srv.execute(request.send()).unwrap(); + // assert_eq!(response.status(), StatusCode::OK); + // let bytes = srv.execute(response.body()).unwrap(); + // let data = Bytes::from(fs::read("Cargo.toml").unwrap()); + // assert_eq!(bytes, data); + + // // nonexistent index file + // let request = srv.get().uri(srv.url("/test/unknown")).finish().unwrap(); + // let response = srv.execute(request.send()).unwrap(); + // assert_eq!(response.status(), StatusCode::NOT_FOUND); + + // let request = srv.get().uri(srv.url("/test/unknown/")).finish().unwrap(); + // let response = srv.execute(request.send()).unwrap(); + // assert_eq!(response.status(), StatusCode::NOT_FOUND); + // } + + // #[test] + // fn integration_percent_encoded() { + // let mut srv = test::TestServer::with_factory(|| { + // App::new().handler( + // "test", + // StaticFiles::new(".").unwrap().index_file("Cargo.toml"), + // ) + // }); + + // let request = srv + // .get() + // .uri(srv.url("/test/%43argo.toml")) + // .finish() + // .unwrap(); + // let response = srv.execute(request.send()).unwrap(); + // assert_eq!(response.status(), StatusCode::OK); + // } + + struct T(&'static str, u64, Vec); + + #[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 + ) + } + } + } + } +} diff --git a/actix-staticfiles/src/named.rs b/actix-staticfiles/src/named.rs new file mode 100644 index 00000000..5fba0483 --- /dev/null +++ b/actix-staticfiles/src/named.rs @@ -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 { + path: PathBuf, + file: File, + pub(crate) content_type: mime::Mime, + pub(crate) content_disposition: header::ContentDisposition, + pub(crate) md: Metadata, + modified: Option, + encoding: Option, + pub(crate) status_code: StatusCode, + _cd_map: PhantomData, +} + +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>(file: File, path: P) -> io::Result { + 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>(path: P) -> io::Result { + Self::open_with_config(path, DefaultConfig) + } +} + +impl NamedFile { + /// 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>( + file: File, + path: P, + _: C, + ) -> io::Result> { + 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>( + path: P, + config: C, + ) -> io::Result> { + 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 { + // 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 { + self.modified.map(|mtime| mtime.into()) + } +} + +impl Deref for NamedFile { + type Target = File; + + fn deref(&self) -> &File { + &self.file + } +} + +impl DerefMut for NamedFile { + 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::() { + 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::() { + 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 Responder for NamedFile { + type Error = Error; + type Future = Result; + + 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)) + } + } +} diff --git a/actix-staticfiles/tests/test space.binary b/actix-staticfiles/tests/test space.binary new file mode 100644 index 00000000..ef8ff024 --- /dev/null +++ b/actix-staticfiles/tests/test space.binary @@ -0,0 +1 @@ +ÂTÇ‘É‚Vù2þvI ª–\ÇRË™–ˆæeÞvDØ:è—½¬RVÖYpíÿ;ÍÏGñùp!2÷CŒ.– û®õpA !ûߦÙx j+Uc÷±©X”c%Û;ï"yì­AI \ No newline at end of file diff --git a/actix-staticfiles/tests/test.binary b/actix-staticfiles/tests/test.binary new file mode 100644 index 00000000..ef8ff024 --- /dev/null +++ b/actix-staticfiles/tests/test.binary @@ -0,0 +1 @@ +ÂTÇ‘É‚Vù2þvI ª–\ÇRË™–ˆæeÞvDØ:è—½¬RVÖYpíÿ;ÍÏGñùp!2÷CŒ.– û®õpA !ûߦÙx j+Uc÷±©X”c%Û;ï"yì­AI \ No newline at end of file diff --git a/actix-staticfiles/tests/test.png b/actix-staticfiles/tests/test.png new file mode 100644 index 0000000000000000000000000000000000000000..6b7cdc0b8fb5a439d3bd19f210e89a37a2354e61 GIT binary patch literal 168 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1SFYWcSQjy&H|6fVg?3oVGw3ym^DWND9B#o z>Fdh=h((rL&%EBHDibIqn;8;O;+&tGo0?Yw &'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) diff --git a/src/app.rs b/src/app.rs index 27ca5c95..d503d8dd 100644 --- a/src/app.rs +++ b/src/app.rs @@ -24,8 +24,13 @@ type HttpService

    = BoxedService, ServiceResponse, ()>; type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; type BoxedResponse = Box>; -pub trait HttpServiceFactory { - type Factory: NewService; +pub trait HttpServiceFactory

    { + type Factory: NewService< + ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + >; fn rdef(&self) -> &ResourceDef; @@ -293,6 +298,29 @@ where } } + /// Register resource handler service. + pub fn service(self, service: F) -> AppRouter> + where + F: HttpServiceFactory

    + '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(mut self, rdef: R, factory: F) -> Self + pub fn service(mut self, factory: F) -> Self where - R: Into, - F: IntoNewService>, - U: NewService, Response = ServiceResponse, Error = ()> - + 'static, + F: HttpServiceFactory

    + '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 diff --git a/src/lib.rs b/src/lib.rs index f21c5e43..44dcde35 100644 --- a/src/lib.rs +++ b/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; diff --git a/src/service.rs b/src/service.rs index 7d17527a..c9666e31 100644 --- a/src/service.rs +++ b/src/service.rs @@ -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

    ServiceRequest

    { pub fn app_extensions(&self) -> &Extensions { self.req.app_extensions() } + + /// Deconstruct request into parts + pub fn into_parts(self) -> (HttpRequest, Payload

    ) { + (self.req, self.payload) + } } impl

    Resource for ServiceRequest

    { @@ -172,6 +178,29 @@ impl

    std::ops::DerefMut for ServiceRequest

    { } } +impl

    fmt::Debug for ServiceRequest

    { + 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

    { req: HttpRequest, payload: Payload

    , @@ -259,6 +288,16 @@ impl ServiceResponse { ServiceResponse { request, response } } + /// Create service response from the error + pub fn from_err>(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 ServiceResponse { } } } + + /// Extract response body + pub fn take_body(&mut self) -> ResponseBody { + self.response.take_body() + } } impl ServiceResponse { @@ -349,3 +393,21 @@ impl IntoFuture for ServiceResponse { ok(self) } } + +impl fmt::Debug for ServiceResponse { + 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 + } +} diff --git a/src/test.rs b/src/test.rs index 22bfe0c3..ccc4b38e 100644 --- a/src/test.rs +++ b/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(app: &mut S, req: R) -> S::Response +where + S: Service, 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. diff --git a/staticfiles/src/lib.rs b/staticfiles/src/lib.rs deleted file mode 100644 index c2ac5f3a..00000000 --- a/staticfiles/src/lib.rs +++ /dev/null @@ -1,2033 +0,0 @@ -//! Static files support -use std::cell::RefCell; -use std::fmt::Write; -use std::fs::{DirEntry, File, Metadata}; -use std::io::{Read, Seek}; -use std::marker::PhantomData; -use std::ops::{Deref, DerefMut}; -use std::path::{Path, PathBuf}; -use std::rc::Rc; -use std::time::{SystemTime, UNIX_EPOCH}; -use std::{cmp, io}; - -#[cfg(unix)] -use std::os::unix::fs::MetadataExt; - -use bytes::Bytes; -use derive_more::Display; -use futures::{Async, Future, Poll, Stream}; -use mime; -use mime_guess::{get_mime_type, guess_mime_type}; -use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; -use v_htmlescape::escape as escape_html_entity; - -use actix_http::error::{Error, ErrorInternalServerError, ResponseError}; -use actix_http::http::header::{ - self, ContentDisposition, DispositionParam, DispositionType, -}; -use actix_http::http::{ContentEncoding, Method, StatusCode}; -use actix_http::{HttpMessage, Response}; -use actix_service::boxed::BoxedNewService; -use actix_service::{NewService, Service}; -use actix_web::{ - blocking, FromRequest, HttpRequest, Responder, ServiceRequest, ServiceResponse, -}; -use futures::future::{err, ok, FutureResult}; - -type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; - -///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 {} - -/// Return the MIME type associated with a filename extension (case-insensitive). -/// If `ext` is empty or no associated type for the extension was found, returns -/// the type `application/octet-stream`. -#[inline] -pub fn file_extension_to_mime(ext: &str) -> mime::Mime { - get_mime_type(ext) -} - -/// A file with an associated name. -#[derive(Debug)] -pub struct NamedFile { - path: PathBuf, - file: File, - content_type: mime::Mime, - content_disposition: header::ContentDisposition, - md: Metadata, - modified: Option, - encoding: Option, - status_code: StatusCode, - _cd_map: PhantomData, -} - -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>(file: File, path: P) -> io::Result { - 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>(path: P) -> io::Result { - Self::open_with_config(path, DefaultConfig) - } -} - -impl NamedFile { - /// 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>( - file: File, - path: P, - _: C, - ) -> io::Result> { - 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>( - path: P, - config: C, - ) -> io::Result> { - 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 - } - - fn etag(&self) -> Option { - // This etag format is similar to Apache's. - self.modified.as_ref().map(|mtime| { - let ino = { - #[cfg(unix)] - { - self.md.ino() - } - #[cfg(not(unix))] - { - 0 - } - }; - - let dur = mtime - .duration_since(UNIX_EPOCH) - .expect("modification time must be after epoch"); - header::EntityTag::strong(format!( - "{:x}:{:x}:{:x}:{:x}", - ino, - self.md.len(), - dur.as_secs(), - dur.subsec_nanos() - )) - }) - } - - fn last_modified(&self) -> Option { - self.modified.map(|mtime| mtime.into()) - } -} - -impl Deref for NamedFile { - type Target = File; - - fn deref(&self) -> &File { - &self.file - } -} - -impl DerefMut for NamedFile { - 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::() { - 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::() { - 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 Responder for NamedFile { - type Error = Error; - type Future = FutureResult; - - fn respond_to(self, req: &HttpRequest) -> Self::Future { - if self.status_code != StatusCode::OK { - let mut resp = Response::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(Response::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 = Response::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)) - } - } -} - -#[doc(hidden)] -/// A helper created from a `std::fs::File` which reads the file -/// chunk-by-chunk on a `ThreadPool`. -pub struct ChunkedReadFile { - size: u64, - offset: u64, - file: Option, - fut: Option>, - counter: u64, -} - -fn handle_error(err: blocking::BlockingError) -> Error { - match err { - blocking::BlockingError::Error(err) => err.into(), - blocking::BlockingError::Canceled => { - ErrorInternalServerError("Unexpected error").into() - } - } -} - -impl Stream for ChunkedReadFile { - type Item = Bytes; - type Error = Error; - - fn poll(&mut self) -> Poll, Error> { - if self.fut.is_some() { - return match self.fut.as_mut().unwrap().poll().map_err(handle_error)? { - Async::Ready((file, bytes)) => { - self.fut.take(); - self.file = Some(file); - self.offset += bytes.len() as u64; - self.counter += bytes.len() as u64; - Ok(Async::Ready(Some(bytes))) - } - Async::NotReady => Ok(Async::NotReady), - }; - } - - let size = self.size; - let offset = self.offset; - let counter = self.counter; - - if size == counter { - Ok(Async::Ready(None)) - } else { - let mut file = self.file.take().expect("Use after completion"); - self.fut = Some(blocking::run(move || { - let max_bytes: usize; - max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; - let mut buf = Vec::with_capacity(max_bytes); - file.seek(io::SeekFrom::Start(offset))?; - let nbytes = - file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?; - if nbytes == 0 { - return Err(io::ErrorKind::UnexpectedEof.into()); - } - Ok((file, Bytes::from(buf))) - })); - self.poll() - } - } -} - -type DirectoryRenderer = - Fn(&Directory, &HttpRequest) -> Result; - -/// A directory; responds with the generated directory listing. -#[derive(Debug)] -pub struct Directory { - /// Base directory - pub base: PathBuf, - /// Path of subdirectory to generate listing for - pub path: PathBuf, -} - -impl Directory { - /// Create a new directory - pub fn new(base: PathBuf, path: PathBuf) -> Directory { - Directory { base, path } - } - - /// Is this entry visible from this directory? - pub fn is_visible(&self, entry: &io::Result) -> bool { - if let Ok(ref entry) = *entry { - if let Some(name) = entry.file_name().to_str() { - if name.starts_with('.') { - return false; - } - } - if let Ok(ref md) = entry.metadata() { - let ft = md.file_type(); - return ft.is_dir() || ft.is_file() || ft.is_symlink(); - } - } - false - } -} - -// show file url as relative to static path -macro_rules! encode_file_url { - ($path:ident) => { - utf8_percent_encode(&$path.to_string_lossy(), DEFAULT_ENCODE_SET) - }; -} - -// " -- " & -- & ' -- ' < -- < > -- > / -- / -macro_rules! encode_file_name { - ($entry:ident) => { - escape_html_entity(&$entry.file_name().to_string_lossy()) - }; -} - -fn directory_listing( - dir: &Directory, - req: &HttpRequest, -) -> Result { - let index_of = format!("Index of {}", req.path()); - let mut body = String::new(); - let base = Path::new(req.path()); - - for entry in dir.path.read_dir()? { - if dir.is_visible(&entry) { - let entry = entry.unwrap(); - let p = match entry.path().strip_prefix(&dir.path) { - Ok(p) => base.join(p), - Err(_) => continue, - }; - - // if file is a directory, add '/' to the end of the name - if let Ok(metadata) = entry.metadata() { - if metadata.is_dir() { - let _ = write!( - body, - "

  • {}/
  • ", - encode_file_url!(p), - encode_file_name!(entry), - ); - } else { - let _ = write!( - body, - "
  • {}
  • ", - encode_file_url!(p), - encode_file_name!(entry), - ); - } - } else { - continue; - } - } - } - - let html = format!( - "\ - {}\ -

    {}

    \ -
      \ - {}\ -
    \n", - index_of, index_of, body - ); - Ok(ServiceResponse::new( - req.clone(), - Response::Ok() - .content_type("text/html; charset=utf-8") - .body(html), - )) -} - -/// Static files handling -/// -/// `StaticFile` handler must be registered with `App::handler()` method, -/// because `StaticFile` handler requires access sub-path information. -/// -/// ```rust,ignore -/// # extern crate actix_web; -/// use actix_web::{fs, App}; -/// -/// fn main() { -/// let app = App::new() -/// .handler("/static", fs::StaticFiles::new(".").unwrap()) -/// .finish(); -/// } -/// ``` -pub struct StaticFiles { - directory: PathBuf, - index: Option, - show_index: bool, - default: Rc>>>>, - renderer: Rc, - _chunk_size: usize, - _follow_symlinks: bool, - _cd_map: PhantomData, -} - -impl StaticFiles { - /// Create new `StaticFiles` instance for specified base directory. - /// - /// `StaticFile` uses `ThreadPool` for blocking filesystem operations. - /// By default pool with 5x threads of available cpus is used. - /// Pool size can be changed by setting ACTIX_CPU_POOL environment variable. - pub fn new>(dir: T) -> Result, Error> { - Self::with_config(dir, DefaultConfig) - } -} - -impl StaticFiles { - /// Create new `StaticFiles` instance for specified base directory. - /// - /// Identical with `new` but allows to specify configiration to use. - pub fn with_config>( - dir: T, - _: C, - ) -> Result, Error> { - let dir = dir.into().canonicalize()?; - - if !dir.is_dir() { - return Err(StaticFilesError::IsNotDirectory.into()); - } - - Ok(StaticFiles { - directory: dir, - index: None, - show_index: false, - default: Rc::new(RefCell::new(None)), - renderer: Rc::new(directory_listing), - _chunk_size: 0, - _follow_symlinks: false, - _cd_map: PhantomData, - }) - } - - /// Show files listing for directories. - /// - /// By default show files listing is disabled. - pub fn show_files_listing(mut self) -> Self { - self.show_index = true; - self - } - - /// Set custom directory renderer - pub fn files_listing_renderer(mut self, f: F) -> Self - where - for<'r, 's> F: - Fn(&'r Directory, &'s HttpRequest) -> Result - + 'static, - { - self.renderer = Rc::new(f); - self - } - - /// Set index file - /// - /// Shows specific index file for directory "/" instead of - /// showing files listing. - pub fn index_file>(mut self, index: T) -> StaticFiles { - self.index = Some(index.into()); - self - } -} - -impl NewService for StaticFiles { - type Request = ServiceRequest; - type Response = ServiceResponse; - type Error = Error; - type Service = StaticFilesService; - type InitError = Error; - type Future = FutureResult; - - fn new_service(&self, _: &()) -> Self::Future { - ok(StaticFilesService { - directory: self.directory.clone(), - index: self.index.clone(), - show_index: self.show_index, - default: self.default.clone(), - renderer: self.renderer.clone(), - _chunk_size: self._chunk_size, - _follow_symlinks: self._follow_symlinks, - _cd_map: self._cd_map, - }) - } -} - -pub struct StaticFilesService { - directory: PathBuf, - index: Option, - show_index: bool, - default: Rc>>>>, - renderer: Rc, - _chunk_size: usize, - _follow_symlinks: bool, - _cd_map: PhantomData, -} - -impl Service for StaticFilesService { - type Request = ServiceRequest; - type Response = ServiceResponse; - type Error = Error; - type Future = FutureResult; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, req: Self::Request) -> Self::Future { - let mut req = req; - let real_path = match PathBufWrp::from_request(&mut req).poll() { - Ok(Async::Ready(item)) => item.0, - Ok(Async::NotReady) => unreachable!(), - Err(e) => return err(Error::from(e)), - }; - // full filepath - let path = match self.directory.join(&real_path).canonicalize() { - Ok(path) => path, - Err(e) => return err(Error::from(e)), - }; - - if path.is_dir() { - if let Some(ref redir_index) = self.index { - let path = path.join(redir_index); - - match NamedFile::open_with_config(path, C::default()) { - Ok(named_file) => match named_file.respond_to(&req).poll() { - Ok(Async::Ready(item)) => { - ok(ServiceResponse::new(req.clone(), item)) - } - Ok(Async::NotReady) => unreachable!(), - Err(e) => err(Error::from(e)), - }, - Err(e) => err(Error::from(e)), - } - } else if self.show_index { - let dir = Directory::new(self.directory.clone(), path); - let x = (self.renderer)(&dir, &req); - match x { - Ok(resp) => ok(resp), - Err(e) => err(Error::from(e)), - } - } else { - err(StaticFilesError::IsDirectory.into()) - } - } else { - match NamedFile::open_with_config(path, C::default()) { - Ok(named_file) => match named_file.respond_to(&req).poll() { - Ok(Async::Ready(item)) => { - ok(ServiceResponse::new(req.clone(), item)) - } - Ok(Async::NotReady) => unreachable!(), - Err(e) => err(Error::from(e)), - }, - Err(e) => err(Error::from(e)), - } - } - } -} - -struct PathBufWrp(PathBuf); - -impl

    FromRequest

    for PathBufWrp { - type Error = UriSegmentError; - type Future = FutureResult; - - fn from_request(req: &mut ServiceRequest

    ) -> Self::Future { - let path_str = req.match_info().path(); - let mut buf = PathBuf::new(); - for segment in path_str.split('/') { - if segment == ".." { - buf.pop(); - } else if segment.starts_with('.') { - return err(UriSegmentError::BadStart('.')); - } else if segment.starts_with('*') { - return err(UriSegmentError::BadStart('*')); - } else if segment.ends_with(':') { - return err(UriSegmentError::BadEnd(':')); - } else if segment.ends_with('>') { - return err(UriSegmentError::BadEnd('>')); - } else if segment.ends_with('<') { - return err(UriSegmentError::BadEnd('<')); - } else if segment.is_empty() { - continue; - } else if cfg!(windows) && segment.contains('\\') { - return err(UriSegmentError::BadChar('\\')); - } else { - buf.push(segment) - } - } - - ok(PathBufWrp(buf)) - } -} - -/// Errors which can occur when serving static files. -#[derive(Display, Debug, PartialEq)] -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) -> Response { - Response::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) -> Response { - Response::new(StatusCode::BAD_REQUEST) - } -} - -/// 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, ()> { - 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> = 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::>()?; - - let ranges: Vec = all_ranges.into_iter().filter_map(|x| x).collect(); - - if no_overlap && ranges.is_empty() { - return Err(()); - } - - Ok(ranges) - } -} - -// #[cfg(test)] -// mod tests { -// use std::fs; -// use std::ops::Add; -// use std::time::Duration; - -// use super::*; -// use application::App; -// use body::{Binary, Body}; -// use http::{header, Method, StatusCode}; -// use test::{self, TestRequest}; - -// #[test] -// fn test_file_extension_to_mime() { -// let m = file_extension_to_mime("jpg"); -// assert_eq!(m, mime::IMAGE_JPEG); - -// let m = file_extension_to_mime("invalid extension!!"); -// assert_eq!(m, mime::APPLICATION_OCTET_STREAM); - -// let m = file_extension_to_mime(""); -// assert_eq!(m, mime::APPLICATION_OCTET_STREAM); -// } - -// #[test] -// fn test_if_modified_since_without_if_none_match() { -// let mut file = NamedFile::open("Cargo.toml") -// .unwrap() -// .set_cpu_pool(CpuPool::new(1)); -// let since = -// header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); - -// let req = TestRequest::default() -// .header(header::IF_MODIFIED_SINCE, since) -// .finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!(resp.status(), StatusCode::NOT_MODIFIED); -// } - -// #[test] -// fn test_if_modified_since_with_if_none_match() { -// let mut file = NamedFile::open("Cargo.toml") -// .unwrap() -// .set_cpu_pool(CpuPool::new(1)); -// let since = -// header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); - -// let req = TestRequest::default() -// .header(header::IF_NONE_MATCH, "miss_etag") -// .header(header::IF_MODIFIED_SINCE, since) -// .finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_ne!(resp.status(), StatusCode::NOT_MODIFIED); -// } - -// #[test] -// fn test_named_file_text() { -// assert!(NamedFile::open("test--").is_err()); -// let mut file = NamedFile::open("Cargo.toml") -// .unwrap() -// .set_cpu_pool(CpuPool::new(1)); -// { -// file.file(); -// let _f: &File = &file; -// } -// { -// let _f: &mut File = &mut file; -// } - -// let req = TestRequest::default().finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "text/x-toml" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "inline; filename=\"Cargo.toml\"" -// ); -// } - -// #[test] -// fn test_named_file_set_content_type() { -// let mut file = NamedFile::open("Cargo.toml") -// .unwrap() -// .set_content_type(mime::TEXT_XML) -// .set_cpu_pool(CpuPool::new(1)); -// { -// file.file(); -// let _f: &File = &file; -// } -// { -// let _f: &mut File = &mut file; -// } - -// let req = TestRequest::default().finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "text/xml" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "inline; filename=\"Cargo.toml\"" -// ); -// } - -// #[test] -// fn test_named_file_image() { -// let mut file = NamedFile::open("tests/test.png") -// .unwrap() -// .set_cpu_pool(CpuPool::new(1)); -// { -// file.file(); -// let _f: &File = &file; -// } -// { -// let _f: &mut File = &mut file; -// } - -// let req = TestRequest::default().finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "image/png" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "inline; filename=\"test.png\"" -// ); -// } - -// #[test] -// fn test_named_file_image_attachment() { -// use header::{ContentDisposition, DispositionParam, DispositionType}; -// let cd = ContentDisposition { -// disposition: DispositionType::Attachment, -// parameters: vec![DispositionParam::Filename(String::from("test.png"))], -// }; -// let mut file = NamedFile::open("tests/test.png") -// .unwrap() -// .set_content_disposition(cd) -// .set_cpu_pool(CpuPool::new(1)); -// { -// file.file(); -// let _f: &File = &file; -// } -// { -// let _f: &mut File = &mut file; -// } - -// let req = TestRequest::default().finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "image/png" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "attachment; filename=\"test.png\"" -// ); -// } - -// #[derive(Default)] -// pub struct AllAttachmentConfig; -// impl StaticFileConfig for AllAttachmentConfig { -// fn content_disposition_map(_typ: mime::Name) -> DispositionType { -// DispositionType::Attachment -// } -// } - -// #[derive(Default)] -// pub struct AllInlineConfig; -// impl StaticFileConfig for AllInlineConfig { -// fn content_disposition_map(_typ: mime::Name) -> DispositionType { -// DispositionType::Inline -// } -// } - -// #[test] -// fn test_named_file_image_attachment_and_custom_config() { -// let file = NamedFile::open_with_config("tests/test.png", AllAttachmentConfig) -// .unwrap() -// .set_cpu_pool(CpuPool::new(1)); - -// let req = TestRequest::default().finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "image/png" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "attachment; filename=\"test.png\"" -// ); - -// let file = NamedFile::open_with_config("tests/test.png", AllInlineConfig) -// .unwrap() -// .set_cpu_pool(CpuPool::new(1)); - -// let req = TestRequest::default().finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "image/png" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "inline; filename=\"test.png\"" -// ); -// } - -// #[test] -// fn test_named_file_binary() { -// let mut file = NamedFile::open("tests/test.binary") -// .unwrap() -// .set_cpu_pool(CpuPool::new(1)); -// { -// file.file(); -// let _f: &File = &file; -// } -// { -// let _f: &mut File = &mut file; -// } - -// let req = TestRequest::default().finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "application/octet-stream" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "attachment; filename=\"test.binary\"" -// ); -// } - -// #[test] -// fn test_named_file_status_code_text() { -// let mut file = NamedFile::open("Cargo.toml") -// .unwrap() -// .set_status_code(StatusCode::NOT_FOUND) -// .set_cpu_pool(CpuPool::new(1)); -// { -// file.file(); -// let _f: &File = &file; -// } -// { -// let _f: &mut File = &mut file; -// } - -// let req = TestRequest::default().finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "text/x-toml" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "inline; filename=\"Cargo.toml\"" -// ); -// assert_eq!(resp.status(), StatusCode::NOT_FOUND); -// } - -// #[test] -// fn test_named_file_ranges_status_code() { -// let mut srv = test::TestServer::with_factory(|| { -// App::new().handler( -// "test", -// StaticFiles::new(".").unwrap().index_file("Cargo.toml"), -// ) -// }); - -// // Valid range header -// let request = srv -// .get() -// .uri(srv.url("/t%65st/Cargo.toml")) -// .header(header::RANGE, "bytes=10-20") -// .finish() -// .unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); - -// // Invalid range header -// let request = srv -// .get() -// .uri(srv.url("/t%65st/Cargo.toml")) -// .header(header::RANGE, "bytes=1-0") -// .finish() -// .unwrap(); -// let response = srv.execute(request.send()).unwrap(); - -// assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); -// } - -// #[test] -// fn test_named_file_content_range_headers() { -// let mut srv = test::TestServer::with_factory(|| { -// App::new().handler( -// "test", -// StaticFiles::new(".") -// .unwrap() -// .index_file("tests/test.binary"), -// ) -// }); - -// // Valid range header -// let request = srv -// .get() -// .uri(srv.url("/t%65st/tests/test.binary")) -// .header(header::RANGE, "bytes=10-20") -// .finish() -// .unwrap(); - -// let response = srv.execute(request.send()).unwrap(); - -// let contentrange = response -// .headers() -// .get(header::CONTENT_RANGE) -// .unwrap() -// .to_str() -// .unwrap(); - -// assert_eq!(contentrange, "bytes 10-20/100"); - -// // Invalid range header -// let request = srv -// .get() -// .uri(srv.url("/t%65st/tests/test.binary")) -// .header(header::RANGE, "bytes=10-5") -// .finish() -// .unwrap(); - -// let response = srv.execute(request.send()).unwrap(); - -// let contentrange = response -// .headers() -// .get(header::CONTENT_RANGE) -// .unwrap() -// .to_str() -// .unwrap(); - -// assert_eq!(contentrange, "bytes */100"); -// } - -// #[test] -// fn test_named_file_content_length_headers() { -// let mut srv = test::TestServer::with_factory(|| { -// App::new().handler( -// "test", -// StaticFiles::new(".") -// .unwrap() -// .index_file("tests/test.binary"), -// ) -// }); - -// // Valid range header -// let request = srv -// .get() -// .uri(srv.url("/t%65st/tests/test.binary")) -// .header(header::RANGE, "bytes=10-20") -// .finish() -// .unwrap(); - -// let response = srv.execute(request.send()).unwrap(); - -// let contentlength = response -// .headers() -// .get(header::CONTENT_LENGTH) -// .unwrap() -// .to_str() -// .unwrap(); - -// assert_eq!(contentlength, "11"); - -// // Invalid range header -// let request = srv -// .get() -// .uri(srv.url("/t%65st/tests/test.binary")) -// .header(header::RANGE, "bytes=10-8") -// .finish() -// .unwrap(); - -// let response = srv.execute(request.send()).unwrap(); - -// let contentlength = response -// .headers() -// .get(header::CONTENT_LENGTH) -// .unwrap() -// .to_str() -// .unwrap(); - -// assert_eq!(contentlength, "0"); - -// // Without range header -// let request = srv -// .get() -// .uri(srv.url("/t%65st/tests/test.binary")) -// .no_default_headers() -// .finish() -// .unwrap(); - -// let response = srv.execute(request.send()).unwrap(); - -// let contentlength = response -// .headers() -// .get(header::CONTENT_LENGTH) -// .unwrap() -// .to_str() -// .unwrap(); - -// assert_eq!(contentlength, "100"); - -// // chunked -// let request = srv -// .get() -// .uri(srv.url("/t%65st/tests/test.binary")) -// .finish() -// .unwrap(); - -// let response = srv.execute(request.send()).unwrap(); -// { -// let te = response -// .headers() -// .get(header::TRANSFER_ENCODING) -// .unwrap() -// .to_str() -// .unwrap(); -// assert_eq!(te, "chunked"); -// } -// let bytes = srv.execute(response.body()).unwrap(); -// let data = Bytes::from(fs::read("tests/test.binary").unwrap()); -// assert_eq!(bytes, data); -// } - -// #[test] -// fn test_static_files_with_spaces() { -// let mut srv = test::TestServer::with_factory(|| { -// App::new() -// .handler("/", StaticFiles::new(".").unwrap().index_file("Cargo.toml")) -// }); -// let request = srv -// .get() -// .uri(srv.url("/tests/test%20space.binary")) -// .finish() -// .unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::OK); - -// let bytes = srv.execute(response.body()).unwrap(); -// let data = Bytes::from(fs::read("tests/test space.binary").unwrap()); -// assert_eq!(bytes, data); -// } - -// #[derive(Default)] -// pub struct OnlyMethodHeadConfig; -// impl StaticFileConfig for OnlyMethodHeadConfig { -// fn is_method_allowed(method: &Method) -> bool { -// match *method { -// Method::HEAD => true, -// _ => false, -// } -// } -// } - -// #[test] -// fn test_named_file_not_allowed() { -// let file = -// NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); -// let req = TestRequest::default().method(Method::POST).finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - -// let file = -// NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); -// let req = TestRequest::default().method(Method::PUT).finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - -// let file = -// NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); -// let req = TestRequest::default().method(Method::GET).finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); -// } - -// #[test] -// fn test_named_file_content_encoding() { -// let req = TestRequest::default().method(Method::GET).finish(); -// let file = NamedFile::open("Cargo.toml").unwrap(); - -// assert!(file.encoding.is_none()); -// let resp = file -// .set_content_encoding(ContentEncoding::Identity) -// .respond_to(&req) -// .unwrap(); - -// assert!(resp.content_encoding().is_some()); -// assert_eq!(resp.content_encoding().unwrap().as_str(), "identity"); -// } - -// #[test] -// fn test_named_file_any_method() { -// let req = TestRequest::default().method(Method::POST).finish(); -// let file = NamedFile::open("Cargo.toml").unwrap(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!(resp.status(), StatusCode::OK); -// } - -// #[test] -// fn test_static_files() { -// let mut st = StaticFiles::new(".").unwrap().show_files_listing(); -// let req = TestRequest::with_uri("/missing") -// .param("tail", "missing") -// .finish(); -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!(resp.status(), StatusCode::NOT_FOUND); - -// st.show_index = false; -// let req = TestRequest::default().finish(); -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!(resp.status(), StatusCode::NOT_FOUND); - -// let req = TestRequest::default().param("tail", "").finish(); - -// st.show_index = true; -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "text/html; charset=utf-8" -// ); -// assert!(resp.body().is_binary()); -// assert!(format!("{:?}", resp.body()).contains("README.md")); -// } - -// #[test] -// fn test_static_files_bad_directory() { -// let st: Result, Error> = StaticFiles::new("missing"); -// assert!(st.is_err()); - -// let st: Result, Error> = StaticFiles::new("Cargo.toml"); -// assert!(st.is_err()); -// } - -// #[test] -// fn test_default_handler_file_missing() { -// let st = StaticFiles::new(".") -// .unwrap() -// .default_handler(|_: &_| "default content"); -// let req = TestRequest::with_uri("/missing") -// .param("tail", "missing") -// .finish(); - -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!(resp.status(), StatusCode::OK); -// assert_eq!( -// resp.body(), -// &Body::Binary(Binary::Slice(b"default content")) -// ); -// } - -// #[test] -// fn test_serve_index() { -// let st = StaticFiles::new(".").unwrap().index_file("test.binary"); -// let req = TestRequest::default().uri("/tests").finish(); - -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!(resp.status(), StatusCode::OK); -// assert_eq!( -// resp.headers() -// .get(header::CONTENT_TYPE) -// .expect("content type"), -// "application/octet-stream" -// ); -// assert_eq!( -// resp.headers() -// .get(header::CONTENT_DISPOSITION) -// .expect("content disposition"), -// "attachment; filename=\"test.binary\"" -// ); - -// let req = TestRequest::default().uri("/tests/").finish(); -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!(resp.status(), StatusCode::OK); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "application/octet-stream" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "attachment; filename=\"test.binary\"" -// ); - -// // nonexistent index file -// let req = TestRequest::default().uri("/tests/unknown").finish(); -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!(resp.status(), StatusCode::NOT_FOUND); - -// let req = TestRequest::default().uri("/tests/unknown/").finish(); -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!(resp.status(), StatusCode::NOT_FOUND); -// } - -// #[test] -// fn test_serve_index_nested() { -// let st = StaticFiles::new(".").unwrap().index_file("mod.rs"); -// let req = TestRequest::default().uri("/src/client").finish(); -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!(resp.status(), StatusCode::OK); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "text/x-rust" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "inline; filename=\"mod.rs\"" -// ); -// } - -// #[test] -// fn integration_serve_index_with_prefix() { -// let mut srv = test::TestServer::with_factory(|| { -// App::new() -// .prefix("public") -// .handler("/", StaticFiles::new(".").unwrap().index_file("Cargo.toml")) -// }); - -// let request = srv.get().uri(srv.url("/public")).finish().unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::OK); -// let bytes = srv.execute(response.body()).unwrap(); -// let data = Bytes::from(fs::read("Cargo.toml").unwrap()); -// assert_eq!(bytes, data); - -// let request = srv.get().uri(srv.url("/public/")).finish().unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::OK); -// let bytes = srv.execute(response.body()).unwrap(); -// let data = Bytes::from(fs::read("Cargo.toml").unwrap()); -// assert_eq!(bytes, data); -// } - -// #[test] -// fn integration_serve_index() { -// let mut srv = test::TestServer::with_factory(|| { -// App::new().handler( -// "test", -// StaticFiles::new(".").unwrap().index_file("Cargo.toml"), -// ) -// }); - -// let request = srv.get().uri(srv.url("/test")).finish().unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::OK); -// let bytes = srv.execute(response.body()).unwrap(); -// let data = Bytes::from(fs::read("Cargo.toml").unwrap()); -// assert_eq!(bytes, data); - -// let request = srv.get().uri(srv.url("/test/")).finish().unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::OK); -// let bytes = srv.execute(response.body()).unwrap(); -// let data = Bytes::from(fs::read("Cargo.toml").unwrap()); -// assert_eq!(bytes, data); - -// // nonexistent index file -// let request = srv.get().uri(srv.url("/test/unknown")).finish().unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::NOT_FOUND); - -// let request = srv.get().uri(srv.url("/test/unknown/")).finish().unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::NOT_FOUND); -// } - -// #[test] -// fn integration_percent_encoded() { -// let mut srv = test::TestServer::with_factory(|| { -// App::new().handler( -// "test", -// StaticFiles::new(".").unwrap().index_file("Cargo.toml"), -// ) -// }); - -// let request = srv -// .get() -// .uri(srv.url("/test/%43argo.toml")) -// .finish() -// .unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::OK); -// } - -// struct T(&'static str, u64, Vec); - -// #[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 -// ) -// } -// } -// } -// } -// }