From 1596893ef7275a2c6530e09abaec7e2cc2f0bf2c Mon Sep 17 00:00:00 2001 From: Silentdoer <1010993610@qq.com> Date: Sat, 19 Sep 2020 22:20:34 +0800 Subject: [PATCH 1/5] update actix-http dev-dependencies (#1696) Co-authored-by: luojinming --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 091e67dd7..d4da4af11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,7 +103,7 @@ tinyvec = { version = "1", features = ["alloc"] } [dev-dependencies] actix = "0.10.0" -actix-http = { version = "2.0.0-beta.4", features = ["actors"] } +actix-http = { version = "2.0.0", features = ["actors"] } rand = "0.7" env_logger = "0.7" serde_derive = "1.0" From f9e3f78e456e56dadaa70554d86b130e2655b148 Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Sun, 20 Sep 2020 19:21:53 +0300 Subject: [PATCH 2/5] eemove non-relevant comment from actix-http README.md (#1701) --- actix-http/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/actix-http/README.md b/actix-http/README.md index d4c96f2a7..96fc54d2e 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -13,12 +13,11 @@ Actix http ## Example ```rust -// see examples/framed_hello.rs for complete list of used crates. use std::{env, io}; use actix_http::{HttpService, Response}; use actix_server::Server; -use futures::future; +use futures_util::future; use http::header::HeaderValue; use log::info; From f7bcad95672940a71abdab9046c45b1ef8cb6c22 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sun, 20 Sep 2020 23:18:25 +0100 Subject: [PATCH 3/5] split up files lib (#1685) --- actix-files/Cargo.toml | 1 - actix-files/src/chunked.rs | 94 +++++ actix-files/src/directory.rs | 114 ++++++ actix-files/src/files.rs | 250 ++++++++++++ actix-files/src/lib.rs | 715 +++-------------------------------- actix-files/src/named.rs | 54 ++- actix-files/src/path_buf.rs | 99 +++++ actix-files/src/range.rs | 5 +- actix-files/src/service.rs | 167 ++++++++ 9 files changed, 825 insertions(+), 674 deletions(-) create mode 100644 actix-files/src/chunked.rs create mode 100644 actix-files/src/directory.rs create mode 100644 actix-files/src/files.rs create mode 100644 actix-files/src/path_buf.rs create mode 100644 actix-files/src/service.rs diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 8841f7fb1..634296c56 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -18,7 +18,6 @@ path = "src/lib.rs" [dependencies] actix-web = { version = "3.0.0", default-features = false } -actix-http = "2.0.0" actix-service = "1.0.6" bitflags = "1" bytes = "0.5.3" diff --git a/actix-files/src/chunked.rs b/actix-files/src/chunked.rs new file mode 100644 index 000000000..580b06787 --- /dev/null +++ b/actix-files/src/chunked.rs @@ -0,0 +1,94 @@ +use std::{ + cmp, fmt, + fs::File, + future::Future, + io::{self, Read, Seek}, + pin::Pin, + task::{Context, Poll}, +}; + +use actix_web::{ + error::{BlockingError, Error}, + web, +}; +use bytes::Bytes; +use futures_core::{ready, Stream}; +use futures_util::future::{FutureExt, LocalBoxFuture}; + +use crate::handle_error; + +type ChunkedBoxFuture = + LocalBoxFuture<'static, Result<(File, Bytes), BlockingError>>; + +#[doc(hidden)] +/// A helper created from a `std::fs::File` which reads the file +/// chunk-by-chunk on a `ThreadPool`. +pub struct ChunkedReadFile { + pub(crate) size: u64, + pub(crate) offset: u64, + pub(crate) file: Option, + pub(crate) fut: Option, + pub(crate) counter: u64, +} + +impl fmt::Debug for ChunkedReadFile { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("ChunkedReadFile") + } +} + +impl Stream for ChunkedReadFile { + type Item = Result; + + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + if let Some(ref mut fut) = self.fut { + return match ready!(Pin::new(fut).poll(cx)) { + Ok((file, bytes)) => { + self.fut.take(); + self.file = Some(file); + + self.offset += bytes.len() as u64; + self.counter += bytes.len() as u64; + + Poll::Ready(Some(Ok(bytes))) + } + Err(e) => Poll::Ready(Some(Err(handle_error(e)))), + }; + } + + let size = self.size; + let offset = self.offset; + let counter = self.counter; + + if size == counter { + Poll::Ready(None) + } else { + let mut file = self.file.take().expect("Use after completion"); + + self.fut = Some( + web::block(move || { + let 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 n_bytes = + file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?; + + if n_bytes == 0 { + return Err(io::ErrorKind::UnexpectedEof.into()); + } + + Ok((file, Bytes::from(buf))) + }) + .boxed_local(), + ); + + self.poll_next(cx) + } + } +} diff --git a/actix-files/src/directory.rs b/actix-files/src/directory.rs new file mode 100644 index 000000000..3717985d3 --- /dev/null +++ b/actix-files/src/directory.rs @@ -0,0 +1,114 @@ +use std::{fmt::Write, fs::DirEntry, io, path::Path, path::PathBuf}; + +use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse}; +use percent_encoding::{utf8_percent_encode, CONTROLS}; +use v_htmlescape::escape as escape_html_entity; + +/// 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 + } +} + +pub(crate) type DirectoryRenderer = + dyn Fn(&Directory, &HttpRequest) -> Result; + +// show file url as relative to static path +macro_rules! encode_file_url { + ($path:ident) => { + utf8_percent_encode(&$path, CONTROLS) + }; +} + +// " -- " & -- & ' -- ' < -- < > -- > / -- / +macro_rules! encode_file_name { + ($entry:ident) => { + escape_html_entity(&$entry.file_name().to_string_lossy()) + }; +} + +pub(crate) 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) if cfg!(windows) => { + base.join(p).to_string_lossy().replace("\\", "/") + } + Ok(p) => base.join(p).to_string_lossy().into_owned(), + 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), + )) +} diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs new file mode 100644 index 000000000..2b55e1aa9 --- /dev/null +++ b/actix-files/src/files.rs @@ -0,0 +1,250 @@ +use std::{cell::RefCell, fmt, io, path::PathBuf, rc::Rc}; + +use actix_service::{boxed, IntoServiceFactory, ServiceFactory}; +use actix_web::{ + dev::{ + AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse, + }, + error::Error, + guard::Guard, + http::header::DispositionType, + HttpRequest, +}; +use futures_util::future::{ok, FutureExt, LocalBoxFuture}; + +use crate::{ + directory_listing, named, Directory, DirectoryRenderer, FilesService, + HttpNewService, MimeOverride, +}; + +/// Static files handling service. +/// +/// `Files` service must be registered with `App::service()` method. +/// +/// ```rust +/// use actix_web::App; +/// use actix_files::Files; +/// +/// let app = App::new() +/// .service(Files::new("/static", ".")); +/// ``` +pub struct Files { + path: String, + directory: PathBuf, + index: Option, + show_index: bool, + redirect_to_slash: bool, + default: Rc>>>, + renderer: Rc, + mime_override: Option>, + file_flags: named::Flags, + guards: Option>, +} + +impl fmt::Debug for Files { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Files") + } +} + +impl Clone for Files { + fn clone(&self) -> Self { + Self { + directory: self.directory.clone(), + index: self.index.clone(), + show_index: self.show_index, + redirect_to_slash: self.redirect_to_slash, + default: self.default.clone(), + renderer: self.renderer.clone(), + file_flags: self.file_flags, + path: self.path.clone(), + mime_override: self.mime_override.clone(), + guards: self.guards.clone(), + } + } +} + +impl Files { + /// Create new `Files` instance for specified base directory. + /// + /// `File` 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_THREADPOOL environment variable. + pub fn new>(path: &str, dir: T) -> Files { + let orig_dir = dir.into(); + let dir = match orig_dir.canonicalize() { + Ok(canon_dir) => canon_dir, + Err(_) => { + log::error!("Specified path is not a directory: {:?}", orig_dir); + PathBuf::new() + } + }; + + Files { + path: path.to_string(), + directory: dir, + index: None, + show_index: false, + redirect_to_slash: false, + default: Rc::new(RefCell::new(None)), + renderer: Rc::new(directory_listing), + mime_override: None, + file_flags: named::Flags::default(), + guards: None, + } + } + + /// 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 + } + + /// Redirects to a slash-ended path when browsing a directory. + /// + /// By default never redirect. + pub fn redirect_to_slash_directory(mut self) -> Self { + self.redirect_to_slash = 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 + } + + /// Specifies mime override callback + pub fn mime_override(mut self, f: F) -> Self + where + F: Fn(&mime::Name<'_>) -> DispositionType + 'static, + { + self.mime_override = Some(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) -> Self { + self.index = Some(index.into()); + self + } + + #[inline] + /// Specifies whether to use ETag or not. + /// + /// Default is true. + pub fn use_etag(mut self, value: bool) -> Self { + self.file_flags.set(named::Flags::ETAG, value); + self + } + + #[inline] + /// Specifies whether to use Last-Modified or not. + /// + /// Default is true. + pub fn use_last_modified(mut self, value: bool) -> Self { + self.file_flags.set(named::Flags::LAST_MD, value); + self + } + + /// Specifies custom guards to use for directory listings and files. + /// + /// Default behaviour allows GET and HEAD. + #[inline] + pub fn use_guards(mut self, guards: G) -> Self { + self.guards = Some(Rc::new(guards)); + self + } + + /// Disable `Content-Disposition` header. + /// + /// By default Content-Disposition` header is enabled. + #[inline] + pub fn disable_content_disposition(mut self) -> Self { + self.file_flags.remove(named::Flags::CONTENT_DISPOSITION); + self + } + + /// Sets default handler which is used when no matched file could be found. + pub fn default_handler(mut self, f: F) -> Self + where + F: IntoServiceFactory, + U: ServiceFactory< + Config = (), + Request = ServiceRequest, + Response = ServiceResponse, + Error = Error, + > + 'static, + { + // create and configure default resource + self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::factory( + f.into_factory().map_init_err(|_| ()), + ))))); + + self + } +} + +impl HttpServiceFactory for Files { + fn register(self, config: &mut AppService) { + if self.default.borrow().is_none() { + *self.default.borrow_mut() = Some(config.default_service()); + } + + let rdef = if config.is_root() { + ResourceDef::root_prefix(&self.path) + } else { + ResourceDef::prefix(&self.path) + }; + + config.register_service(rdef, None, self, None) + } +} + +impl ServiceFactory for Files { + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = Error; + type Config = (); + type Service = FilesService; + type InitError = (); + type Future = LocalBoxFuture<'static, Result>; + + fn new_service(&self, _: ()) -> Self::Future { + let mut srv = FilesService { + directory: self.directory.clone(), + index: self.index.clone(), + show_index: self.show_index, + redirect_to_slash: self.redirect_to_slash, + default: None, + renderer: self.renderer.clone(), + mime_override: self.mime_override.clone(), + file_flags: self.file_flags, + guards: self.guards.clone(), + }; + + if let Some(ref default) = *self.default.borrow() { + default + .new_service(()) + .map(move |result| match result { + Ok(default) => { + srv.default = Some(default); + Ok(srv) + } + Err(_) => Err(()), + }) + .boxed_local() + } else { + ok(srv).boxed_local() + } + } +} diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 91c054947..1fc7cb3f3 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -1,44 +1,52 @@ -//! Static files support +//! Static files support for Actix Web. +//! +//! Provides a non-blocking service for serving static files from disk. +//! +//! # Example +//! ```rust +//! use actix_web::App; +//! use actix_files::Files; +//! +//! let app = App::new() +//! .service(Files::new("/static", ".")); +//! ``` +//! +//! # Implementation Quirks +//! - If a filename contains non-ascii characters, that file will be served with the `charset=utf-8` +//! extension on the Content-Type header. #![deny(rust_2018_idioms)] -#![allow(clippy::borrow_interior_mutable_const)] +#![warn(missing_docs, missing_debug_implementations)] -use std::cell::RefCell; -use std::fmt::Write; -use std::fs::{DirEntry, File}; -use std::future::Future; -use std::io::{Read, Seek}; -use std::path::{Path, PathBuf}; -use std::pin::Pin; -use std::rc::Rc; -use std::task::{Context, Poll}; -use std::{cmp, io}; +use std::io; -use actix_service::boxed::{self, BoxService, BoxServiceFactory}; -use actix_service::{IntoServiceFactory, Service, ServiceFactory}; -use actix_web::dev::{ - AppService, HttpServiceFactory, Payload, ResourceDef, ServiceRequest, - ServiceResponse, +use actix_service::boxed::{BoxService, BoxServiceFactory}; +use actix_web::{ + dev::{ServiceRequest, ServiceResponse}, + error::{BlockingError, Error, ErrorInternalServerError}, + http::header::DispositionType, }; -use actix_web::error::{BlockingError, Error, ErrorInternalServerError}; -use actix_web::guard::Guard; -use actix_web::http::header::{self, DispositionType}; -use actix_web::http::Method; -use actix_web::{web, FromRequest, HttpRequest, HttpResponse}; -use bytes::Bytes; -use futures_core::Stream; -use futures_util::future::{ok, ready, Either, FutureExt, LocalBoxFuture, Ready}; use mime_guess::from_ext; -use percent_encoding::{utf8_percent_encode, CONTROLS}; -use v_htmlescape::escape as escape_html_entity; +mod chunked; +mod directory; mod error; +mod files; mod named; +mod path_buf; mod range; +mod service; -use self::error::{FilesError, UriSegmentError}; +pub use crate::chunked::ChunkedReadFile; +pub use crate::directory::Directory; +pub use crate::files::Files; pub use crate::named::NamedFile; pub use crate::range::HttpRange; +pub use crate::service::FilesService; + +use self::directory::{directory_listing, DirectoryRenderer}; +use self::error::FilesError; +use self::path_buf::PathBufWrap; type HttpService = BoxService; type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; @@ -51,612 +59,37 @@ pub fn file_extension_to_mime(ext: &str) -> mime::Mime { from_ext(ext).first_or_octet_stream() } -fn handle_error(err: BlockingError) -> Error { +pub(crate) fn handle_error(err: BlockingError) -> Error { match err { BlockingError::Error(err) => err.into(), BlockingError::Canceled => ErrorInternalServerError("Unexpected error"), } } -#[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, - #[allow(clippy::type_complexity)] - fut: - Option>>>, - counter: u64, -} - -impl Stream for ChunkedReadFile { - type Item = Result; - - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - if let Some(ref mut fut) = self.fut { - return match Pin::new(fut).poll(cx) { - Poll::Ready(Ok((file, bytes))) => { - self.fut.take(); - self.file = Some(file); - self.offset += bytes.len() as u64; - self.counter += bytes.len() as u64; - Poll::Ready(Some(Ok(bytes))) - } - Poll::Ready(Err(e)) => Poll::Ready(Some(Err(handle_error(e)))), - Poll::Pending => Poll::Pending, - }; - } - - let size = self.size; - let offset = self.offset; - let counter = self.counter; - - if size == counter { - Poll::Ready(None) - } else { - let mut file = self.file.take().expect("Use after completion"); - self.fut = Some( - web::block(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))) - }) - .boxed_local(), - ); - self.poll_next(cx) - } - } -} - -type DirectoryRenderer = - dyn 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, CONTROLS) - }; -} - -// " -- " & -- & ' -- ' < -- < > -- > / -- / -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) if cfg!(windows) => { - base.join(p).to_string_lossy().replace("\\", "/") - } - Ok(p) => base.join(p).to_string_lossy().into_owned(), - 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), - )) -} type MimeOverride = dyn Fn(&mime::Name<'_>) -> DispositionType; -/// Static files handling -/// -/// `Files` service must be registered with `App::service()` method. -/// -/// ```rust -/// use actix_web::App; -/// use actix_files::Files; -/// -/// let app = App::new() -/// .service(Files::new("/static", ".")); -/// ``` -pub struct Files { - path: String, - directory: PathBuf, - index: Option, - show_index: bool, - redirect_to_slash: bool, - default: Rc>>>, - renderer: Rc, - mime_override: Option>, - file_flags: named::Flags, - // FIXME: Should re-visit later. - #[allow(clippy::redundant_allocation)] - guards: Option>>, -} - -impl Clone for Files { - fn clone(&self) -> Self { - Self { - directory: self.directory.clone(), - index: self.index.clone(), - show_index: self.show_index, - redirect_to_slash: self.redirect_to_slash, - default: self.default.clone(), - renderer: self.renderer.clone(), - file_flags: self.file_flags, - path: self.path.clone(), - mime_override: self.mime_override.clone(), - guards: self.guards.clone(), - } - } -} - -impl Files { - /// Create new `Files` instance for specified base directory. - /// - /// `File` 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_THREADPOOL environment variable. - pub fn new>(path: &str, dir: T) -> Files { - let orig_dir = dir.into(); - let dir = match orig_dir.canonicalize() { - Ok(canon_dir) => canon_dir, - Err(_) => { - log::error!("Specified path is not a directory: {:?}", orig_dir); - PathBuf::new() - } - }; - - Files { - path: path.to_string(), - directory: dir, - index: None, - show_index: false, - redirect_to_slash: false, - default: Rc::new(RefCell::new(None)), - renderer: Rc::new(directory_listing), - mime_override: None, - file_flags: named::Flags::default(), - guards: None, - } - } - - /// 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 - } - - /// Redirects to a slash-ended path when browsing a directory. - /// - /// By default never redirect. - pub fn redirect_to_slash_directory(mut self) -> Self { - self.redirect_to_slash = 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 - } - - /// Specifies mime override callback - pub fn mime_override(mut self, f: F) -> Self - where - F: Fn(&mime::Name<'_>) -> DispositionType + 'static, - { - self.mime_override = Some(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) -> Self { - self.index = Some(index.into()); - self - } - - #[inline] - /// Specifies whether to use ETag or not. - /// - /// Default is true. - pub fn use_etag(mut self, value: bool) -> Self { - self.file_flags.set(named::Flags::ETAG, value); - self - } - - #[inline] - /// Specifies whether to use Last-Modified or not. - /// - /// Default is true. - pub fn use_last_modified(mut self, value: bool) -> Self { - self.file_flags.set(named::Flags::LAST_MD, value); - self - } - - /// Specifies custom guards to use for directory listings and files. - /// - /// Default behaviour allows GET and HEAD. - #[inline] - pub fn use_guards(mut self, guards: G) -> Self { - self.guards = Some(Rc::new(Box::new(guards))); - self - } - - /// Disable `Content-Disposition` header. - /// - /// By default Content-Disposition` header is enabled. - #[inline] - pub fn disable_content_disposition(mut self) -> Self { - self.file_flags.remove(named::Flags::CONTENT_DISPOSITION); - self - } - - /// Sets default handler which is used when no matched file could be found. - pub fn default_handler(mut self, f: F) -> Self - where - F: IntoServiceFactory, - U: ServiceFactory< - Config = (), - Request = ServiceRequest, - Response = ServiceResponse, - Error = Error, - > + 'static, - { - // create and configure default resource - self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::factory( - f.into_factory().map_init_err(|_| ()), - ))))); - - self - } -} - -impl HttpServiceFactory for Files { - fn register(self, config: &mut AppService) { - if self.default.borrow().is_none() { - *self.default.borrow_mut() = Some(config.default_service()); - } - let rdef = if config.is_root() { - ResourceDef::root_prefix(&self.path) - } else { - ResourceDef::prefix(&self.path) - }; - config.register_service(rdef, None, self, None) - } -} - -impl ServiceFactory for Files { - type Request = ServiceRequest; - type Response = ServiceResponse; - type Error = Error; - type Config = (); - type Service = FilesService; - type InitError = (); - type Future = LocalBoxFuture<'static, Result>; - - fn new_service(&self, _: ()) -> Self::Future { - let mut srv = FilesService { - directory: self.directory.clone(), - index: self.index.clone(), - show_index: self.show_index, - redirect_to_slash: self.redirect_to_slash, - default: None, - renderer: self.renderer.clone(), - mime_override: self.mime_override.clone(), - file_flags: self.file_flags, - guards: self.guards.clone(), - }; - - if let Some(ref default) = *self.default.borrow() { - default - .new_service(()) - .map(move |result| match result { - Ok(default) => { - srv.default = Some(default); - Ok(srv) - } - Err(_) => Err(()), - }) - .boxed_local() - } else { - ok(srv).boxed_local() - } - } -} - -pub struct FilesService { - directory: PathBuf, - index: Option, - show_index: bool, - redirect_to_slash: bool, - default: Option, - renderer: Rc, - mime_override: Option>, - file_flags: named::Flags, - // FIXME: Should re-visit later. - #[allow(clippy::redundant_allocation)] - guards: Option>>, -} - -impl FilesService { - #[allow(clippy::type_complexity)] - fn handle_err( - &mut self, - e: io::Error, - req: ServiceRequest, - ) -> Either< - Ready>, - LocalBoxFuture<'static, Result>, - > { - log::debug!("Files: Failed to handle {}: {}", req.path(), e); - if let Some(ref mut default) = self.default { - Either::Right(default.call(req)) - } else { - Either::Left(ok(req.error_response(e))) - } - } -} - -impl Service for FilesService { - type Request = ServiceRequest; - type Response = ServiceResponse; - type Error = Error; - #[allow(clippy::type_complexity)] - type Future = Either< - Ready>, - LocalBoxFuture<'static, Result>, - >; - - fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, req: ServiceRequest) -> Self::Future { - let is_method_valid = if let Some(guard) = &self.guards { - // execute user defined guards - (**guard).check(req.head()) - } else { - // default behavior - matches!(*req.method(), Method::HEAD | Method::GET) - }; - - if !is_method_valid { - return Either::Left(ok(req.into_response( - actix_web::HttpResponse::MethodNotAllowed() - .header(header::CONTENT_TYPE, "text/plain") - .body("Request did not meet this resource's requirements."), - ))); - } - - let real_path = match PathBufWrp::get_pathbuf(req.match_info().path()) { - Ok(item) => item, - Err(e) => return Either::Left(ok(req.error_response(e))), - }; - - // full file path - let path = match self.directory.join(&real_path.0).canonicalize() { - Ok(path) => path, - Err(e) => return self.handle_err(e, req), - }; - - if path.is_dir() { - if let Some(ref redir_index) = self.index { - if self.redirect_to_slash && !req.path().ends_with('/') { - let redirect_to = format!("{}/", req.path()); - return Either::Left(ok(req.into_response( - HttpResponse::Found() - .header(header::LOCATION, redirect_to) - .body("") - .into_body(), - ))); - } - - let path = path.join(redir_index); - - match NamedFile::open(path) { - Ok(mut named_file) => { - if let Some(ref mime_override) = self.mime_override { - let new_disposition = - mime_override(&named_file.content_type.type_()); - named_file.content_disposition.disposition = new_disposition; - } - - named_file.flags = self.file_flags; - let (req, _) = req.into_parts(); - Either::Left(ok(match named_file.into_response(&req) { - Ok(item) => ServiceResponse::new(req, item), - Err(e) => ServiceResponse::from_err(e, req), - })) - } - Err(e) => self.handle_err(e, req), - } - } else if self.show_index { - let dir = Directory::new(self.directory.clone(), path); - let (req, _) = req.into_parts(); - let x = (self.renderer)(&dir, &req); - match x { - Ok(resp) => Either::Left(ok(resp)), - Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))), - } - } else { - Either::Left(ok(ServiceResponse::from_err( - FilesError::IsDirectory, - req.into_parts().0, - ))) - } - } else { - match NamedFile::open(path) { - Ok(mut named_file) => { - if let Some(ref mime_override) = self.mime_override { - let new_disposition = - mime_override(&named_file.content_type.type_()); - named_file.content_disposition.disposition = new_disposition; - } - - named_file.flags = self.file_flags; - let (req, _) = req.into_parts(); - match named_file.into_response(&req) { - Ok(item) => { - Either::Left(ok(ServiceResponse::new(req.clone(), item))) - } - Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))), - } - } - Err(e) => self.handle_err(e, req), - } - } - } -} - -#[derive(Debug)] -struct PathBufWrp(PathBuf); - -impl PathBufWrp { - fn get_pathbuf(path: &str) -> Result { - let mut buf = PathBuf::new(); - for segment in path.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 = Ready>; - type Config = (); - - fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { - ready(PathBufWrp::get_pathbuf(req.match_info().path())) - } -} - #[cfg(test)] mod tests { - use std::fs; - use std::iter::FromIterator; - use std::ops::Add; - use std::time::{Duration, SystemTime}; + use std::{ + fs::{self, File}, + ops::Add, + time::{Duration, SystemTime}, + }; + + use actix_service::ServiceFactory; + use actix_web::{ + guard, + http::{ + header::{self, ContentDisposition, DispositionParam, DispositionType}, + Method, StatusCode, + }, + middleware::Compress, + test::{self, TestRequest}, + web, App, HttpResponse, Responder, + }; + use futures_util::future::ok; use super::*; - use actix_web::guard; - use actix_web::http::header::{ - self, ContentDisposition, DispositionParam, DispositionType, - }; - use actix_web::http::{Method, StatusCode}; - use actix_web::middleware::Compress; - use actix_web::test::{self, TestRequest}; - use actix_web::{App, Responder}; #[actix_rt::test] async fn test_file_extension_to_mime() { @@ -1013,7 +446,7 @@ mod tests { // Check file contents let bytes = response.body().await.unwrap(); - let data = Bytes::from(fs::read("tests/test.binary").unwrap()); + let data = web::Bytes::from(fs::read("tests/test.binary").unwrap()); assert_eq!(bytes, data); } @@ -1046,7 +479,7 @@ mod tests { assert_eq!(response.status(), StatusCode::OK); let bytes = test::read_body(response).await; - let data = Bytes::from(fs::read("tests/test space.binary").unwrap()); + let data = web::Bytes::from(fs::read("tests/test space.binary").unwrap()); assert_eq!(bytes, data); } @@ -1224,7 +657,7 @@ mod tests { let resp = test::call_service(&mut st, req).await; assert_eq!(resp.status(), StatusCode::OK); let bytes = test::read_body(resp).await; - assert_eq!(bytes, Bytes::from_static(b"default content")); + assert_eq!(bytes, web::Bytes::from_static(b"default content")); } // #[actix_rt::test] @@ -1340,36 +773,4 @@ mod tests { // let response = srv.execute(request.send()).unwrap(); // assert_eq!(response.status(), StatusCode::OK); // } - - #[actix_rt::test] - async fn test_path_buf() { - assert_eq!( - PathBufWrp::get_pathbuf("/test/.tt").map(|t| t.0), - Err(UriSegmentError::BadStart('.')) - ); - assert_eq!( - PathBufWrp::get_pathbuf("/test/*tt").map(|t| t.0), - Err(UriSegmentError::BadStart('*')) - ); - assert_eq!( - PathBufWrp::get_pathbuf("/test/tt:").map(|t| t.0), - Err(UriSegmentError::BadEnd(':')) - ); - assert_eq!( - PathBufWrp::get_pathbuf("/test/tt<").map(|t| t.0), - Err(UriSegmentError::BadEnd('<')) - ); - assert_eq!( - PathBufWrp::get_pathbuf("/test/tt>").map(|t| t.0), - Err(UriSegmentError::BadEnd('>')) - ); - assert_eq!( - PathBufWrp::get_pathbuf("/seg1/seg2/").unwrap().0, - PathBuf::from_iter(vec!["seg1", "seg2"]) - ); - assert_eq!( - PathBufWrp::get_pathbuf("/seg1/../seg2/").unwrap().0, - PathBuf::from_iter(vec!["seg2"]) - ); - } } diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 12da722d2..3caa4a809 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -7,17 +7,20 @@ use std::time::{SystemTime, UNIX_EPOCH}; #[cfg(unix)] use std::os::unix::fs::MetadataExt; -use bitflags::bitflags; -use mime_guess::from_path; - -use actix_http::body::SizedStream; -use actix_web::dev::BodyEncoding; -use actix_web::http::header::{ - self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue, +use actix_web::{ + dev::{BodyEncoding, SizedStream}, + http::{ + header::{ + self, Charset, ContentDisposition, DispositionParam, DispositionType, + ExtendedValue, + }, + ContentEncoding, StatusCode, + }, + Error, HttpMessage, HttpRequest, HttpResponse, Responder, }; -use actix_web::http::{ContentEncoding, StatusCode}; -use actix_web::{Error, HttpMessage, HttpRequest, HttpResponse, Responder}; +use bitflags::bitflags; use futures_util::future::{ready, Ready}; +use mime_guess::from_path; use crate::range::HttpRange; use crate::ChunkedReadFile; @@ -93,8 +96,10 @@ impl NamedFile { mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, _ => DispositionType::Attachment, }; + let mut parameters = vec![DispositionParam::Filename(String::from(filename.as_ref()))]; + if !filename.is_ascii() { parameters.push(DispositionParam::FilenameExt(ExtendedValue { charset: Charset::Ext(String::from("UTF-8")), @@ -102,16 +107,19 @@ impl NamedFile { value: filename.into_owned().into_bytes(), })) } + let cd = ContentDisposition { disposition, parameters, }; + (ct, cd) }; let md = file.metadata()?; let modified = md.modified().ok(); let encoding = None; + Ok(NamedFile { path, file, @@ -242,6 +250,7 @@ impl NamedFile { let dur = mtime .duration_since(UNIX_EPOCH) .expect("modification time must be after epoch"); + header::EntityTag::strong(format!( "{:x}:{:x}:{:x}:{:x}", ino, @@ -256,9 +265,11 @@ impl NamedFile { self.modified.map(|mtime| mtime.into()) } + /// Creates an `HttpResponse` with file as a streaming body. pub fn into_response(self, req: &HttpRequest) -> Result { if self.status_code != StatusCode::OK { let mut resp = HttpResponse::build(self.status_code); + resp.set(header::ContentType(self.content_type.clone())) .if_true(self.flags.contains(Flags::CONTENT_DISPOSITION), |res| { res.header( @@ -266,9 +277,11 @@ impl NamedFile { self.content_disposition.to_string(), ); }); + if let Some(current_encoding) = self.encoding { resp.encoding(current_encoding); } + let reader = ChunkedReadFile { size: self.md.len(), offset: 0, @@ -276,6 +289,7 @@ impl NamedFile { fut: None, counter: 0, }; + return Ok(resp.streaming(reader)); } @@ -284,6 +298,7 @@ impl NamedFile { } else { None }; + let last_modified = if self.flags.contains(Flags::LAST_MD) { self.last_modified() } else { @@ -298,6 +313,7 @@ impl NamedFile { { let t1: SystemTime = m.clone().into(); let t2: SystemTime = since.clone().into(); + match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { (Ok(t1), Ok(t2)) => t1 > t2, _ => false, @@ -309,13 +325,14 @@ impl NamedFile { // check last modified let not_modified = if !none_match(etag.as_ref(), req) { true - } else if req.headers().contains_key(&header::IF_NONE_MATCH) { + } 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()) { let t1: SystemTime = m.clone().into(); let t2: SystemTime = since.clone().into(); + match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { (Ok(t1), Ok(t2)) => t1 <= t2, _ => false, @@ -332,6 +349,7 @@ impl NamedFile { self.content_disposition.to_string(), ); }); + // default compressing if let Some(current_encoding) = self.encoding { resp.encoding(current_encoding); @@ -350,11 +368,12 @@ impl NamedFile { 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; + if let Some(ranges) = req.headers().get(header::RANGE) { + if let Ok(ranges_header) = ranges.to_str() { + if let Ok(ranges) = HttpRange::parse(ranges_header, length) { + length = ranges[0].length; + offset = ranges[0].start; + resp.encoding(ContentEncoding::Identity); resp.header( header::CONTENT_RANGE, @@ -414,6 +433,7 @@ impl DerefMut for NamedFile { 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 { @@ -422,6 +442,7 @@ fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { } } } + false } } @@ -431,6 +452,7 @@ fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { 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 { @@ -439,8 +461,10 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { } } } + true } + None => true, } } diff --git a/actix-files/src/path_buf.rs b/actix-files/src/path_buf.rs new file mode 100644 index 000000000..2f3ae84d4 --- /dev/null +++ b/actix-files/src/path_buf.rs @@ -0,0 +1,99 @@ +use std::{ + path::{Path, PathBuf}, + str::FromStr, +}; + +use actix_web::{dev::Payload, FromRequest, HttpRequest}; +use futures_util::future::{ready, Ready}; + +use crate::error::UriSegmentError; + +#[derive(Debug)] +pub(crate) struct PathBufWrap(PathBuf); + +impl FromStr for PathBufWrap { + type Err = UriSegmentError; + + fn from_str(path: &str) -> Result { + let mut buf = PathBuf::new(); + + for segment in path.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(PathBufWrap(buf)) + } +} + +impl AsRef for PathBufWrap { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} + +impl FromRequest for PathBufWrap { + type Error = UriSegmentError; + type Future = Ready>; + type Config = (); + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + ready(req.match_info().path().parse()) + } +} + +#[cfg(test)] +mod tests { + use std::iter::FromIterator; + + use super::*; + + #[test] + fn test_path_buf() { + assert_eq!( + PathBufWrap::from_str("/test/.tt").map(|t| t.0), + Err(UriSegmentError::BadStart('.')) + ); + assert_eq!( + PathBufWrap::from_str("/test/*tt").map(|t| t.0), + Err(UriSegmentError::BadStart('*')) + ); + assert_eq!( + PathBufWrap::from_str("/test/tt:").map(|t| t.0), + Err(UriSegmentError::BadEnd(':')) + ); + assert_eq!( + PathBufWrap::from_str("/test/tt<").map(|t| t.0), + Err(UriSegmentError::BadEnd('<')) + ); + assert_eq!( + PathBufWrap::from_str("/test/tt>").map(|t| t.0), + Err(UriSegmentError::BadEnd('>')) + ); + assert_eq!( + PathBufWrap::from_str("/seg1/seg2/").unwrap().0, + PathBuf::from_iter(vec!["seg1", "seg2"]) + ); + assert_eq!( + PathBufWrap::from_str("/seg1/../seg2/").unwrap().0, + PathBuf::from_iter(vec!["seg2"]) + ); + } +} diff --git a/actix-files/src/range.rs b/actix-files/src/range.rs index 47673b0b0..e891ca7ec 100644 --- a/actix-files/src/range.rs +++ b/actix-files/src/range.rs @@ -1,11 +1,14 @@ /// HTTP Range header representation. #[derive(Debug, Clone, Copy)] pub struct HttpRange { + /// Start of range. pub start: u64, + + /// Length of range. pub length: u64, } -static PREFIX: &str = "bytes="; +const PREFIX: &str = "bytes="; const PREFIX_LEN: usize = 6; impl HttpRange { diff --git a/actix-files/src/service.rs b/actix-files/src/service.rs new file mode 100644 index 000000000..cbf4c2d3b --- /dev/null +++ b/actix-files/src/service.rs @@ -0,0 +1,167 @@ +use std::{ + fmt, io, + path::PathBuf, + rc::Rc, + task::{Context, Poll}, +}; + +use actix_service::Service; +use actix_web::{ + dev::{ServiceRequest, ServiceResponse}, + error::Error, + guard::Guard, + http::{header, Method}, + HttpResponse, +}; +use futures_util::future::{ok, Either, LocalBoxFuture, Ready}; + +use crate::{ + named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, + NamedFile, PathBufWrap, +}; + +/// Assembled file serving service. +pub struct FilesService { + pub(crate) directory: PathBuf, + pub(crate) index: Option, + pub(crate) show_index: bool, + pub(crate) redirect_to_slash: bool, + pub(crate) default: Option, + pub(crate) renderer: Rc, + pub(crate) mime_override: Option>, + pub(crate) file_flags: named::Flags, + pub(crate) guards: Option>, +} + +type FilesServiceFuture = Either< + Ready>, + LocalBoxFuture<'static, Result>, +>; + +impl FilesService { + fn handle_err(&mut self, e: io::Error, req: ServiceRequest) -> FilesServiceFuture { + log::debug!("Failed to handle {}: {}", req.path(), e); + + if let Some(ref mut default) = self.default { + Either::Right(default.call(req)) + } else { + Either::Left(ok(req.error_response(e))) + } + } +} + +impl fmt::Debug for FilesService { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("FilesService") + } +} + +impl Service for FilesService { + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = Error; + type Future = FilesServiceFuture; + + fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: ServiceRequest) -> Self::Future { + let is_method_valid = if let Some(guard) = &self.guards { + // execute user defined guards + (**guard).check(req.head()) + } else { + // default behavior + matches!(*req.method(), Method::HEAD | Method::GET) + }; + + if !is_method_valid { + return Either::Left(ok(req.into_response( + actix_web::HttpResponse::MethodNotAllowed() + .header(header::CONTENT_TYPE, "text/plain") + .body("Request did not meet this resource's requirements."), + ))); + } + + let real_path: PathBufWrap = match req.match_info().path().parse() { + Ok(item) => item, + Err(e) => return Either::Left(ok(req.error_response(e))), + }; + + // full file path + let path = match self.directory.join(&real_path).canonicalize() { + Ok(path) => path, + Err(e) => return self.handle_err(e, req), + }; + + if path.is_dir() { + if let Some(ref redir_index) = self.index { + if self.redirect_to_slash && !req.path().ends_with('/') { + let redirect_to = format!("{}/", req.path()); + + return Either::Left(ok(req.into_response( + HttpResponse::Found() + .header(header::LOCATION, redirect_to) + .body("") + .into_body(), + ))); + } + + let path = path.join(redir_index); + + match NamedFile::open(path) { + Ok(mut named_file) => { + if let Some(ref mime_override) = self.mime_override { + let new_disposition = + mime_override(&named_file.content_type.type_()); + named_file.content_disposition.disposition = new_disposition; + } + named_file.flags = self.file_flags; + + let (req, _) = req.into_parts(); + Either::Left(ok(match named_file.into_response(&req) { + Ok(item) => ServiceResponse::new(req, item), + Err(e) => ServiceResponse::from_err(e, req), + })) + } + Err(e) => self.handle_err(e, req), + } + } else if self.show_index { + let dir = Directory::new(self.directory.clone(), path); + + let (req, _) = req.into_parts(); + let x = (self.renderer)(&dir, &req); + + match x { + Ok(resp) => Either::Left(ok(resp)), + Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))), + } + } else { + Either::Left(ok(ServiceResponse::from_err( + FilesError::IsDirectory, + req.into_parts().0, + ))) + } + } else { + match NamedFile::open(path) { + Ok(mut named_file) => { + if let Some(ref mime_override) = self.mime_override { + let new_disposition = + mime_override(&named_file.content_type.type_()); + named_file.content_disposition.disposition = new_disposition; + } + named_file.flags = self.file_flags; + + let (req, _) = req.into_parts(); + match named_file.into_response(&req) { + Ok(item) => { + Either::Left(ok(ServiceResponse::new(req.clone(), item))) + } + Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))), + } + } + Err(e) => self.handle_err(e, req), + } + } + } +} From 162121bf8d51d497565961d73dde22ba1c36f3a4 Mon Sep 17 00:00:00 2001 From: Arniu Tseng Date: Wed, 23 Sep 2020 05:42:51 +0800 Subject: [PATCH 4/5] Unify route macros (#1705) --- actix-web-codegen/src/lib.rs | 230 +++++++----------- actix-web-codegen/src/route.rs | 159 ++++++------ .../route-duplicate-method-fail.stderr | 2 +- .../route-unexpected-method-fail.stderr | 2 +- .../tests/trybuild/simple-fail.rs | 5 + .../tests/trybuild/simple-fail.stderr | 6 + 6 files changed, 182 insertions(+), 222 deletions(-) diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 7ae6a26b1..62a1cc5fa 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -1,161 +1,103 @@ #![recursion_limit = "512"] -//! Helper and convenience macros for Actix-web. -//! -//! ## Runtime Setup -//! -//! - [main](attr.main.html) -//! -//! ## Resource Macros: -//! -//! - [get](attr.get.html) -//! - [post](attr.post.html) -//! - [put](attr.put.html) -//! - [delete](attr.delete.html) -//! - [head](attr.head.html) -//! - [connect](attr.connect.html) -//! - [options](attr.options.html) -//! - [trace](attr.trace.html) -//! - [patch](attr.patch.html) -//! -//! ### Attributes: -//! -//! - `"path"` - Raw literal string with path for which to register handle. Mandatory. -//! - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard` -//! - `wrap="Middleware"` - Registers a resource middleware. -//! -//! ### Notes -//! -//! Function name can be specified as any expression that is going to be accessible to the generate -//! code (e.g `my_guard` or `my_module::my_guard`) -//! -//! ### Example: -//! -//! ```rust -//! use actix_web::HttpResponse; -//! use actix_web_codegen::get; -//! -//! #[get("/test")] -//! async fn async_test() -> Result { -//! Ok(HttpResponse::Ok().finish()) -//! } -//! ``` - extern crate proc_macro; -mod route; - use proc_macro::TokenStream; -/// Creates route handler with `GET` method guard. -/// -/// Syntax: `#[get("path" [, attributes])]` -/// -/// ## Attributes: -/// -/// - `"path"` - Raw literal string with path for which to register handler. Mandatory. -/// - `guard = "function_name"` - Registers function as guard using `actix_web::guard::fn_guard` -/// - `wrap = "Middleware"` - Registers a resource middleware. -#[proc_macro_attribute] -pub fn get(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Get) -} - -/// Creates route handler with `POST` method guard. -/// -/// Syntax: `#[post("path" [, attributes])]` -/// -/// Attributes are the same as in [get](attr.get.html) -#[proc_macro_attribute] -pub fn post(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Post) -} - -/// Creates route handler with `PUT` method guard. -/// -/// Syntax: `#[put("path" [, attributes])]` -/// -/// Attributes are the same as in [get](attr.get.html) -#[proc_macro_attribute] -pub fn put(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Put) -} - -/// Creates route handler with `DELETE` method guard. -/// -/// Syntax: `#[delete("path" [, attributes])]` -/// -/// Attributes are the same as in [get](attr.get.html). -#[proc_macro_attribute] -pub fn delete(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Delete) -} - -/// Creates route handler with `HEAD` method guard. -/// -/// Syntax: `#[head("path" [, attributes])]` -/// -/// Attributes are the same as in [get](attr.get.html). -#[proc_macro_attribute] -pub fn head(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Head) -} - -/// Creates route handler with `CONNECT` method guard. -/// -/// Syntax: `#[connect("path" [, attributes])]` -/// -/// Attributes are the same as in [get](attr.get.html). -#[proc_macro_attribute] -pub fn connect(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Connect) -} - -/// Creates route handler with `OPTIONS` method guard. -/// -/// Syntax: `#[options("path" [, attributes])]` -/// -/// Attributes are the same as in [get](attr.get.html). -#[proc_macro_attribute] -pub fn options(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Options) -} - -/// Creates route handler with `TRACE` method guard. -/// -/// Syntax: `#[trace("path" [, attributes])]` -/// -/// Attributes are the same as in [get](attr.get.html). -#[proc_macro_attribute] -pub fn trace(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Trace) -} - -/// Creates route handler with `PATCH` method guard. -/// -/// Syntax: `#[patch("path" [, attributes])]` -/// -/// Attributes are the same as in [get](attr.get.html). -#[proc_macro_attribute] -pub fn patch(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Patch) -} +mod route; /// Creates resource handler, allowing multiple HTTP method guards. /// -/// Syntax: `#[route("path"[, attributes])]` +/// ## Syntax +/// ```text +/// #[route("path", method="HTTP_METHOD"[, attributes])] +/// ``` /// -/// Example: `#[route("/", method="GET", method="HEAD")]` -/// -/// ## Attributes -/// -/// - `"path"` - Raw literal string with path for which to register handler. Mandatory. -/// - `method="HTTP_METHOD"` - Registers HTTP method to provide guard for. +/// ### Attributes +/// - `"path"` - Raw literal string with path for which to register handler. +/// - `method="HTTP_METHOD"` - Registers HTTP method to provide guard for. Upper-case string, "GET", "POST" for example. /// - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard` /// - `wrap="Middleware"` - Registers a resource middleware. +/// +/// ### Notes +/// Function name can be specified as any expression that is going to be accessible to the generate +/// code, e.g `my_guard` or `my_module::my_guard`. +/// +/// ## Example +/// +/// ```rust +/// use actix_web::HttpResponse; +/// use actix_web_codegen::route; +/// +/// #[route("/", method="GET", method="HEAD")] +/// async fn example() -> HttpResponse { +/// HttpResponse::Ok().finish() +/// } +/// ``` #[proc_macro_attribute] pub fn route(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Multi) + route::with_method(None, args, input) +} + +macro_rules! doc_comment { + ($x:expr; $($tt:tt)*) => { + #[doc = $x] + $($tt)* + }; +} + +macro_rules! method_macro { + ( + $($variant:ident, $method:ident,)+ + ) => { + $(doc_comment! { +concat!(" +Creates route handler with `actix_web::guard::", stringify!($variant), "`. + +## Syntax +```text +#[", stringify!($method), r#"("path"[, attributes])] +``` + +### Attributes +- `"path"` - Raw literal string with path for which to register handler. +- `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard`. +- `wrap="Middleware"` - Registers a resource middleware. + +### Notes +Function name can be specified as any expression that is going to be accessible to the generate +code, e.g `my_guard` or `my_module::my_guard`. + +## Example + +```rust +use actix_web::HttpResponse; +use actix_web_codegen::"#, stringify!($method), "; + +#[", stringify!($method), r#"("/")] +async fn example() -> HttpResponse { + HttpResponse::Ok().finish() +} +``` +"#); + #[proc_macro_attribute] + pub fn $method(args: TokenStream, input: TokenStream) -> TokenStream { + route::with_method(Some(route::MethodType::$variant), args, input) + } + })+ + }; +} + +method_macro! { + Get, get, + Post, post, + Put, put, + Delete, delete, + Head, head, + Connect, connect, + Options, options, + Trace, trace, + Patch, patch, } /// Marks async main function as the actix system entry-point. diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index 394ced212..ddbd42454 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -20,63 +20,59 @@ impl ToTokens for ResourceType { } } -#[derive(Debug, PartialEq, Eq, Hash)] -pub enum GuardType { - Get, - Post, - Put, - Delete, - Head, - Connect, - Options, - Trace, - Patch, - Multi, -} - -impl GuardType { - fn as_str(&self) -> &'static str { - match self { - GuardType::Get => "Get", - GuardType::Post => "Post", - GuardType::Put => "Put", - GuardType::Delete => "Delete", - GuardType::Head => "Head", - GuardType::Connect => "Connect", - GuardType::Options => "Options", - GuardType::Trace => "Trace", - GuardType::Patch => "Patch", - GuardType::Multi => "Multi", +macro_rules! method_type { + ( + $($variant:ident, $upper:ident,)+ + ) => { + #[derive(Debug, PartialEq, Eq, Hash)] + pub enum MethodType { + $( + $variant, + )+ } - } + + impl MethodType { + fn as_str(&self) -> &'static str { + match self { + $(Self::$variant => stringify!($variant),)+ + } + } + + fn parse(method: &str) -> Result { + match method { + $(stringify!($upper) => Ok(Self::$variant),)+ + _ => Err(format!("Unexpected HTTP method: `{}`", method)), + } + } + } + }; } -impl ToTokens for GuardType { +method_type! { + Get, GET, + Post, POST, + Put, PUT, + Delete, DELETE, + Head, HEAD, + Connect, CONNECT, + Options, OPTIONS, + Trace, TRACE, + Patch, PATCH, +} + +impl ToTokens for MethodType { fn to_tokens(&self, stream: &mut TokenStream2) { let ident = Ident::new(self.as_str(), Span::call_site()); stream.append(ident); } } -impl TryFrom<&syn::LitStr> for GuardType { +impl TryFrom<&syn::LitStr> for MethodType { type Error = syn::Error; fn try_from(value: &syn::LitStr) -> Result { - match value.value().as_str() { - "CONNECT" => Ok(GuardType::Connect), - "DELETE" => Ok(GuardType::Delete), - "GET" => Ok(GuardType::Get), - "HEAD" => Ok(GuardType::Head), - "OPTIONS" => Ok(GuardType::Options), - "PATCH" => Ok(GuardType::Patch), - "POST" => Ok(GuardType::Post), - "PUT" => Ok(GuardType::Put), - "TRACE" => Ok(GuardType::Trace), - _ => Err(syn::Error::new_spanned( - value, - &format!("Unexpected HTTP Method: `{}`", value.value()), - )), - } + Self::parse(value.value().as_str()) + .map_err(|message| syn::Error::new_spanned(value, message)) } } @@ -84,15 +80,21 @@ struct Args { path: syn::LitStr, guards: Vec, wrappers: Vec, - methods: HashSet, + methods: HashSet, } impl Args { - fn new(args: AttributeArgs) -> syn::Result { + fn new(args: AttributeArgs, method: Option) -> syn::Result { let mut path = None; let mut guards = Vec::new(); let mut wrappers = Vec::new(); let mut methods = HashSet::new(); + + let is_route_macro = method.is_none(); + if let Some(method) = method { + methods.insert(method); + } + for arg in args { match arg { NestedMeta::Lit(syn::Lit::Str(lit)) => match path { @@ -126,13 +128,18 @@ impl Args { )); } } else if nv.path.is_ident("method") { - if let syn::Lit::Str(ref lit) = nv.lit { - let guard = GuardType::try_from(lit)?; - if !methods.insert(guard) { + if !is_route_macro { + return Err(syn::Error::new_spanned( + &nv, + "HTTP method forbidden here. To handle multiple methods, use `route` instead", + )); + } else if let syn::Lit::Str(ref lit) = nv.lit { + let method = MethodType::try_from(lit)?; + if !methods.insert(method) { return Err(syn::Error::new_spanned( &nv.lit, &format!( - "HTTP Method defined more than once: `{}`", + "HTTP method defined more than once: `{}`", lit.value() ), )); @@ -169,7 +176,6 @@ pub struct Route { args: Args, ast: syn::ItemFn, resource_type: ResourceType, - guard: GuardType, } fn guess_resource_type(typ: &syn::Type) -> ResourceType { @@ -198,23 +204,25 @@ impl Route { pub fn new( args: AttributeArgs, input: TokenStream, - guard: GuardType, + method: Option, ) -> syn::Result { if args.is_empty() { return Err(syn::Error::new( Span::call_site(), format!( - r#"invalid server definition, expected #[{}("")]"#, - guard.as_str().to_ascii_lowercase() + r#"invalid service definition, expected #[{}("")]"#, + method + .map(|it| it.as_str()) + .unwrap_or("route") + .to_ascii_lowercase() ), )); } let ast: syn::ItemFn = syn::parse(input)?; let name = ast.sig.ident.clone(); - let args = Args::new(args)?; - - if guard == GuardType::Multi && args.methods.is_empty() { + let args = Args::new(args, method)?; + if args.methods.is_empty() { return Err(syn::Error::new( Span::call_site(), "The #[route(..)] macro requires at least one `method` attribute", @@ -240,7 +248,6 @@ impl Route { args, ast, resource_type, - guard, }) } } @@ -249,7 +256,6 @@ impl ToTokens for Route { fn to_tokens(&self, output: &mut TokenStream2) { let Self { name, - guard, ast, args: Args { @@ -261,21 +267,22 @@ impl ToTokens for Route { resource_type, } = self; let resource_name = name.to_string(); - let mut methods = methods.iter(); - - let method_guards = if *guard == GuardType::Multi { + let method_guards = { + let mut others = methods.iter(); // unwrapping since length is checked to be at least one - let first = methods.next().unwrap(); + let first = others.next().unwrap(); - quote! { - .guard( - actix_web::guard::Any(actix_web::guard::#first()) - #(.or(actix_web::guard::#methods()))* - ) - } - } else { - quote! { - .guard(actix_web::guard::#guard()) + if methods.len() > 1 { + quote! { + .guard( + actix_web::guard::Any(actix_web::guard::#first()) + #(.or(actix_web::guard::#others()))* + ) + } + } else { + quote! { + .guard(actix_web::guard::#first()) + } } }; @@ -302,13 +309,13 @@ impl ToTokens for Route { } } -pub(crate) fn generate( +pub(crate) fn with_method( + method: Option, args: TokenStream, input: TokenStream, - guard: GuardType, ) -> TokenStream { let args = parse_macro_input!(args as syn::AttributeArgs); - match Route::new(args, input, guard) { + match Route::new(args, input, method) { Ok(route) => route.into_token_stream().into(), Err(err) => err.to_compile_error().into(), } diff --git a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr index 8bf857c4d..613054de5 100644 --- a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr @@ -1,4 +1,4 @@ -error: HTTP Method defined more than once: `GET` +error: HTTP method defined more than once: `GET` --> $DIR/route-duplicate-method-fail.rs:3:35 | 3 | #[route("/", method="GET", method="GET")] diff --git a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr index 3fe49f774..fe17fdf12 100644 --- a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr @@ -1,4 +1,4 @@ -error: Unexpected HTTP Method: `UNEXPECTED` +error: Unexpected HTTP method: `UNEXPECTED` --> $DIR/route-unexpected-method-fail.rs:3:21 | 3 | #[route("/", method="UNEXPECTED")] diff --git a/actix-web-codegen/tests/trybuild/simple-fail.rs b/actix-web-codegen/tests/trybuild/simple-fail.rs index 140497687..368cff046 100644 --- a/actix-web-codegen/tests/trybuild/simple-fail.rs +++ b/actix-web-codegen/tests/trybuild/simple-fail.rs @@ -22,4 +22,9 @@ async fn four() -> impl Responder { HttpResponse::Ok() } +#[delete("/five", method="GET")] +async fn five() -> impl Responder { + HttpResponse::Ok() +} + fn main() {} diff --git a/actix-web-codegen/tests/trybuild/simple-fail.stderr b/actix-web-codegen/tests/trybuild/simple-fail.stderr index 12c32c00d..cffc81ff8 100644 --- a/actix-web-codegen/tests/trybuild/simple-fail.stderr +++ b/actix-web-codegen/tests/trybuild/simple-fail.stderr @@ -21,3 +21,9 @@ error: Multiple paths specified! Should be only one! | 20 | #[delete("/four", "/five")] | ^^^^^^^ + +error: HTTP method forbidden here. To handle multiple methods, use `route` instead + --> $DIR/simple-fail.rs:25:19 + | +25 | #[delete("/five", method="GET")] + | ^^^^^^^^^^^^ From c53e9468bc1d977afb3514fe7f8239ed9deb2e68 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 24 Sep 2020 23:54:01 +0100 Subject: [PATCH 5/5] prepare codegen 0.4.0 release (#1702) --- actix-web-codegen/CHANGES.md | 3 + actix-web-codegen/Cargo.toml | 4 +- actix-web-codegen/src/lib.rs | 97 +++++++++++++++---- .../trybuild/route-duplicate-method-fail.rs | 8 +- .../route-duplicate-method-fail.stderr | 4 +- .../route-missing-method-fail-msrv.rs | 16 +-- .../route-missing-method-fail-msrv.stderr | 4 +- .../trybuild/route-missing-method-fail.rs | 8 +- .../trybuild/route-missing-method-fail.stderr | 4 +- actix-web-codegen/tests/trybuild/route-ok.rs | 8 +- .../trybuild/route-unexpected-method-fail.rs | 8 +- .../route-unexpected-method-fail.stderr | 4 +- .../tests/trybuild/simple-fail.rs | 22 ++--- actix-web-codegen/tests/trybuild/simple.rs | 3 +- 14 files changed, 124 insertions(+), 69 deletions(-) mode change 100644 => 120000 actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.rs diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index 793864d43..ad1a22b88 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2020-xx-xx + + +## 0.4.0 - 2020-09-20 * Added compile success and failure testing. [#1677] * Add `route` macro for supporting multiple HTTP methods guards. diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index da37b8de6..fd99a8376 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-codegen" -version = "0.3.0" +version = "0.4.0" description = "Actix web proc macros" readme = "README.md" homepage = "https://actix.rs" @@ -19,7 +19,7 @@ syn = { version = "1", features = ["full", "parsing"] } proc-macro2 = "1" [dev-dependencies] -actix-rt = "1.0.0" +actix-rt = "1.1.1" actix-web = "3.0.0" futures-util = { version = "0.3.5", default-features = false } trybuild = "1" diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 62a1cc5fa..af2bc7f18 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -1,6 +1,64 @@ -#![recursion_limit = "512"] +//! Macros for reducing boilerplate code in Actix Web applications. +//! +//! ## Actix Web Re-exports +//! Actix Web re-exports a version of this crate in it's entirety so you usually don't have to +//! specify a dependency on this crate explicitly. Sometimes, however, updates are made to this +//! crate before the actix-web dependency is updated. Therefore, code examples here will show +//! explicit imports. Check the latest [actix-web attributes docs] to see which macros +//! are re-exported. +//! +//! # Runtime Setup +//! Used for setting up the actix async runtime. See [main] macro docs. +//! +//! ```rust +//! #[actix_web_codegen::main] // or `#[actix_web::main]` in Actix Web apps +//! async fn main() { +//! async { println!("Hello world"); }.await +//! } +//! ``` +//! +//! # Single Method Handler +//! There is a macro to set up a handler for each of the most common HTTP methods that also define +//! additional guards and route-specific middleware. +//! +//! See docs for: [GET], [POST], [PATCH], [PUT], [DELETE], [HEAD], [CONNECT], [OPTIONS], [TRACE] +//! +//! ```rust +//! # use actix_web::HttpResponse; +//! # use actix_web_codegen::get; +//! #[get("/test")] +//! async fn get_handler() -> HttpResponse { +//! HttpResponse::Ok().finish() +//! } +//! ``` +//! +//! # Multiple Method Handlers +//! Similar to the single method handler macro but takes one or more arguments for the HTTP methods +//! it should respond to. See [route] macro docs. +//! +//! ```rust +//! # use actix_web::HttpResponse; +//! # use actix_web_codegen::route; +//! #[route("/test", method="GET", method="HEAD")] +//! async fn get_and_head_handler() -> HttpResponse { +//! HttpResponse::Ok().finish() +//! } +//! ``` +//! +//! [actix-web attributes docs]: https://docs.rs/actix-web/*/actix_web/#attributes +//! [main]: attr.main.html +//! [route]: attr.route.html +//! [GET]: attr.get.html +//! [POST]: attr.post.html +//! [PUT]: attr.put.html +//! [DELETE]: attr.delete.html +//! [HEAD]: attr.head.html +//! [CONNECT]: attr.connect.html +//! [OPTIONS]: attr.options.html +//! [TRACE]: attr.trace.html +//! [PATCH]: attr.patch.html -extern crate proc_macro; +#![recursion_limit = "512"] use proc_macro::TokenStream; @@ -8,28 +66,27 @@ mod route; /// Creates resource handler, allowing multiple HTTP method guards. /// -/// ## Syntax +/// # Syntax /// ```text /// #[route("path", method="HTTP_METHOD"[, attributes])] /// ``` /// -/// ### Attributes +/// # Attributes /// - `"path"` - Raw literal string with path for which to register handler. /// - `method="HTTP_METHOD"` - Registers HTTP method to provide guard for. Upper-case string, "GET", "POST" for example. /// - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard` /// - `wrap="Middleware"` - Registers a resource middleware. /// -/// ### Notes +/// # Notes /// Function name can be specified as any expression that is going to be accessible to the generate /// code, e.g `my_guard` or `my_module::my_guard`. /// -/// ## Example +/// # Example /// /// ```rust -/// use actix_web::HttpResponse; -/// use actix_web_codegen::route; -/// -/// #[route("/", method="GET", method="HEAD")] +/// # use actix_web::HttpResponse; +/// # use actix_web_codegen::route; +/// #[route("/test", method="GET", method="HEAD")] /// async fn example() -> HttpResponse { /// HttpResponse::Ok().finish() /// } @@ -54,26 +111,25 @@ macro_rules! method_macro { concat!(" Creates route handler with `actix_web::guard::", stringify!($variant), "`. -## Syntax +# Syntax ```text #[", stringify!($method), r#"("path"[, attributes])] ``` -### Attributes +# Attributes - `"path"` - Raw literal string with path for which to register handler. - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard`. - `wrap="Middleware"` - Registers a resource middleware. -### Notes +# Notes Function name can be specified as any expression that is going to be accessible to the generate code, e.g `my_guard` or `my_module::my_guard`. -## Example +# Example ```rust -use actix_web::HttpResponse; -use actix_web_codegen::"#, stringify!($method), "; - +# use actix_web::HttpResponse; +# use actix_web_codegen::"#, stringify!($method), "; #[", stringify!($method), r#"("/")] async fn example() -> HttpResponse { HttpResponse::Ok().finish() @@ -102,16 +158,17 @@ method_macro! { /// Marks async main function as the actix system entry-point. /// -/// ## Usage +/// # Actix Web Re-export +/// This macro can be applied with `#[actix_web::main]` when used in Actix Web applications. /// +/// # Usage /// ```rust -/// #[actix_web::main] +/// #[actix_web_codegen::main] /// async fn main() { /// async { println!("Hello world"); }.await /// } /// ``` #[proc_macro_attribute] -#[cfg(not(test))] // Work around for rust-lang/rust#62127 pub fn main(_: TokenStream, item: TokenStream) -> TokenStream { use quote::quote; diff --git a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.rs b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.rs index 9ce980251..9a38050f7 100644 --- a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.rs +++ b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.rs @@ -1,12 +1,14 @@ -use actix_web::*; +use actix_web_codegen::*; #[route("/", method="GET", method="GET")] -async fn index() -> impl Responder { - HttpResponse::Ok() +async fn index() -> String { + "Hello World!".to_owned() } #[actix_web::main] async fn main() { + use actix_web::{App, test}; + let srv = test::start(|| App::new().service(index)); let request = srv.get("/"); diff --git a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr index 613054de5..f3eda68af 100644 --- a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr @@ -5,7 +5,7 @@ error: HTTP method defined more than once: `GET` | ^^^^^ error[E0425]: cannot find value `index` in this scope - --> $DIR/route-duplicate-method-fail.rs:10:49 + --> $DIR/route-duplicate-method-fail.rs:12:49 | -10 | let srv = test::start(|| App::new().service(index)); +12 | let srv = test::start(|| App::new().service(index)); | ^^^^^ not found in this scope diff --git a/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.rs b/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.rs deleted file mode 100644 index 5c30b57ce..000000000 --- a/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.rs +++ /dev/null @@ -1,15 +0,0 @@ -use actix_web::*; - -#[route("/")] -async fn index() -> impl Responder { - HttpResponse::Ok() -} - -#[actix_web::main] -async fn main() { - let srv = test::start(|| App::new().service(index)); - - let request = srv.get("/"); - let response = request.send().await.unwrap(); - assert!(response.status().is_success()); -} diff --git a/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.rs b/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.rs new file mode 120000 index 000000000..70a5c0e33 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.rs @@ -0,0 +1 @@ +route-missing-method-fail.rs \ No newline at end of file diff --git a/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.stderr b/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.stderr index f59f6c27e..d3e2b60ae 100644 --- a/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.stderr +++ b/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.stderr @@ -5,7 +5,7 @@ error: The #[route(..)] macro requires at least one `method` attribute | ^^^^^^^^^^^^^ error[E0425]: cannot find value `index` in this scope - --> $DIR/route-missing-method-fail-msrv.rs:10:49 + --> $DIR/route-missing-method-fail-msrv.rs:12:49 | -10 | let srv = test::start(|| App::new().service(index)); +12 | let srv = test::start(|| App::new().service(index)); | ^^^^^ not found in this scope diff --git a/actix-web-codegen/tests/trybuild/route-missing-method-fail.rs b/actix-web-codegen/tests/trybuild/route-missing-method-fail.rs index 5c30b57ce..ce87a55a4 100644 --- a/actix-web-codegen/tests/trybuild/route-missing-method-fail.rs +++ b/actix-web-codegen/tests/trybuild/route-missing-method-fail.rs @@ -1,12 +1,14 @@ -use actix_web::*; +use actix_web_codegen::*; #[route("/")] -async fn index() -> impl Responder { - HttpResponse::Ok() +async fn index() -> String { + "Hello World!".to_owned() } #[actix_web::main] async fn main() { + use actix_web::{App, test}; + let srv = test::start(|| App::new().service(index)); let request = srv.get("/"); diff --git a/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr index 6d35ea600..0518a61ed 100644 --- a/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr @@ -7,7 +7,7 @@ error: The #[route(..)] macro requires at least one `method` attribute = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) error[E0425]: cannot find value `index` in this scope - --> $DIR/route-missing-method-fail.rs:10:49 + --> $DIR/route-missing-method-fail.rs:12:49 | -10 | let srv = test::start(|| App::new().service(index)); +12 | let srv = test::start(|| App::new().service(index)); | ^^^^^ not found in this scope diff --git a/actix-web-codegen/tests/trybuild/route-ok.rs b/actix-web-codegen/tests/trybuild/route-ok.rs index bfac56e12..c4f679604 100644 --- a/actix-web-codegen/tests/trybuild/route-ok.rs +++ b/actix-web-codegen/tests/trybuild/route-ok.rs @@ -1,12 +1,14 @@ -use actix_web::*; +use actix_web_codegen::*; #[route("/", method="GET", method="HEAD")] -async fn index() -> impl Responder { - HttpResponse::Ok() +async fn index() -> String { + "Hello World!".to_owned() } #[actix_web::main] async fn main() { + use actix_web::{App, test}; + let srv = test::start(|| App::new().service(index)); let request = srv.get("/"); diff --git a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.rs b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.rs index f4d8d9445..28cd1344c 100644 --- a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.rs +++ b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.rs @@ -1,12 +1,14 @@ -use actix_web::*; +use actix_web_codegen::*; #[route("/", method="UNEXPECTED")] -async fn index() -> impl Responder { - HttpResponse::Ok() +async fn index() -> String { + "Hello World!".to_owned() } #[actix_web::main] async fn main() { + use actix_web::{App, test}; + let srv = test::start(|| App::new().service(index)); let request = srv.get("/"); diff --git a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr index fe17fdf12..9d87f310b 100644 --- a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr @@ -5,7 +5,7 @@ error: Unexpected HTTP method: `UNEXPECTED` | ^^^^^^^^^^^^ error[E0425]: cannot find value `index` in this scope - --> $DIR/route-unexpected-method-fail.rs:10:49 + --> $DIR/route-unexpected-method-fail.rs:12:49 | -10 | let srv = test::start(|| App::new().service(index)); +12 | let srv = test::start(|| App::new().service(index)); | ^^^^^ not found in this scope diff --git a/actix-web-codegen/tests/trybuild/simple-fail.rs b/actix-web-codegen/tests/trybuild/simple-fail.rs index 368cff046..a57fdc16d 100644 --- a/actix-web-codegen/tests/trybuild/simple-fail.rs +++ b/actix-web-codegen/tests/trybuild/simple-fail.rs @@ -1,30 +1,30 @@ -use actix_web::*; +use actix_web_codegen::*; #[get("/one", other)] -async fn one() -> impl Responder { - HttpResponse::Ok() +async fn one() -> String { + "Hello World!".to_owned() } #[post(/two)] -async fn two() -> impl Responder { - HttpResponse::Ok() +async fn two() -> String { + "Hello World!".to_owned() } static PATCH_PATH: &str = "/three"; #[patch(PATCH_PATH)] -async fn three() -> impl Responder { - HttpResponse::Ok() +async fn three() -> String { + "Hello World!".to_owned() } #[delete("/four", "/five")] -async fn four() -> impl Responder { - HttpResponse::Ok() +async fn four() -> String { + "Hello World!".to_owned() } #[delete("/five", method="GET")] -async fn five() -> impl Responder { - HttpResponse::Ok() +async fn five() -> String { + "Hello World!".to_owned() } fn main() {} diff --git a/actix-web-codegen/tests/trybuild/simple.rs b/actix-web-codegen/tests/trybuild/simple.rs index 6b1e67442..761b04905 100644 --- a/actix-web-codegen/tests/trybuild/simple.rs +++ b/actix-web-codegen/tests/trybuild/simple.rs @@ -1,4 +1,5 @@ -use actix_web::*; +use actix_web::{Responder, HttpResponse, App, test}; +use actix_web_codegen::*; #[get("/config")] async fn config() -> impl Responder {