mirror of
https://github.com/fafhrd91/actix-web
synced 2025-07-22 17:07:19 +02:00
Compare commits
22 Commits
web-v3.0.0
...
codegen-v0
Author | SHA1 | Date | |
---|---|---|---|
|
c53e9468bc | ||
|
162121bf8d | ||
|
f7bcad9567 | ||
|
f9e3f78e45 | ||
|
1596893ef7 | ||
|
2a2474ca09 | ||
|
509b2e6eec | ||
|
d707704556 | ||
|
a429ee6646 | ||
|
7f8073233a | ||
|
4b4c9d1b93 | ||
|
3fde3be3d8 | ||
|
f861508789 | ||
|
a4546f02d2 | ||
|
64a2c13cdf | ||
|
bf53fe5a22 | ||
|
cf5138e740 | ||
|
121075c1ef | ||
|
22089aff87 | ||
|
7787638f26 | ||
|
2f6e9738c4 | ||
|
e39d166a17 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -13,3 +13,6 @@ guide/build/
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# Configuration directory generated by CLion
|
||||
.idea
|
||||
|
21
CHANGES.md
21
CHANGES.md
@@ -1,6 +1,27 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2020-xx-xx
|
||||
|
||||
|
||||
## 3.0.2 - 2020-09-15
|
||||
### Fixed
|
||||
* `NormalizePath` when used with `TrailingSlash::Trim` no longer trims the root path "/". [#1678]
|
||||
|
||||
[#1678]: https://github.com/actix/actix-web/pull/1678
|
||||
|
||||
|
||||
## 3.0.1 - 2020-09-13
|
||||
### Changed
|
||||
* `middleware::normalize::TrailingSlash` enum is now accessible. [#1673]
|
||||
|
||||
[#1673]: https://github.com/actix/actix-web/pull/1673
|
||||
|
||||
|
||||
## 3.0.0 - 2020-09-11
|
||||
* No significant changes from `3.0.0-beta.4`.
|
||||
|
||||
|
||||
## 3.0.0-beta.4 - 2020-09-09
|
||||
### Added
|
||||
* `middleware::NormalizePath` now has configurable behaviour for either always having a trailing
|
||||
slash, or as the new addition, always trimming trailing slashes. [#1639]
|
||||
|
17
Cargo.toml
17
Cargo.toml
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "actix-web"
|
||||
version = "3.0.0-beta.4"
|
||||
version = "3.0.2"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust."
|
||||
description = "Actix web is a powerful, pragmatic, and extremely fast web framework for Rust."
|
||||
readme = "README.md"
|
||||
keywords = ["actix", "http", "web", "framework", "async"]
|
||||
homepage = "https://actix.rs"
|
||||
@@ -76,9 +76,9 @@ actix-macros = "0.1.0"
|
||||
actix-threadpool = "0.3.1"
|
||||
actix-tls = "2.0.0"
|
||||
|
||||
actix-web-codegen = "0.3.0-beta.1"
|
||||
actix-http = "2.0.0-beta.4"
|
||||
awc = { version = "2.0.0-beta.4", default-features = false }
|
||||
actix-web-codegen = "0.3.0"
|
||||
actix-http = "2.0.0"
|
||||
awc = { version = "2.0.0", default-features = false }
|
||||
|
||||
bytes = "0.5.3"
|
||||
derive_more = "0.99.2"
|
||||
@@ -92,17 +92,18 @@ mime = "0.3"
|
||||
socket2 = "0.3"
|
||||
pin-project = "0.4.17"
|
||||
regex = "1.3"
|
||||
serde = { version = "1.0", features=["derive"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_urlencoded = "0.6.1"
|
||||
time = { version = "0.2.7", default-features = false, features = ["std"] }
|
||||
url = "2.1"
|
||||
open-ssl = { package = "openssl", version = "0.10", optional = true }
|
||||
rust-tls = { package = "rustls", version = "0.18.0", optional = true }
|
||||
tinyvec = { version = "0.3", features = ["alloc"] }
|
||||
tinyvec = { version = "1", features = ["alloc"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix = "0.10.0-alpha.1"
|
||||
actix = "0.10.0"
|
||||
actix-http = { version = "2.0.0", features = ["actors"] }
|
||||
rand = "0.7"
|
||||
env_logger = "0.7"
|
||||
serde_derive = "1.0"
|
||||
|
@@ -186,7 +186,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2017-NOW Nikolay Kim
|
||||
Copyright 2017-NOW Actix Team
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2017 Nikolay Kim
|
||||
Copyright (c) 2017 Actix Team
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
|
15
MIGRATION.md
15
MIGRATION.md
@@ -1,11 +1,25 @@
|
||||
## Unreleased
|
||||
|
||||
|
||||
## 3.0.0
|
||||
|
||||
* The return type for `ServiceRequest::app_data::<T>()` was changed from returning a `Data<T>` to
|
||||
simply a `T`. To access a `Data<T>` use `ServiceRequest::app_data::<Data<T>>()`.
|
||||
|
||||
* Cookie handling has been offloaded to the `cookie` crate:
|
||||
* `USERINFO_ENCODE_SET` is no longer exposed. Percent-encoding is still supported; check docs.
|
||||
* Some types now require lifetime parameters.
|
||||
|
||||
* The time crate was updated to `v0.2`, a major breaking change to the time crate, which affects
|
||||
any `actix-web` method previously expecting a time v0.1 input.
|
||||
|
||||
* Setting a cookie's SameSite property, explicitly, to `SameSite::None` will now
|
||||
result in `SameSite=None` being sent with the response Set-Cookie header.
|
||||
To create a cookie without a SameSite attribute, remove any calls setting same_site.
|
||||
|
||||
* actix-http support for Actors messages was moved to actix-http crate and is enabled
|
||||
with feature `actors`
|
||||
|
||||
* content_length function is removed from actix-http.
|
||||
You can set Content-Length by normally setting the response body or calling no_chunking function.
|
||||
|
||||
@@ -40,6 +54,7 @@
|
||||
|
||||
* `HttpServer::maxconnrate` is renamed to the more expressive `HttpServer::max_connection_rate`.
|
||||
|
||||
|
||||
## 2.0.0
|
||||
|
||||
* `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to
|
||||
|
@@ -37,17 +37,12 @@
|
||||
## Documentation
|
||||
|
||||
* [Website & User Guide](https://actix.rs)
|
||||
* [Examples Repository](https://actix.rs/actix-web/actix_web)
|
||||
* [Examples Repository](https://github.com/actix/examples)
|
||||
* [API Documentation](https://docs.rs/actix-web)
|
||||
* [API Documentation (master branch)](https://actix.rs/actix-web/actix_web)
|
||||
|
||||
## Example
|
||||
|
||||
<h2>
|
||||
WARNING: This example is for the master branch which is currently in beta stages for v3. For
|
||||
Actix web v2 see the <a href="https://actix.rs/docs/getting-started/">getting started guide</a>.
|
||||
</h2>
|
||||
|
||||
Dependencies:
|
||||
|
||||
```toml
|
||||
|
@@ -1,11 +0,0 @@
|
||||
# Cors Middleware for actix web framework [](https://travis-ci.org/actix/actix-web) [](https://codecov.io/gh/actix/actix-web) [](https://crates.io/crates/actix-cors) [](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
**This crate moved to https://github.com/actix/actix-extras.**
|
||||
|
||||
## Documentation & community resources
|
||||
|
||||
* [User Guide](https://actix.rs/docs/)
|
||||
* [API Documentation](https://docs.rs/actix-cors/)
|
||||
* [Chat on gitter](https://gitter.im/actix/actix)
|
||||
* Cargo package: [actix-cors](https://crates.io/crates/actix-cors)
|
||||
* Minimum supported Rust version: 1.34 or later
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-files"
|
||||
version = "0.3.0-beta.1"
|
||||
version = "0.3.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Static files support for actix web."
|
||||
readme = "README.md"
|
||||
@@ -17,9 +17,8 @@ name = "actix_files"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
actix-web = { version = "3.0.0-beta.4", default-features = false }
|
||||
actix-http = "2.0.0-beta.4"
|
||||
actix-service = "1.0.1"
|
||||
actix-web = { version = "3.0.0", default-features = false }
|
||||
actix-service = "1.0.6"
|
||||
bitflags = "1"
|
||||
bytes = "0.5.3"
|
||||
futures-core = { version = "0.3.5", default-features = false }
|
||||
@@ -33,4 +32,4 @@ v_htmlescape = "0.10"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "1.0.0"
|
||||
actix-web = { version = "3.0.0-beta.4", features = ["openssl"] }
|
||||
actix-web = { version = "3.0.0", features = ["openssl"] }
|
||||
|
94
actix-files/src/chunked.rs
Normal file
94
actix-files/src/chunked.rs
Normal file
@@ -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<io::Error>>>;
|
||||
|
||||
#[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<File>,
|
||||
pub(crate) fut: Option<ChunkedBoxFuture>,
|
||||
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<Bytes, Error>;
|
||||
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
114
actix-files/src/directory.rs
Normal file
114
actix-files/src/directory.rs
Normal file
@@ -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<DirEntry>) -> 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<ServiceResponse, io::Error>;
|
||||
|
||||
// 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<ServiceResponse, io::Error> {
|
||||
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,
|
||||
"<li><a href=\"{}\">{}/</a></li>",
|
||||
encode_file_url!(p),
|
||||
encode_file_name!(entry),
|
||||
);
|
||||
} else {
|
||||
let _ = write!(
|
||||
body,
|
||||
"<li><a href=\"{}\">{}</a></li>",
|
||||
encode_file_url!(p),
|
||||
encode_file_name!(entry),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let html = format!(
|
||||
"<html>\
|
||||
<head><title>{}</title></head>\
|
||||
<body><h1>{}</h1>\
|
||||
<ul>\
|
||||
{}\
|
||||
</ul></body>\n</html>",
|
||||
index_of, index_of, body
|
||||
);
|
||||
Ok(ServiceResponse::new(
|
||||
req.clone(),
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(html),
|
||||
))
|
||||
}
|
250
actix-files/src/files.rs
Normal file
250
actix-files/src/files.rs
Normal file
@@ -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<String>,
|
||||
show_index: bool,
|
||||
redirect_to_slash: bool,
|
||||
default: Rc<RefCell<Option<Rc<HttpNewService>>>>,
|
||||
renderer: Rc<DirectoryRenderer>,
|
||||
mime_override: Option<Rc<MimeOverride>>,
|
||||
file_flags: named::Flags,
|
||||
guards: Option<Rc<dyn Guard>>,
|
||||
}
|
||||
|
||||
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<T: Into<PathBuf>>(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<F>(mut self, f: F) -> Self
|
||||
where
|
||||
for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) -> Result<ServiceResponse, io::Error>
|
||||
+ 'static,
|
||||
{
|
||||
self.renderer = Rc::new(f);
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies mime override callback
|
||||
pub fn mime_override<F>(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<T: Into<String>>(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<G: Guard + 'static>(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<F, U>(mut self, f: F) -> Self
|
||||
where
|
||||
F: IntoServiceFactory<U>,
|
||||
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<Self::Service, Self::InitError>>;
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,42 +1,52 @@
|
||||
#![allow(clippy::borrow_interior_mutable_const, clippy::type_complexity)]
|
||||
//! 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.
|
||||
|
||||
//! Static files support
|
||||
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};
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![warn(missing_docs, missing_debug_implementations)]
|
||||
|
||||
use actix_service::boxed::{self, BoxService, BoxServiceFactory};
|
||||
use actix_service::{IntoServiceFactory, Service, ServiceFactory};
|
||||
use actix_web::dev::{
|
||||
AppService, HttpServiceFactory, Payload, ResourceDef, ServiceRequest,
|
||||
ServiceResponse,
|
||||
use std::io;
|
||||
|
||||
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<ServiceRequest, ServiceResponse, Error>;
|
||||
type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>;
|
||||
@@ -49,611 +59,37 @@ pub fn file_extension_to_mime(ext: &str) -> mime::Mime {
|
||||
from_ext(ext).first_or_octet_stream()
|
||||
}
|
||||
|
||||
fn handle_error(err: BlockingError<io::Error>) -> Error {
|
||||
pub(crate) fn handle_error(err: BlockingError<io::Error>) -> 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<File>,
|
||||
fut:
|
||||
Option<LocalBoxFuture<'static, Result<(File, Bytes), BlockingError<io::Error>>>>,
|
||||
counter: u64,
|
||||
}
|
||||
|
||||
impl Stream for ChunkedReadFile {
|
||||
type Item = Result<Bytes, Error>;
|
||||
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
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<ServiceResponse, io::Error>;
|
||||
|
||||
/// 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<DirEntry>) -> 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<ServiceResponse, io::Error> {
|
||||
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,
|
||||
"<li><a href=\"{}\">{}/</a></li>",
|
||||
encode_file_url!(p),
|
||||
encode_file_name!(entry),
|
||||
);
|
||||
} else {
|
||||
let _ = write!(
|
||||
body,
|
||||
"<li><a href=\"{}\">{}</a></li>",
|
||||
encode_file_url!(p),
|
||||
encode_file_name!(entry),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let html = format!(
|
||||
"<html>\
|
||||
<head><title>{}</title></head>\
|
||||
<body><h1>{}</h1>\
|
||||
<ul>\
|
||||
{}\
|
||||
</ul></body>\n</html>",
|
||||
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 as fs;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let app = App::new()
|
||||
/// .service(fs::Files::new("/static", "."));
|
||||
/// }
|
||||
/// ```
|
||||
pub struct Files {
|
||||
path: String,
|
||||
directory: PathBuf,
|
||||
index: Option<String>,
|
||||
show_index: bool,
|
||||
redirect_to_slash: bool,
|
||||
default: Rc<RefCell<Option<Rc<HttpNewService>>>>,
|
||||
renderer: Rc<DirectoryRenderer>,
|
||||
mime_override: Option<Rc<MimeOverride>>,
|
||||
file_flags: named::Flags,
|
||||
// FIXME: Should re-visit later.
|
||||
#[allow(clippy::redundant_allocation)]
|
||||
guards: Option<Rc<Box<dyn Guard>>>,
|
||||
}
|
||||
|
||||
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<T: Into<PathBuf>>(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<F>(mut self, f: F) -> Self
|
||||
where
|
||||
for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) -> Result<ServiceResponse, io::Error>
|
||||
+ 'static,
|
||||
{
|
||||
self.renderer = Rc::new(f);
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies mime override callback
|
||||
pub fn mime_override<F>(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<T: Into<String>>(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<G: Guard + 'static>(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<F, U>(mut self, f: F) -> Self
|
||||
where
|
||||
F: IntoServiceFactory<U>,
|
||||
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<Self::Service, Self::InitError>>;
|
||||
|
||||
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<String>,
|
||||
show_index: bool,
|
||||
redirect_to_slash: bool,
|
||||
default: Option<HttpService>,
|
||||
renderer: Rc<DirectoryRenderer>,
|
||||
mime_override: Option<Rc<MimeOverride>>,
|
||||
file_flags: named::Flags,
|
||||
// FIXME: Should re-visit later.
|
||||
#[allow(clippy::redundant_allocation)]
|
||||
guards: Option<Rc<Box<dyn Guard>>>,
|
||||
}
|
||||
|
||||
impl FilesService {
|
||||
fn handle_err(
|
||||
&mut self,
|
||||
e: io::Error,
|
||||
req: ServiceRequest,
|
||||
) -> Either<
|
||||
Ready<Result<ServiceResponse, Error>>,
|
||||
LocalBoxFuture<'static, Result<ServiceResponse, Error>>,
|
||||
> {
|
||||
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;
|
||||
type Future = Either<
|
||||
Ready<Result<Self::Response, Self::Error>>,
|
||||
LocalBoxFuture<'static, Result<Self::Response, Self::Error>>,
|
||||
>;
|
||||
|
||||
fn poll_ready(&mut self, _: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
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<Self, UriSegmentError> {
|
||||
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<Result<Self, Self::Error>>;
|
||||
type Config = ();
|
||||
|
||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||
ready(PathBufWrp::get_pathbuf(req.match_info().path()))
|
||||
}
|
||||
}
|
||||
type MimeOverride = dyn Fn(&mime::Name<'_>) -> DispositionType;
|
||||
|
||||
#[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() {
|
||||
@@ -898,7 +334,7 @@ mod tests {
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_mime_override() {
|
||||
fn all_attachment(_: &mime::Name) -> DispositionType {
|
||||
fn all_attachment(_: &mime::Name<'_>) -> DispositionType {
|
||||
DispositionType::Attachment
|
||||
}
|
||||
|
||||
@@ -1010,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);
|
||||
}
|
||||
|
||||
@@ -1043,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);
|
||||
}
|
||||
|
||||
@@ -1221,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]
|
||||
@@ -1337,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"])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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<HttpResponse, Error> {
|
||||
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::<header::IfMatch>() {
|
||||
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::<header::IfNoneMatch>() {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
99
actix-files/src/path_buf.rs
Normal file
99
actix-files/src/path_buf.rs
Normal file
@@ -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<Self, Self::Err> {
|
||||
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<Path> for PathBufWrap {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRequest for PathBufWrap {
|
||||
type Error = UriSegmentError;
|
||||
type Future = Ready<Result<Self, Self::Error>>;
|
||||
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"])
|
||||
);
|
||||
}
|
||||
}
|
@@ -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 {
|
||||
|
167
actix-files/src/service.rs
Normal file
167
actix-files/src/service.rs
Normal file
@@ -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<String>,
|
||||
pub(crate) show_index: bool,
|
||||
pub(crate) redirect_to_slash: bool,
|
||||
pub(crate) default: Option<HttpService>,
|
||||
pub(crate) renderer: Rc<DirectoryRenderer>,
|
||||
pub(crate) mime_override: Option<Rc<MimeOverride>>,
|
||||
pub(crate) file_flags: named::Flags,
|
||||
pub(crate) guards: Option<Rc<dyn Guard>>,
|
||||
}
|
||||
|
||||
type FilesServiceFuture = Either<
|
||||
Ready<Result<ServiceResponse, Error>>,
|
||||
LocalBoxFuture<'static, Result<ServiceResponse, Error>>,
|
||||
>;
|
||||
|
||||
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<Result<(), Self::Error>> {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
# Framed app for actix web
|
||||
|
||||
**This crate has been deprecated and removed.**
|
@@ -3,6 +3,10 @@
|
||||
## Unreleased - 2020-xx-xx
|
||||
|
||||
|
||||
## 2.0.0 - 2020-09-11
|
||||
* No significant changes from `2.0.0-beta.4`.
|
||||
|
||||
|
||||
## 2.0.0-beta.4 - 2020-09-09
|
||||
### Changed
|
||||
* Update actix-codec and actix-utils dependencies.
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-http"
|
||||
version = "2.0.0-beta.4"
|
||||
version = "2.0.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix HTTP primitives"
|
||||
readme = "README.md"
|
||||
@@ -40,14 +40,14 @@ secure-cookies = ["cookie/secure"]
|
||||
actors = ["actix"]
|
||||
|
||||
[dependencies]
|
||||
actix-service = "1.0.5"
|
||||
actix-service = "1.0.6"
|
||||
actix-codec = "0.3.0"
|
||||
actix-connect = "2.0.0"
|
||||
actix-utils = "2.0.0"
|
||||
actix-rt = "1.0.0"
|
||||
actix-threadpool = "0.3.1"
|
||||
actix-tls = { version = "2.0.0", optional = true }
|
||||
actix = { version = "0.10.0-alpha.1", optional = true }
|
||||
actix = { version = "0.10.0", optional = true }
|
||||
|
||||
base64 = "0.12"
|
||||
bitflags = "1.2"
|
||||
@@ -88,7 +88,7 @@ flate2 = { version = "1.0.13", optional = true }
|
||||
[dev-dependencies]
|
||||
actix-server = "1.0.1"
|
||||
actix-connect = { version = "2.0.0", features = ["openssl"] }
|
||||
actix-http-test = { version = "2.0.0-alpha.1", features = ["openssl"] }
|
||||
actix-http-test = { version = "2.0.0", features = ["openssl"] }
|
||||
actix-tls = { version = "2.0.0", features = ["openssl"] }
|
||||
criterion = "0.3"
|
||||
env_logger = "0.7"
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -714,7 +714,7 @@ mod tests {
|
||||
let body = resp_body.downcast_ref::<String>().unwrap();
|
||||
assert_eq!(body, "hello cast");
|
||||
let body = &mut resp_body.downcast_mut::<String>().unwrap();
|
||||
body.push_str("!");
|
||||
body.push('!');
|
||||
let body = resp_body.downcast_ref::<String>().unwrap();
|
||||
assert_eq!(body, "hello cast!");
|
||||
let not_body = resp_body.downcast_ref::<()>();
|
||||
|
@@ -119,11 +119,11 @@ where
|
||||
match poll_fn(|cx| Poll::Ready(inner.borrow_mut().acquire(&key, cx))).await {
|
||||
Acquire::Acquired(io, created) => {
|
||||
// use existing connection
|
||||
return Ok(IoConnection::new(
|
||||
Ok(IoConnection::new(
|
||||
io,
|
||||
created,
|
||||
Some(Acquired(key, Some(inner))),
|
||||
));
|
||||
))
|
||||
}
|
||||
Acquire::Available => {
|
||||
// open tcp connection
|
||||
|
@@ -283,11 +283,11 @@ impl DispositionParam {
|
||||
/// Some("\u{1f600}.svg".as_bytes()));
|
||||
/// ```
|
||||
///
|
||||
/// # WARN
|
||||
/// # Security Note
|
||||
///
|
||||
/// If "filename" parameter is supplied, do not use the file name blindly, check and possibly
|
||||
/// change to match local file system conventions if applicable, and do not use directory path
|
||||
/// information that may be present. See [RFC2183](https://tools.ietf.org/html/rfc2183#section-2.3)
|
||||
/// .
|
||||
/// information that may be present. See [RFC2183](https://tools.ietf.org/html/rfc2183#section-2.3).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ContentDisposition {
|
||||
/// The disposition type
|
||||
|
@@ -1,5 +1,6 @@
|
||||
//! Basic http primitives for actix-net framework.
|
||||
#![warn(rust_2018_idioms, warnings)]
|
||||
|
||||
#![deny(rust_2018_idioms)]
|
||||
#](https://travis-ci.org/actix/actix-web) [](https://codecov.io/gh/actix/actix-web) [](https://crates.io/crates/actix-identity) [](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
**This crate moved to https://github.com/actix/actix-extras.**
|
||||
|
||||
## Documentation & community resources
|
||||
|
||||
* [User Guide](https://actix.rs/docs/)
|
||||
* [API Documentation](https://docs.rs/actix-identity/)
|
||||
* [Chat on gitter](https://gitter.im/actix/actix)
|
||||
* Cargo package: [actix-session](https://crates.io/crates/actix-identity)
|
||||
* Minimum supported Rust version: 1.34 or later
|
@@ -3,6 +3,14 @@
|
||||
## Unreleased - 2020-xx-xx
|
||||
|
||||
|
||||
## 3.0.0 - 2020-09-11
|
||||
* No significant changes from `3.0.0-beta.2`.
|
||||
|
||||
|
||||
## 3.0.0-beta.2 - 2020-09-10
|
||||
* Update `actix-*` dependencies to latest versions.
|
||||
|
||||
|
||||
## 0.3.0-beta.1 - 2020-07-15
|
||||
* Update `actix-web` to 3.0.0-beta.1
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-multipart"
|
||||
version = "0.3.0-beta.1"
|
||||
version = "0.3.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Multipart support for actix web framework."
|
||||
readme = "README.md"
|
||||
@@ -16,8 +16,8 @@ name = "actix_multipart"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
actix-web = { version = "3.0.0-beta.4", default-features = false }
|
||||
actix-service = "1.0.1"
|
||||
actix-web = { version = "3.0.0", default-features = false }
|
||||
actix-service = "1.0.6"
|
||||
actix-utils = "2.0.0"
|
||||
bytes = "0.5.3"
|
||||
derive_more = "0.99.2"
|
||||
@@ -29,4 +29,4 @@ twoway = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "1.0.0"
|
||||
actix-http = "2.0.0-beta.4"
|
||||
actix-http = "2.0.0"
|
||||
|
@@ -1,3 +1,6 @@
|
||||
//! Multipart form support for Actix web.
|
||||
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![allow(clippy::borrow_interior_mutable_const)]
|
||||
|
||||
mod error;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
//! Multipart payload support
|
||||
|
||||
use std::cell::{Cell, RefCell, RefMut};
|
||||
use std::convert::TryFrom;
|
||||
use std::marker::PhantomData;
|
||||
@@ -108,7 +109,7 @@ impl Stream for Multipart {
|
||||
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
if let Some(err) = self.error.take() {
|
||||
Poll::Ready(Some(Err(err)))
|
||||
@@ -244,7 +245,7 @@ impl InnerMultipart {
|
||||
fn poll(
|
||||
&mut self,
|
||||
safety: &Safety,
|
||||
cx: &mut Context,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Field, MultipartError>>> {
|
||||
if self.state == InnerState::Eof {
|
||||
Poll::Ready(None)
|
||||
@@ -416,7 +417,10 @@ impl Field {
|
||||
impl Stream for Field {
|
||||
type Item = Result<Bytes, MultipartError>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
if self.safety.current() {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
if let Some(mut payload) =
|
||||
@@ -434,7 +438,7 @@ impl Stream for Field {
|
||||
}
|
||||
|
||||
impl fmt::Debug for Field {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(f, "\nField: {}", self.ct)?;
|
||||
writeln!(f, " boundary: {}", self.inner.borrow().boundary)?;
|
||||
writeln!(f, " headers:")?;
|
||||
@@ -689,7 +693,7 @@ impl Safety {
|
||||
self.clean.get()
|
||||
}
|
||||
|
||||
fn clone(&self, cx: &mut Context) -> Safety {
|
||||
fn clone(&self, cx: &mut Context<'_>) -> Safety {
|
||||
let payload = Rc::clone(&self.payload);
|
||||
let s = Safety {
|
||||
task: LocalWaker::new(),
|
||||
@@ -734,7 +738,7 @@ impl PayloadBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_stream(&mut self, cx: &mut Context) -> Result<(), PayloadError> {
|
||||
fn poll_stream(&mut self, cx: &mut Context<'_>) -> Result<(), PayloadError> {
|
||||
loop {
|
||||
match Pin::new(&mut self.stream).poll_next(cx) {
|
||||
Poll::Ready(Some(Ok(data))) => self.buf.extend_from_slice(&data),
|
||||
@@ -887,7 +891,7 @@ mod tests {
|
||||
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
let this = self.get_mut();
|
||||
if !this.ready {
|
||||
|
@@ -1,11 +0,0 @@
|
||||
# Session for actix web framework [](https://travis-ci.org/actix/actix-web) [](https://codecov.io/gh/actix/actix-web) [](https://crates.io/crates/actix-session) [](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
**This crate moved to https://github.com/actix/actix-extras.**
|
||||
|
||||
## Documentation & community resources
|
||||
|
||||
* [User Guide](https://actix.rs/docs/)
|
||||
* [API Documentation](https://docs.rs/actix-session/)
|
||||
* [Chat on gitter](https://gitter.im/actix/actix)
|
||||
* Cargo package: [actix-session](https://crates.io/crates/actix-session)
|
||||
* Minimum supported Rust version: 1.34 or later
|
@@ -1,6 +1,14 @@
|
||||
# Changes
|
||||
|
||||
## [Unreleased] - 2020-xx-xx
|
||||
## Unreleased - 2020-xx-xx
|
||||
|
||||
|
||||
## 3.0.0 - 2020-09-11
|
||||
* No significant changes from `3.0.0-beta.2`.
|
||||
|
||||
|
||||
## 3.0.0-beta.2 - 2020-09-10
|
||||
* Update `actix-*` dependencies to latest versions.
|
||||
|
||||
|
||||
## [3.0.0-beta.1] - 2020-xx-xx
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-web-actors"
|
||||
version = "3.0.0-beta.1"
|
||||
version = "3.0.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix actors support for actix web framework."
|
||||
readme = "README.md"
|
||||
@@ -16,9 +16,9 @@ name = "actix_web_actors"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
actix = "0.10.0-alpha.2"
|
||||
actix-web = { version = "3.0.0-beta.4", default-features = false }
|
||||
actix-http = "2.0.0-beta.4"
|
||||
actix = "0.10.0"
|
||||
actix-web = { version = "3.0.0", default-features = false }
|
||||
actix-http = "2.0.0"
|
||||
actix-codec = "0.3.0"
|
||||
bytes = "0.5.2"
|
||||
futures-channel = { version = "0.3.5", default-features = false }
|
||||
@@ -26,6 +26,6 @@ futures-core = { version = "0.3.5", default-features = false }
|
||||
pin-project = "0.4.17"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "1.0.0"
|
||||
actix-rt = "1.1.1"
|
||||
env_logger = "0.7"
|
||||
futures-util = { version = "0.3.5", default-features = false }
|
||||
|
@@ -1,5 +1,8 @@
|
||||
#![allow(clippy::borrow_interior_mutable_const)]
|
||||
//! Actix actors integration for Actix web framework
|
||||
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![allow(clippy::borrow_interior_mutable_const)]
|
||||
|
||||
mod context;
|
||||
pub mod ws;
|
||||
|
||||
|
@@ -3,6 +3,18 @@
|
||||
## 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.
|
||||
|
||||
[#1677]: https://github.com/actix/actix-web/pull/1677
|
||||
[#1674]: https://github.com/actix/actix-web/pull/1674
|
||||
|
||||
|
||||
## 0.3.0 - 2020-09-11
|
||||
* No significant changes from `0.3.0-beta.1`.
|
||||
|
||||
|
||||
## 0.3.0-beta.1 - 2020-07-14
|
||||
* Add main entry-point macro that uses re-exported runtime. [#1559]
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-web-codegen"
|
||||
version = "0.3.0-beta.1"
|
||||
version = "0.4.0"
|
||||
description = "Actix web proc macros"
|
||||
readme = "README.md"
|
||||
homepage = "https://actix.rs"
|
||||
@@ -19,6 +19,8 @@ syn = { version = "1", features = ["full", "parsing"] }
|
||||
proc-macro2 = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "1.0.0"
|
||||
actix-web = "3.0.0-beta.4"
|
||||
actix-rt = "1.1.1"
|
||||
actix-web = "3.0.0"
|
||||
futures-util = { version = "0.3.5", default-features = false }
|
||||
trybuild = "1"
|
||||
rustversion = "1"
|
||||
|
@@ -1,8 +1,22 @@
|
||||
# Helper and convenience macros for Actix-web. [](https://travis-ci.org/actix/actix-web) [](https://codecov.io/gh/actix/actix-web) [](https://crates.io/crates/actix-web-codegen) [](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
# actix-web-codegen
|
||||
|
||||
> Helper and convenience macros for Actix Web
|
||||
|
||||
[](https://crates.io/crates/actix-web-codegen)
|
||||
[](https://docs.rs/actix-web)
|
||||
[](https://blog.rust-lang.org/2020/03/12/Rust-1.42.html)
|
||||
[](https://travis-ci.org/actix/actix-web)
|
||||
[](https://codecov.io/gh/actix/actix-web)
|
||||
[](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
## Documentation & Resources
|
||||
|
||||
* [API Documentation](https://docs.rs/actix-web-codegen/)
|
||||
* [Chat on gitter](https://gitter.im/actix/actix)
|
||||
* Cargo package: [actix-web-codegen](https://crates.io/crates/actix-web-codegen)
|
||||
* Minimum supported Rust version: 1.40 or later
|
||||
- [API Documentation](https://docs.rs/actix-web-codegen)
|
||||
- [Chat on Gitter](https://gitter.im/actix/actix-web)
|
||||
- Cargo package: [actix-web-codegen](https://crates.io/crates/actix-web-codegen)
|
||||
- Minimum supported Rust version: 1.42 or later.
|
||||
|
||||
## Compile Testing
|
||||
Uses the [`trybuild`] crate. All compile fail tests should include a stderr file generated by `trybuild`. See the [workflow section](https://github.com/dtolnay/trybuild#workflow) of the trybuild docs for info on how to do this.
|
||||
|
||||
[`trybuild`]: https://github.com/dtolnay/trybuild
|
||||
|
@@ -1,158 +1,174 @@
|
||||
#![recursion_limit = "512"]
|
||||
|
||||
//! Helper and convenience macros for Actix-web.
|
||||
//! Macros for reducing boilerplate code in Actix Web applications.
|
||||
//!
|
||||
//! ## Runtime Setup
|
||||
//! ## 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.
|
||||
//!
|
||||
//! - [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:
|
||||
//! # Runtime Setup
|
||||
//! Used for setting up the actix async runtime. See [main] macro docs.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use actix_web::HttpResponse;
|
||||
//! use actix_web_codegen::get;
|
||||
//!
|
||||
//! #[get("/test")]
|
||||
//! async fn async_test() -> Result<HttpResponse, actix_web::Error> {
|
||||
//! Ok(HttpResponse::Ok().finish())
|
||||
//! #[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;
|
||||
|
||||
mod route;
|
||||
#![recursion_limit = "512"]
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
/// Creates route handler with `GET` method guard.
|
||||
mod route;
|
||||
|
||||
/// Creates resource handler, allowing multiple HTTP method guards.
|
||||
///
|
||||
/// Syntax: `#[get("path"[, attributes])]`
|
||||
/// # Syntax
|
||||
/// ```text
|
||||
/// #[route("path", method="HTTP_METHOD"[, attributes])]
|
||||
/// ```
|
||||
///
|
||||
/// ## Attributes:
|
||||
///
|
||||
/// - `"path"` - Raw literal string with path for which to register handler. Mandatory.
|
||||
/// # 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("/test", method="GET", method="HEAD")]
|
||||
/// async fn example() -> HttpResponse {
|
||||
/// HttpResponse::Ok().finish()
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
pub fn get(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
route::generate(args, input, route::GuardType::Get)
|
||||
pub fn route(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
route::with_method(None, args, input)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
macro_rules! doc_comment {
|
||||
($x:expr; $($tt:tt)*) => {
|
||||
#[doc = $x]
|
||||
$($tt)*
|
||||
};
|
||||
}
|
||||
|
||||
/// 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)
|
||||
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)
|
||||
}
|
||||
})+
|
||||
};
|
||||
}
|
||||
|
||||
/// 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 [head](attr.head.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 [connect](attr.connect.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 [options](attr.options.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 [trace](attr.trace.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 [patch](attr.patch.html)
|
||||
#[proc_macro_attribute]
|
||||
pub fn patch(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
route::generate(args, input, route::GuardType::Patch)
|
||||
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.
|
||||
///
|
||||
/// ## 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;
|
||||
|
||||
|
@@ -1,5 +1,8 @@
|
||||
extern crate proc_macro;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::{Span, TokenStream as TokenStream2};
|
||||
use quote::{format_ident, quote, ToTokens, TokenStreamExt};
|
||||
@@ -17,53 +20,81 @@ impl ToTokens for ResourceType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum GuardType {
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Head,
|
||||
Connect,
|
||||
Options,
|
||||
Trace,
|
||||
Patch,
|
||||
}
|
||||
|
||||
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",
|
||||
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<Self, String> {
|
||||
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 MethodType {
|
||||
type Error = syn::Error;
|
||||
|
||||
fn try_from(value: &syn::LitStr) -> Result<Self, Self::Error> {
|
||||
Self::parse(value.value().as_str())
|
||||
.map_err(|message| syn::Error::new_spanned(value, message))
|
||||
}
|
||||
}
|
||||
|
||||
struct Args {
|
||||
path: syn::LitStr,
|
||||
guards: Vec<Ident>,
|
||||
wrappers: Vec<syn::Type>,
|
||||
methods: HashSet<MethodType>,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
fn new(args: AttributeArgs) -> syn::Result<Self> {
|
||||
fn new(args: AttributeArgs, method: Option<MethodType>) -> syn::Result<Self> {
|
||||
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 {
|
||||
@@ -96,10 +127,33 @@ impl Args {
|
||||
"Attribute wrap expects type",
|
||||
));
|
||||
}
|
||||
} else if nv.path.is_ident("method") {
|
||||
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: `{}`",
|
||||
lit.value()
|
||||
),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err(syn::Error::new_spanned(
|
||||
nv.lit,
|
||||
"Attribute method expects literal string!",
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err(syn::Error::new_spanned(
|
||||
nv.path,
|
||||
"Unknown attribute key is specified. Allowed: guard and wrap",
|
||||
"Unknown attribute key is specified. Allowed: guard, method and wrap",
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -112,6 +166,7 @@ impl Args {
|
||||
path: path.unwrap(),
|
||||
guards,
|
||||
wrappers,
|
||||
methods,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -121,7 +176,6 @@ pub struct Route {
|
||||
args: Args,
|
||||
ast: syn::ItemFn,
|
||||
resource_type: ResourceType,
|
||||
guard: GuardType,
|
||||
}
|
||||
|
||||
fn guess_resource_type(typ: &syn::Type) -> ResourceType {
|
||||
@@ -150,21 +204,30 @@ impl Route {
|
||||
pub fn new(
|
||||
args: AttributeArgs,
|
||||
input: TokenStream,
|
||||
guard: GuardType,
|
||||
method: Option<MethodType>,
|
||||
) -> syn::Result<Self> {
|
||||
if args.is_empty() {
|
||||
return Err(syn::Error::new(
|
||||
Span::call_site(),
|
||||
format!(
|
||||
r#"invalid server definition, expected #[{}("<some path>")]"#,
|
||||
guard.as_str().to_ascii_lowercase()
|
||||
r#"invalid service definition, expected #[{}("<some path>")]"#,
|
||||
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)?;
|
||||
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",
|
||||
));
|
||||
}
|
||||
|
||||
let resource_type = if ast.sig.asyncness.is_some() {
|
||||
ResourceType::Async
|
||||
@@ -185,7 +248,6 @@ impl Route {
|
||||
args,
|
||||
ast,
|
||||
resource_type,
|
||||
guard,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -194,17 +256,36 @@ impl ToTokens for Route {
|
||||
fn to_tokens(&self, output: &mut TokenStream2) {
|
||||
let Self {
|
||||
name,
|
||||
guard,
|
||||
ast,
|
||||
args:
|
||||
Args {
|
||||
path,
|
||||
guards,
|
||||
wrappers,
|
||||
methods,
|
||||
},
|
||||
resource_type,
|
||||
} = self;
|
||||
let resource_name = name.to_string();
|
||||
let method_guards = {
|
||||
let mut others = methods.iter();
|
||||
// unwrapping since length is checked to be at least one
|
||||
let first = others.next().unwrap();
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let stream = quote! {
|
||||
#[allow(non_camel_case_types, missing_docs)]
|
||||
pub struct #name;
|
||||
@@ -214,7 +295,7 @@ impl ToTokens for Route {
|
||||
#ast
|
||||
let __resource = actix_web::Resource::new(#path)
|
||||
.name(#resource_name)
|
||||
.guard(actix_web::guard::#guard())
|
||||
#method_guards
|
||||
#(.guard(actix_web::guard::fn_guard(#guards)))*
|
||||
#(.wrap(#wrappers))*
|
||||
.#resource_type(#name);
|
||||
@@ -228,13 +309,13 @@ impl ToTokens for Route {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn generate(
|
||||
pub(crate) fn with_method(
|
||||
method: Option<MethodType>,
|
||||
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(),
|
||||
}
|
||||
|
@@ -5,7 +5,9 @@ use std::task::{Context, Poll};
|
||||
use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
|
||||
use actix_web::http::header::{HeaderName, HeaderValue};
|
||||
use actix_web::{http, test, web::Path, App, Error, HttpResponse, Responder};
|
||||
use actix_web_codegen::{connect, delete, get, head, options, patch, post, put, trace};
|
||||
use actix_web_codegen::{
|
||||
connect, delete, get, head, options, patch, post, put, route, trace,
|
||||
};
|
||||
use futures_util::future;
|
||||
|
||||
// Make sure that we can name function as 'config'
|
||||
@@ -79,6 +81,11 @@ async fn get_param_test(_: Path<String>) -> impl Responder {
|
||||
HttpResponse::Ok()
|
||||
}
|
||||
|
||||
#[route("/multi", method = "GET", method = "POST", method = "HEAD")]
|
||||
async fn route_test() -> impl Responder {
|
||||
HttpResponse::Ok()
|
||||
}
|
||||
|
||||
pub struct ChangeStatusCode;
|
||||
|
||||
impl<S, B> Transform<S> for ChangeStatusCode
|
||||
@@ -172,6 +179,7 @@ async fn test_body() {
|
||||
.service(trace_test)
|
||||
.service(patch_test)
|
||||
.service(test_handler)
|
||||
.service(route_test)
|
||||
});
|
||||
let request = srv.request(http::Method::GET, srv.url("/test"));
|
||||
let response = request.send().await.unwrap();
|
||||
@@ -210,6 +218,22 @@ async fn test_body() {
|
||||
let request = srv.request(http::Method::GET, srv.url("/test"));
|
||||
let response = request.send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
|
||||
let request = srv.request(http::Method::GET, srv.url("/multi"));
|
||||
let response = request.send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
|
||||
let request = srv.request(http::Method::POST, srv.url("/multi"));
|
||||
let response = request.send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
|
||||
let request = srv.request(http::Method::HEAD, srv.url("/multi"));
|
||||
let response = request.send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
|
||||
let request = srv.request(http::Method::PATCH, srv.url("/multi"));
|
||||
let response = request.send().await.unwrap();
|
||||
assert!(!response.status().is_success());
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
|
27
actix-web-codegen/tests/trybuild.rs
Normal file
27
actix-web-codegen/tests/trybuild.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
#[test]
|
||||
fn compile_macros() {
|
||||
let t = trybuild::TestCases::new();
|
||||
|
||||
t.pass("tests/trybuild/simple.rs");
|
||||
t.compile_fail("tests/trybuild/simple-fail.rs");
|
||||
|
||||
t.pass("tests/trybuild/route-ok.rs");
|
||||
t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs");
|
||||
t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs");
|
||||
|
||||
test_route_missing_method(&t)
|
||||
}
|
||||
|
||||
#[rustversion::stable(1.42)]
|
||||
fn test_route_missing_method(t: &trybuild::TestCases) {
|
||||
t.compile_fail("tests/trybuild/route-missing-method-fail-msrv.rs");
|
||||
}
|
||||
|
||||
#[rustversion::not(stable(1.42))]
|
||||
#[rustversion::not(nightly)]
|
||||
fn test_route_missing_method(t: &trybuild::TestCases) {
|
||||
t.compile_fail("tests/trybuild/route-missing-method-fail.rs");
|
||||
}
|
||||
|
||||
#[rustversion::nightly]
|
||||
fn test_route_missing_method(_t: &trybuild::TestCases) {}
|
@@ -0,0 +1,17 @@
|
||||
use actix_web_codegen::*;
|
||||
|
||||
#[route("/", method="GET", method="GET")]
|
||||
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("/");
|
||||
let response = request.send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
error: HTTP method defined more than once: `GET`
|
||||
--> $DIR/route-duplicate-method-fail.rs:3:35
|
||||
|
|
||||
3 | #[route("/", method="GET", method="GET")]
|
||||
| ^^^^^
|
||||
|
||||
error[E0425]: cannot find value `index` in this scope
|
||||
--> $DIR/route-duplicate-method-fail.rs:12:49
|
||||
|
|
||||
12 | let srv = test::start(|| App::new().service(index));
|
||||
| ^^^^^ not found in this scope
|
@@ -0,0 +1 @@
|
||||
route-missing-method-fail.rs
|
@@ -0,0 +1,11 @@
|
||||
error: The #[route(..)] macro requires at least one `method` attribute
|
||||
--> $DIR/route-missing-method-fail-msrv.rs:3:1
|
||||
|
|
||||
3 | #[route("/")]
|
||||
| ^^^^^^^^^^^^^
|
||||
|
||||
error[E0425]: cannot find value `index` in this scope
|
||||
--> $DIR/route-missing-method-fail-msrv.rs:12:49
|
||||
|
|
||||
12 | let srv = test::start(|| App::new().service(index));
|
||||
| ^^^^^ not found in this scope
|
@@ -0,0 +1,17 @@
|
||||
use actix_web_codegen::*;
|
||||
|
||||
#[route("/")]
|
||||
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("/");
|
||||
let response = request.send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
error: The #[route(..)] macro requires at least one `method` attribute
|
||||
--> $DIR/route-missing-method-fail.rs:3:1
|
||||
|
|
||||
3 | #[route("/")]
|
||||
| ^^^^^^^^^^^^^
|
||||
|
|
||||
= 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:12:49
|
||||
|
|
||||
12 | let srv = test::start(|| App::new().service(index));
|
||||
| ^^^^^ not found in this scope
|
17
actix-web-codegen/tests/trybuild/route-ok.rs
Normal file
17
actix-web-codegen/tests/trybuild/route-ok.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use actix_web_codegen::*;
|
||||
|
||||
#[route("/", method="GET", method="HEAD")]
|
||||
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("/");
|
||||
let response = request.send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
use actix_web_codegen::*;
|
||||
|
||||
#[route("/", method="UNEXPECTED")]
|
||||
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("/");
|
||||
let response = request.send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
error: Unexpected HTTP method: `UNEXPECTED`
|
||||
--> $DIR/route-unexpected-method-fail.rs:3:21
|
||||
|
|
||||
3 | #[route("/", method="UNEXPECTED")]
|
||||
| ^^^^^^^^^^^^
|
||||
|
||||
error[E0425]: cannot find value `index` in this scope
|
||||
--> $DIR/route-unexpected-method-fail.rs:12:49
|
||||
|
|
||||
12 | let srv = test::start(|| App::new().service(index));
|
||||
| ^^^^^ not found in this scope
|
30
actix-web-codegen/tests/trybuild/simple-fail.rs
Normal file
30
actix-web-codegen/tests/trybuild/simple-fail.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use actix_web_codegen::*;
|
||||
|
||||
#[get("/one", other)]
|
||||
async fn one() -> String {
|
||||
"Hello World!".to_owned()
|
||||
}
|
||||
|
||||
#[post(/two)]
|
||||
async fn two() -> String {
|
||||
"Hello World!".to_owned()
|
||||
}
|
||||
|
||||
static PATCH_PATH: &str = "/three";
|
||||
|
||||
#[patch(PATCH_PATH)]
|
||||
async fn three() -> String {
|
||||
"Hello World!".to_owned()
|
||||
}
|
||||
|
||||
#[delete("/four", "/five")]
|
||||
async fn four() -> String {
|
||||
"Hello World!".to_owned()
|
||||
}
|
||||
|
||||
#[delete("/five", method="GET")]
|
||||
async fn five() -> String {
|
||||
"Hello World!".to_owned()
|
||||
}
|
||||
|
||||
fn main() {}
|
29
actix-web-codegen/tests/trybuild/simple-fail.stderr
Normal file
29
actix-web-codegen/tests/trybuild/simple-fail.stderr
Normal file
@@ -0,0 +1,29 @@
|
||||
error: Unknown attribute.
|
||||
--> $DIR/simple-fail.rs:3:15
|
||||
|
|
||||
3 | #[get("/one", other)]
|
||||
| ^^^^^
|
||||
|
||||
error: expected identifier or literal
|
||||
--> $DIR/simple-fail.rs:8:8
|
||||
|
|
||||
8 | #[post(/two)]
|
||||
| ^
|
||||
|
||||
error: Unknown attribute.
|
||||
--> $DIR/simple-fail.rs:15:9
|
||||
|
|
||||
15 | #[patch(PATCH_PATH)]
|
||||
| ^^^^^^^^^^
|
||||
|
||||
error: Multiple paths specified! Should be only one!
|
||||
--> $DIR/simple-fail.rs:20:19
|
||||
|
|
||||
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")]
|
||||
| ^^^^^^^^^^^^
|
16
actix-web-codegen/tests/trybuild/simple.rs
Normal file
16
actix-web-codegen/tests/trybuild/simple.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use actix_web::{Responder, HttpResponse, App, test};
|
||||
use actix_web_codegen::*;
|
||||
|
||||
#[get("/config")]
|
||||
async fn config() -> impl Responder {
|
||||
HttpResponse::Ok()
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() {
|
||||
let srv = test::start(|| App::new().service(config));
|
||||
|
||||
let request = srv.get("/config");
|
||||
let response = request.send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
}
|
@@ -3,6 +3,11 @@
|
||||
## Unreleased - 2020-xx-xx
|
||||
|
||||
|
||||
## 2.0.0 - 2020-09-11
|
||||
### Changed
|
||||
* `Client::build` was renamed to `Client::builder`.
|
||||
|
||||
|
||||
## 2.0.0-beta.4 - 2020-09-09
|
||||
### Changed
|
||||
* Update actix-codec & actix-tls dependencies.
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "awc"
|
||||
version = "2.0.0-beta.4"
|
||||
version = "2.0.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Async HTTP client library that uses the Actix runtime."
|
||||
readme = "README.md"
|
||||
@@ -38,8 +38,8 @@ compress = ["actix-http/compress"]
|
||||
|
||||
[dependencies]
|
||||
actix-codec = "0.3.0"
|
||||
actix-service = "1.0.1"
|
||||
actix-http = "2.0.0-beta.4"
|
||||
actix-service = "1.0.6"
|
||||
actix-http = "2.0.0"
|
||||
actix-rt = "1.0.0"
|
||||
|
||||
base64 = "0.12"
|
||||
@@ -58,9 +58,9 @@ rust-tls = { version = "0.18.0", package = "rustls", optional = true, features =
|
||||
|
||||
[dev-dependencies]
|
||||
actix-connect = { version = "2.0.0", features = ["openssl"] }
|
||||
actix-web = { version = "3.0.0-beta.4", features = ["openssl"] }
|
||||
actix-http = { version = "2.0.0-beta.4", features = ["openssl"] }
|
||||
actix-http-test = { version = "2.0.0-alpha.1", features = ["openssl"] }
|
||||
actix-web = { version = "3.0.0", features = ["openssl"] }
|
||||
actix-http = { version = "2.0.0", features = ["openssl"] }
|
||||
actix-http-test = { version = "2.0.0", features = ["openssl"] }
|
||||
actix-utils = "2.0.0"
|
||||
actix-server = "1.0.0"
|
||||
actix-tls = { version = "2.0.0", features = ["openssl", "rustls"] }
|
||||
|
@@ -1,4 +1,4 @@
|
||||
#![warn(rust_2018_idioms, warnings)]
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![allow(
|
||||
clippy::type_complexity,
|
||||
clippy::borrow_interior_mutable_const,
|
||||
@@ -166,8 +166,9 @@ impl Client {
|
||||
Client::default()
|
||||
}
|
||||
|
||||
/// Build client instance.
|
||||
pub fn build() -> ClientBuilder {
|
||||
/// Create `Client` builder.
|
||||
/// This function is equivalent of `ClientBuilder::new()`.
|
||||
pub fn builder() -> ClientBuilder {
|
||||
ClientBuilder::new()
|
||||
}
|
||||
|
||||
|
@@ -623,7 +623,7 @@ mod tests {
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_client_header() {
|
||||
let req = Client::build()
|
||||
let req = Client::builder()
|
||||
.header(header::CONTENT_TYPE, "111")
|
||||
.finish()
|
||||
.get("/");
|
||||
@@ -641,7 +641,7 @@ mod tests {
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_client_header_override() {
|
||||
let req = Client::build()
|
||||
let req = Client::builder()
|
||||
.header(header::CONTENT_TYPE, "111")
|
||||
.finish()
|
||||
.get("/")
|
||||
|
@@ -193,11 +193,7 @@ impl RequestSender {
|
||||
}
|
||||
};
|
||||
|
||||
SendClientRequest::new(
|
||||
fut,
|
||||
response_decompress,
|
||||
timeout.or_else(|| config.timeout),
|
||||
)
|
||||
SendClientRequest::new(fut, response_decompress, timeout.or(config.timeout))
|
||||
}
|
||||
|
||||
pub(crate) fn send_json<T: Serialize>(
|
||||
|
@@ -434,7 +434,7 @@ mod tests {
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_header_override() {
|
||||
let req = Client::build()
|
||||
let req = Client::builder()
|
||||
.header(header::CONTENT_TYPE, "111")
|
||||
.finish()
|
||||
.ws("/")
|
||||
|
@@ -120,7 +120,7 @@ async fn test_timeout() {
|
||||
.timeout(Duration::from_secs(15))
|
||||
.finish();
|
||||
|
||||
let client = awc::Client::build()
|
||||
let client = awc::Client::builder()
|
||||
.connector(connector)
|
||||
.timeout(Duration::from_millis(50))
|
||||
.finish();
|
||||
@@ -141,7 +141,7 @@ async fn test_timeout_override() {
|
||||
})))
|
||||
});
|
||||
|
||||
let client = awc::Client::build()
|
||||
let client = awc::Client::builder()
|
||||
.timeout(Duration::from_millis(50000))
|
||||
.finish();
|
||||
let request = client
|
||||
@@ -291,7 +291,7 @@ async fn test_connection_wait_queue() {
|
||||
})
|
||||
.await;
|
||||
|
||||
let client = awc::Client::build()
|
||||
let client = awc::Client::builder()
|
||||
.connector(awc::Connector::new().limit(1).finish())
|
||||
.finish();
|
||||
|
||||
@@ -340,7 +340,7 @@ async fn test_connection_wait_queue_force_close() {
|
||||
})
|
||||
.await;
|
||||
|
||||
let client = awc::Client::build()
|
||||
let client = awc::Client::builder()
|
||||
.connector(awc::Connector::new().limit(1).finish())
|
||||
.finish();
|
||||
|
||||
|
@@ -47,7 +47,7 @@ async fn test_connection_window_size() {
|
||||
.set_alpn_protos(b"\x02h2\x08http/1.1")
|
||||
.map_err(|e| log::error!("Can not set alpn protocol: {:?}", e));
|
||||
|
||||
let client = awc::Client::build()
|
||||
let client = awc::Client::builder()
|
||||
.connector(awc::Connector::new().ssl(builder.build()).finish())
|
||||
.initial_window_size(100)
|
||||
.initial_connection_window_size(100)
|
||||
|
@@ -82,7 +82,7 @@ async fn _test_connection_reuse_h2() {
|
||||
.dangerous()
|
||||
.set_certificate_verifier(Arc::new(danger::NoCertificateVerification {}));
|
||||
|
||||
let client = awc::Client::build()
|
||||
let client = awc::Client::builder()
|
||||
.connector(awc::Connector::new().rustls(Arc::new(config)).finish())
|
||||
.finish();
|
||||
|
||||
|
@@ -62,7 +62,7 @@ async fn test_connection_reuse_h2() {
|
||||
.set_alpn_protos(b"\x02h2\x08http/1.1")
|
||||
.map_err(|e| log::error!("Can not set alpn protocol: {:?}", e));
|
||||
|
||||
let client = awc::Client::build()
|
||||
let client = awc::Client::builder()
|
||||
.connector(awc::Connector::new().ssl(builder.build()).finish())
|
||||
.finish();
|
||||
|
||||
|
10
src/lib.rs
10
src/lib.rs
@@ -1,6 +1,3 @@
|
||||
#![warn(rust_2018_idioms, warnings)]
|
||||
#![allow(clippy::needless_doctest_main, clippy::type_complexity)]
|
||||
|
||||
//! Actix web is a powerful, pragmatic, and extremely fast web framework for Rust.
|
||||
//!
|
||||
//! ## Example
|
||||
@@ -59,7 +56,7 @@
|
||||
//! * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
|
||||
//! * Includes an async [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html)
|
||||
//! * Supports [Actix actor framework](https://github.com/actix/actix)
|
||||
//! * Runs on stable Rust 1.41+
|
||||
//! * Runs on stable Rust 1.42+
|
||||
//!
|
||||
//! ## Crate Features
|
||||
//!
|
||||
@@ -68,6 +65,11 @@
|
||||
//! * `rustls` - HTTPS support via `rustls` crate, supports `HTTP/2`
|
||||
//! * `secure-cookies` - secure cookies support
|
||||
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![allow(clippy::needless_doctest_main, clippy::type_complexity)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
|
||||
mod app;
|
||||
mod app_service;
|
||||
mod config;
|
||||
|
@@ -9,7 +9,7 @@ mod condition;
|
||||
mod defaultheaders;
|
||||
pub mod errhandlers;
|
||||
mod logger;
|
||||
mod normalize;
|
||||
pub mod normalize;
|
||||
|
||||
pub use self::condition::Condition;
|
||||
pub use self::defaultheaders::DefaultHeaders;
|
||||
|
@@ -79,6 +79,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct NormalizePathNormalization<S> {
|
||||
service: S,
|
||||
merge_slash: Regex,
|
||||
@@ -113,6 +114,10 @@ where
|
||||
// normalize multiple /'s to one /
|
||||
let path = self.merge_slash.replace_all(&path, "/");
|
||||
|
||||
// Ensure root paths are still resolvable. If resulting path is blank after previous step
|
||||
// it means the path was one or more slashes. Reduce to single slash.
|
||||
let path = if path.is_empty() { "/" } else { path.as_ref() };
|
||||
|
||||
// Check whether the path has been changed
|
||||
//
|
||||
// This check was previously implemented as string length comparison
|
||||
@@ -158,10 +163,23 @@ mod tests {
|
||||
let mut app = init_service(
|
||||
App::new()
|
||||
.wrap(NormalizePath::default())
|
||||
.service(web::resource("/").to(HttpResponse::Ok))
|
||||
.service(web::resource("/v1/something/").to(HttpResponse::Ok)),
|
||||
)
|
||||
.await;
|
||||
|
||||
let req = TestRequest::with_uri("/").to_request();
|
||||
let res = call_service(&mut app, req).await;
|
||||
assert!(res.status().is_success());
|
||||
|
||||
let req = TestRequest::with_uri("/?query=test").to_request();
|
||||
let res = call_service(&mut app, req).await;
|
||||
assert!(res.status().is_success());
|
||||
|
||||
let req = TestRequest::with_uri("///").to_request();
|
||||
let res = call_service(&mut app, req).await;
|
||||
assert!(res.status().is_success());
|
||||
|
||||
let req = TestRequest::with_uri("/v1//something////").to_request();
|
||||
let res = call_service(&mut app, req).await;
|
||||
assert!(res.status().is_success());
|
||||
@@ -184,10 +202,24 @@ mod tests {
|
||||
let mut app = init_service(
|
||||
App::new()
|
||||
.wrap(NormalizePath(TrailingSlash::Trim))
|
||||
.service(web::resource("/").to(HttpResponse::Ok))
|
||||
.service(web::resource("/v1/something").to(HttpResponse::Ok)),
|
||||
)
|
||||
.await;
|
||||
|
||||
// root paths should still work
|
||||
let req = TestRequest::with_uri("/").to_request();
|
||||
let res = call_service(&mut app, req).await;
|
||||
assert!(res.status().is_success());
|
||||
|
||||
let req = TestRequest::with_uri("/?query=test").to_request();
|
||||
let res = call_service(&mut app, req).await;
|
||||
assert!(res.status().is_success());
|
||||
|
||||
let req = TestRequest::with_uri("///").to_request();
|
||||
let res = call_service(&mut app, req).await;
|
||||
assert!(res.status().is_success());
|
||||
|
||||
let req = TestRequest::with_uri("/v1/something////").to_request();
|
||||
let res = call_service(&mut app, req).await;
|
||||
assert!(res.status().is_success());
|
||||
|
@@ -368,7 +368,7 @@ mod tests {
|
||||
}))
|
||||
.route(web::post().to(|| async {
|
||||
delay_for(Duration::from_millis(100)).await;
|
||||
HttpResponse::Created()
|
||||
Ok::<_, ()>(HttpResponse::Created())
|
||||
}))
|
||||
.route(web::delete().to(|| async {
|
||||
delay_for(Duration::from_millis(100)).await;
|
||||
|
10
src/scope.rs
10
src/scope.rs
@@ -826,7 +826,7 @@ mod tests {
|
||||
async fn test_scope_variable_segment() {
|
||||
let mut srv =
|
||||
init_service(App::new().service(web::scope("/ab-{project}").service(
|
||||
web::resource("/path1").to(|r: HttpRequest| async move {
|
||||
web::resource("/path1").to(|r: HttpRequest| {
|
||||
HttpResponse::Ok()
|
||||
.body(format!("project: {}", &r.match_info()["project"]))
|
||||
}),
|
||||
@@ -926,7 +926,7 @@ mod tests {
|
||||
async fn test_nested_scope_with_variable_segment() {
|
||||
let mut srv = init_service(App::new().service(web::scope("/app").service(
|
||||
web::scope("/{project_id}").service(web::resource("/path1").to(
|
||||
|r: HttpRequest| async move {
|
||||
|r: HttpRequest| {
|
||||
HttpResponse::Created()
|
||||
.body(format!("project: {}", &r.match_info()["project_id"]))
|
||||
},
|
||||
@@ -951,7 +951,7 @@ mod tests {
|
||||
async fn test_nested2_scope_with_variable_segment() {
|
||||
let mut srv = init_service(App::new().service(web::scope("/app").service(
|
||||
web::scope("/{project}").service(web::scope("/{id}").service(
|
||||
web::resource("/path1").to(|r: HttpRequest| async move {
|
||||
web::resource("/path1").to(|r: HttpRequest| {
|
||||
HttpResponse::Created().body(format!(
|
||||
"project: {} - {}",
|
||||
&r.match_info()["project"],
|
||||
@@ -1178,7 +1178,7 @@ mod tests {
|
||||
);
|
||||
s.route(
|
||||
"/",
|
||||
web::get().to(|req: HttpRequest| async move {
|
||||
web::get().to(|req: HttpRequest| {
|
||||
HttpResponse::Ok().body(
|
||||
req.url_for("youtube", &["xxxxxx"]).unwrap().to_string(),
|
||||
)
|
||||
@@ -1199,7 +1199,7 @@ mod tests {
|
||||
async fn test_url_for_nested() {
|
||||
let mut srv = init_service(App::new().service(web::scope("/a").service(
|
||||
web::scope("/b").service(web::resource("/c/{stuff}").name("c").route(
|
||||
web::get().to(|req: HttpRequest| async move {
|
||||
web::get().to(|req: HttpRequest| {
|
||||
HttpResponse::Ok()
|
||||
.body(format!("{}", req.url_for("c", &["12345"]).unwrap()))
|
||||
}),
|
||||
|
138
src/test.rs
138
src/test.rs
@@ -822,7 +822,7 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
Client::build().connector(connector).finish()
|
||||
Client::builder().connector(connector).finish()
|
||||
};
|
||||
|
||||
TestServer {
|
||||
@@ -1072,14 +1072,9 @@ mod tests {
|
||||
let mut app = init_service(
|
||||
App::new().service(
|
||||
web::resource("/index.html")
|
||||
.route(web::put().to(|| async { HttpResponse::Ok().body("put!") }))
|
||||
.route(
|
||||
web::patch().to(|| async { HttpResponse::Ok().body("patch!") }),
|
||||
)
|
||||
.route(
|
||||
web::delete()
|
||||
.to(|| async { HttpResponse::Ok().body("delete!") }),
|
||||
),
|
||||
.route(web::put().to(|| HttpResponse::Ok().body("put!")))
|
||||
.route(web::patch().to(|| HttpResponse::Ok().body("patch!")))
|
||||
.route(web::delete().to(|| HttpResponse::Ok().body("delete!"))),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
@@ -1107,11 +1102,13 @@ mod tests {
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_response() {
|
||||
let mut app =
|
||||
init_service(App::new().service(web::resource("/index.html").route(
|
||||
web::post().to(|| async { HttpResponse::Ok().body("welcome!") }),
|
||||
)))
|
||||
.await;
|
||||
let mut app = init_service(
|
||||
App::new().service(
|
||||
web::resource("/index.html")
|
||||
.route(web::post().to(|| HttpResponse::Ok().body("welcome!"))),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
let req = TestRequest::post()
|
||||
.uri("/index.html")
|
||||
@@ -1124,11 +1121,13 @@ mod tests {
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_send_request() {
|
||||
let mut app =
|
||||
init_service(App::new().service(web::resource("/index.html").route(
|
||||
web::get().to(|| async { HttpResponse::Ok().body("welcome!") }),
|
||||
)))
|
||||
.await;
|
||||
let mut app = init_service(
|
||||
App::new().service(
|
||||
web::resource("/index.html")
|
||||
.route(web::get().to(|| HttpResponse::Ok().body("welcome!"))),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
let resp = TestRequest::get()
|
||||
.uri("/index.html")
|
||||
@@ -1148,7 +1147,7 @@ mod tests {
|
||||
#[actix_rt::test]
|
||||
async fn test_response_json() {
|
||||
let mut app = init_service(App::new().service(web::resource("/people").route(
|
||||
web::post().to(|person: web::Json<Person>| async {
|
||||
web::post().to(|person: web::Json<Person>| {
|
||||
HttpResponse::Ok().json(person.into_inner())
|
||||
}),
|
||||
)))
|
||||
@@ -1169,7 +1168,7 @@ mod tests {
|
||||
#[actix_rt::test]
|
||||
async fn test_body_json() {
|
||||
let mut app = init_service(App::new().service(web::resource("/people").route(
|
||||
web::post().to(|person: web::Json<Person>| async {
|
||||
web::post().to(|person: web::Json<Person>| {
|
||||
HttpResponse::Ok().json(person.into_inner())
|
||||
}),
|
||||
)))
|
||||
@@ -1191,7 +1190,7 @@ mod tests {
|
||||
#[actix_rt::test]
|
||||
async fn test_request_response_form() {
|
||||
let mut app = init_service(App::new().service(web::resource("/people").route(
|
||||
web::post().to(|person: web::Form<Person>| async {
|
||||
web::post().to(|person: web::Form<Person>| {
|
||||
HttpResponse::Ok().json(person.into_inner())
|
||||
}),
|
||||
)))
|
||||
@@ -1217,7 +1216,7 @@ mod tests {
|
||||
#[actix_rt::test]
|
||||
async fn test_request_response_json() {
|
||||
let mut app = init_service(App::new().service(web::resource("/people").route(
|
||||
web::post().to(|person: web::Json<Person>| async {
|
||||
web::post().to(|person: web::Json<Person>| {
|
||||
HttpResponse::Ok().json(person.into_inner())
|
||||
}),
|
||||
)))
|
||||
@@ -1282,53 +1281,54 @@ mod tests {
|
||||
assert!(res.status().is_success());
|
||||
}
|
||||
|
||||
/*
|
||||
#[actix_rt::test]
|
||||
async fn test_actor() {
|
||||
use crate::Error;
|
||||
use actix::prelude::*;
|
||||
|
||||
Comment out until actix decoupled of actix-http:
|
||||
https://github.com/actix/actix/issues/321
|
||||
struct MyActor;
|
||||
|
||||
use futures::FutureExt;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_actor() {
|
||||
use actix::Actor;
|
||||
|
||||
struct MyActor;
|
||||
|
||||
struct Num(usize);
|
||||
impl actix::Message for Num {
|
||||
type Result = usize;
|
||||
}
|
||||
impl actix::Actor for MyActor {
|
||||
type Context = actix::Context<Self>;
|
||||
}
|
||||
impl actix::Handler<Num> for MyActor {
|
||||
type Result = usize;
|
||||
fn handle(&mut self, msg: Num, _: &mut Self::Context) -> Self::Result {
|
||||
msg.0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let mut app = init_service(App::new().service(web::resource("/index.html").to(
|
||||
move || {
|
||||
addr.send(Num(1)).map(|res| match res {
|
||||
Ok(res) => {
|
||||
if res == 1 {
|
||||
Ok(HttpResponse::Ok())
|
||||
} else {
|
||||
Ok(HttpResponse::BadRequest())
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
})
|
||||
},
|
||||
)))
|
||||
.await;
|
||||
|
||||
let req = TestRequest::post().uri("/index.html").to_request();
|
||||
let res = app.call(req).await.unwrap();
|
||||
assert!(res.status().is_success());
|
||||
impl Actor for MyActor {
|
||||
type Context = Context<Self>;
|
||||
}
|
||||
*/
|
||||
|
||||
struct Num(usize);
|
||||
|
||||
impl Message for Num {
|
||||
type Result = usize;
|
||||
}
|
||||
|
||||
impl Handler<Num> for MyActor {
|
||||
type Result = usize;
|
||||
|
||||
fn handle(&mut self, msg: Num, _: &mut Self::Context) -> Self::Result {
|
||||
msg.0
|
||||
}
|
||||
}
|
||||
|
||||
let addr = MyActor.start();
|
||||
|
||||
async fn actor_handler(
|
||||
addr: Data<Addr<MyActor>>,
|
||||
) -> Result<impl Responder, Error> {
|
||||
// `?` operator tests "actors" feature flag on actix-http
|
||||
let res = addr.send(Num(1)).await?;
|
||||
|
||||
if res == 1 {
|
||||
Ok(HttpResponse::Ok())
|
||||
} else {
|
||||
Ok(HttpResponse::BadRequest())
|
||||
}
|
||||
}
|
||||
|
||||
let srv = App::new()
|
||||
.data(addr.clone())
|
||||
.service(web::resource("/").to(actor_handler));
|
||||
|
||||
let mut app = init_service(srv).await;
|
||||
|
||||
let req = TestRequest::post().uri("/").to_request();
|
||||
let res = app.call(req).await.unwrap();
|
||||
assert!(res.status().is_success());
|
||||
}
|
||||
}
|
||||
|
@@ -195,7 +195,7 @@ impl<T: Serialize> Responder for Form<T> {
|
||||
/// web::resource("/index.html")
|
||||
/// // change `Form` extractor configuration
|
||||
/// .app_data(
|
||||
/// web::Form::<FormData>::configure(|cfg| cfg.limit(4097))
|
||||
/// web::FormConfig::default().limit(4097)
|
||||
/// )
|
||||
/// .route(web::get().to(index))
|
||||
/// );
|
||||
|
@@ -209,10 +209,10 @@ where
|
||||
|
||||
/// Json extractor configuration
|
||||
///
|
||||
/// # Examples
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_web::{error, web, App, FromRequest, HttpRequest, HttpResponse};
|
||||
/// use actix_web::{error, web, App, FromRequest, HttpResponse};
|
||||
/// use serde_derive::Deserialize;
|
||||
///
|
||||
/// #[derive(Deserialize)]
|
||||
@@ -225,35 +225,26 @@ where
|
||||
/// format!("Welcome {}!", info.username)
|
||||
/// }
|
||||
///
|
||||
/// /// Return either a 400 or 415, and include the error message from serde
|
||||
/// /// in the response body
|
||||
/// fn json_error_handler(err: error::JsonPayloadError, _req: &HttpRequest) -> error::Error {
|
||||
/// let detail = err.to_string();
|
||||
/// let response = match &err {
|
||||
/// error::JsonPayloadError::ContentType => {
|
||||
/// HttpResponse::UnsupportedMediaType().content_type("text/plain").body(detail)
|
||||
/// }
|
||||
/// _ => HttpResponse::BadRequest().content_type("text/plain").body(detail),
|
||||
/// };
|
||||
/// error::InternalError::from_response(err, response).into()
|
||||
/// }
|
||||
///
|
||||
/// fn main() {
|
||||
/// let app = App::new().service(
|
||||
/// web::resource("/index.html")
|
||||
/// .app_data(
|
||||
/// // change json extractor configuration
|
||||
/// web::Json::<Info>::configure(|cfg| {
|
||||
/// cfg.limit(4096)
|
||||
/// .content_type(|mime| { // <- accept text/plain content type
|
||||
/// mime.type_() == mime::TEXT && mime.subtype() == mime::PLAIN
|
||||
/// })
|
||||
/// .error_handler(json_error_handler) // Use our custom error response
|
||||
/// }))
|
||||
/// // Json extractor configuration for this resource.
|
||||
/// web::JsonConfig::default()
|
||||
/// .limit(4096) // Limit request payload size
|
||||
/// .content_type(|mime| { // <- accept text/plain content type
|
||||
/// mime.type_() == mime::TEXT && mime.subtype() == mime::PLAIN
|
||||
/// })
|
||||
/// .error_handler(|err, req| { // <- create custom error response
|
||||
/// error::InternalError::from_response(
|
||||
/// err, HttpResponse::Conflict().finish()).into()
|
||||
/// })
|
||||
/// )
|
||||
/// .route(web::post().to(index))
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
#[derive(Clone)]
|
||||
pub struct JsonConfig {
|
||||
limit: usize,
|
||||
|
@@ -188,12 +188,12 @@ where
|
||||
/// let app = App::new().service(
|
||||
/// web::resource("/index.html").app_data(
|
||||
/// // change query extractor configuration
|
||||
/// web::Query::<Info>::configure(|cfg| {
|
||||
/// cfg.error_handler(|err, req| { // <- create custom error response
|
||||
/// web::QueryConfig::default()
|
||||
/// .error_handler(|err, req| { // <- create custom error response
|
||||
/// error::InternalError::from_response(
|
||||
/// err, HttpResponse::Conflict().finish()).into()
|
||||
/// })
|
||||
/// }))
|
||||
/// )
|
||||
/// .route(web::post().to(index))
|
||||
/// );
|
||||
/// }
|
||||
|
@@ -1,10 +1,15 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2020-xx-xx
|
||||
|
||||
* add ability to set address for `TestServer` [#1645]
|
||||
|
||||
[#1645]: https://github.com/actix/actix-web/pull/1645
|
||||
|
||||
## 2.0.0 - 2020-09-11
|
||||
* Update actix-codec and actix-utils dependencies.
|
||||
|
||||
## [2.0.0-alpha.1] - 2020-05-23
|
||||
|
||||
## 2.0.0-alpha.1 - 2020-05-23
|
||||
* Update the `time` dependency to 0.2.7
|
||||
* Update `actix-connect` dependency to 2.0.0-alpha.2
|
||||
* Make `test_server` `async` fn.
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-http-test"
|
||||
version = "2.0.0-alpha.1"
|
||||
version = "2.0.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix HTTP test server"
|
||||
readme = "README.md"
|
||||
@@ -29,14 +29,14 @@ default = []
|
||||
openssl = ["open-ssl", "awc/openssl"]
|
||||
|
||||
[dependencies]
|
||||
actix-service = "1.0.1"
|
||||
actix-service = "1.0.6"
|
||||
actix-codec = "0.3.0"
|
||||
actix-connect = "2.0.0"
|
||||
actix-utils = "2.0.0"
|
||||
actix-rt = "1.0.0"
|
||||
actix-rt = "1.1.1"
|
||||
actix-server = "1.0.0"
|
||||
actix-testing = "1.0.0"
|
||||
awc = "2.0.0-beta.4"
|
||||
awc = "2.0.0"
|
||||
|
||||
base64 = "0.12"
|
||||
bytes = "0.5.3"
|
||||
@@ -52,5 +52,5 @@ time = { version = "0.2.7", default-features = false, features = ["std"] }
|
||||
open-ssl = { version = "0.10", package = "openssl", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-web = "3.0.0-beta.4"
|
||||
actix-http = "2.0.0-beta.4"
|
||||
actix-web = "3.0.0"
|
||||
actix-http = "2.0.0"
|
||||
|
@@ -44,12 +44,20 @@ pub use actix_testing::*;
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn test_server<F: ServiceFactory<TcpStream>>(factory: F) -> TestServer {
|
||||
let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
test_server_with_addr(tcp, factory).await
|
||||
}
|
||||
|
||||
/// Start [`test server`](./fn.test_server.html) on a concrete Address
|
||||
pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
|
||||
tcp: net::TcpListener,
|
||||
factory: F,
|
||||
) -> TestServer {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
// run server in separate thread
|
||||
thread::spawn(move || {
|
||||
let sys = System::new("actix-test-server");
|
||||
let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let local_addr = tcp.local_addr().unwrap();
|
||||
|
||||
Server::build()
|
||||
@@ -90,7 +98,7 @@ pub async fn test_server<F: ServiceFactory<TcpStream>>(factory: F) -> TestServer
|
||||
}
|
||||
};
|
||||
|
||||
Client::build().connector(connector).finish()
|
||||
Client::builder().connector(connector).finish()
|
||||
};
|
||||
actix_connect::start_default_resolver().await.unwrap();
|
||||
|
||||
|
@@ -43,7 +43,7 @@ async fn test_start() {
|
||||
{
|
||||
use actix_http::client;
|
||||
|
||||
let client = awc::Client::build()
|
||||
let client = awc::Client::builder()
|
||||
.connector(
|
||||
client::Connector::new()
|
||||
.timeout(Duration::from_millis(100))
|
||||
@@ -115,7 +115,7 @@ async fn test_start_ssl() {
|
||||
.set_alpn_protos(b"\x02h2\x08http/1.1")
|
||||
.map_err(|e| log::error!("Can not set alpn protocol: {:?}", e));
|
||||
|
||||
let client = awc::Client::build()
|
||||
let client = awc::Client::builder()
|
||||
.connector(
|
||||
awc::Connector::new()
|
||||
.ssl(builder.build())
|
||||
|
@@ -16,7 +16,8 @@ use futures_util::ready;
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
|
||||
use actix_web::dev::BodyEncoding;
|
||||
use actix_web::middleware::Compress;
|
||||
use actix_web::middleware::normalize::TrailingSlash;
|
||||
use actix_web::middleware::{Compress, NormalizePath};
|
||||
use actix_web::{dev, test, web, App, Error, HttpResponse};
|
||||
|
||||
const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
|
||||
@@ -866,6 +867,20 @@ async fn test_slow_request() {
|
||||
assert!(data.starts_with("HTTP/1.1 408 Request Timeout"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_normalize() {
|
||||
let srv = test::start_with(test::config().h1(), || {
|
||||
App::new()
|
||||
.wrap(NormalizePath::new(TrailingSlash::Trim))
|
||||
.service(
|
||||
web::resource("/one").route(web::to(|| HttpResponse::Ok().finish())),
|
||||
)
|
||||
});
|
||||
|
||||
let response = srv.get("/one/").send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
}
|
||||
|
||||
// #[cfg(feature = "openssl")]
|
||||
// #[actix_rt::test]
|
||||
// async fn test_ssl_handshake_timeout() {
|
||||
|
Reference in New Issue
Block a user