mirror of
https://github.com/actix/actix-web.git
synced 2024-12-17 21:56:38 +00:00
add setup to commpress middleware
This commit is contained in:
parent
6627109984
commit
21219e0843
2 changed files with 200 additions and 27 deletions
|
@ -30,6 +30,22 @@ use crate::{
|
|||
|
||||
const MAX_CHUNK_SIZE_ENCODE_IN_PLACE: usize = 1024;
|
||||
|
||||
const DEFLATE_MIN_LEVEL: u32 = 0;
|
||||
const DEFLATE_MAX_LEVEL: u32 = 9;
|
||||
const DEFLATE_DEFAULT: u32 = 1;
|
||||
|
||||
const GZIP_MIN_LEVEL: u32 = 0;
|
||||
const GZIP_MAX_LEVEL: u32 = 9;
|
||||
const GZIP_DEFAULT: u32 = 1;
|
||||
|
||||
const BROTLI_MIN_QUALITY: u32 = 0;
|
||||
const BROTLI_MAX_QUALITY: u32 = 11;
|
||||
const BROTLI_DEFAULT: u32 = 3;
|
||||
|
||||
const ZSTD_MIN_LEVEL: i32 = 0;
|
||||
const ZSTD_MAX_LEVEL: i32 = 22;
|
||||
const ZSTD_DEFAULT: i32 = 3;
|
||||
|
||||
pin_project! {
|
||||
pub struct Encoder<B> {
|
||||
#[pin]
|
||||
|
@ -53,6 +69,15 @@ impl<B: MessageBody> Encoder<B> {
|
|||
}
|
||||
|
||||
pub fn response(encoding: ContentEncoding, head: &mut ResponseHead, body: B) -> Self {
|
||||
Encoder::response_with_level(encoding, head, body, None)
|
||||
}
|
||||
|
||||
pub fn response_with_level(
|
||||
encoding: ContentEncoding,
|
||||
head: &mut ResponseHead,
|
||||
body: B,
|
||||
level: Option<u32>,
|
||||
) -> Self {
|
||||
// no need to compress an empty body
|
||||
if matches!(body.size(), BodySize::None) {
|
||||
return Self::none();
|
||||
|
@ -69,8 +94,9 @@ impl<B: MessageBody> Encoder<B> {
|
|||
};
|
||||
|
||||
if should_encode {
|
||||
let enconding_level = ContentEncodingWithLevel::new(encoding, level);
|
||||
// wrap body only if encoder is feature-enabled
|
||||
if let Some(enc) = ContentEncoder::select(encoding) {
|
||||
if let Some(enc) = ContentEncoder::select(enconding_level) {
|
||||
update_head(encoding, head);
|
||||
|
||||
return Encoder {
|
||||
|
@ -278,27 +304,73 @@ enum ContentEncoder {
|
|||
Zstd(ZstdEncoder<'static, Writer>),
|
||||
}
|
||||
|
||||
enum ContentEncodingWithLevel {
|
||||
Deflate(u32),
|
||||
Gzip(u32),
|
||||
Brotli(u32),
|
||||
Zstd(i32),
|
||||
Identity,
|
||||
}
|
||||
|
||||
impl ContentEncodingWithLevel {
|
||||
pub fn new(encoding: ContentEncoding, level: Option<u32>) -> Self {
|
||||
match encoding {
|
||||
ContentEncoding::Deflate => {
|
||||
let level = level
|
||||
.filter(|l| (DEFLATE_MIN_LEVEL..(DEFLATE_MAX_LEVEL + 1)).contains(l))
|
||||
.unwrap_or(DEFLATE_DEFAULT);
|
||||
ContentEncodingWithLevel::Deflate(level)
|
||||
}
|
||||
ContentEncoding::Gzip => {
|
||||
let level = level
|
||||
.filter(|l| (GZIP_MIN_LEVEL..(GZIP_MAX_LEVEL + 1)).contains(l))
|
||||
.unwrap_or(GZIP_DEFAULT);
|
||||
ContentEncodingWithLevel::Gzip(level)
|
||||
}
|
||||
ContentEncoding::Brotli => {
|
||||
let level = level
|
||||
.filter(|l| (BROTLI_MIN_QUALITY..(BROTLI_MAX_QUALITY + 1)).contains(l))
|
||||
.unwrap_or(BROTLI_DEFAULT);
|
||||
ContentEncodingWithLevel::Brotli(level)
|
||||
}
|
||||
ContentEncoding::Zstd => {
|
||||
let level = level
|
||||
.map(|l| l as i32)
|
||||
.filter(|l| (ZSTD_MIN_LEVEL..(ZSTD_MAX_LEVEL + 1)).contains(l))
|
||||
.unwrap_or(ZSTD_DEFAULT);
|
||||
ContentEncodingWithLevel::Zstd(level)
|
||||
}
|
||||
ContentEncoding::Identity => ContentEncodingWithLevel::Identity,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContentEncoder {
|
||||
fn select(encoding: ContentEncoding) -> Option<Self> {
|
||||
fn select(encoding: ContentEncodingWithLevel) -> Option<Self> {
|
||||
match encoding {
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
ContentEncoding::Deflate => Some(ContentEncoder::Deflate(ZlibEncoder::new(
|
||||
Writer::new(),
|
||||
flate2::Compression::fast(),
|
||||
))),
|
||||
ContentEncodingWithLevel::Deflate(level) => Some(ContentEncoder::Deflate(
|
||||
ZlibEncoder::new(Writer::new(), flate2::Compression::new(level)),
|
||||
)),
|
||||
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
ContentEncoding::Gzip => Some(ContentEncoder::Gzip(GzEncoder::new(
|
||||
Writer::new(),
|
||||
flate2::Compression::fast(),
|
||||
))),
|
||||
ContentEncodingWithLevel::Gzip(level) => Some(ContentEncoder::Gzip(
|
||||
GzEncoder::new(Writer::new(), flate2::Compression::new(level)),
|
||||
)),
|
||||
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
ContentEncoding::Brotli => Some(ContentEncoder::Brotli(new_brotli_compressor())),
|
||||
ContentEncodingWithLevel::Brotli(level) => Some(ContentEncoder::Brotli(Box::new(
|
||||
brotli::CompressorWriter::new(
|
||||
Writer::new(),
|
||||
32 * 1024, // 32 KiB buffer
|
||||
level, // BROTLI_PARAM_QUALITY
|
||||
22, // BROTLI_PARAM_LGWIN
|
||||
),
|
||||
))),
|
||||
|
||||
#[cfg(feature = "compress-zstd")]
|
||||
ContentEncoding::Zstd => {
|
||||
let encoder = ZstdEncoder::new(Writer::new(), 3).ok()?;
|
||||
ContentEncodingWithLevel::Zstd(level) => {
|
||||
let encoder = ZstdEncoder::new(Writer::new(), level).ok()?;
|
||||
Some(ContentEncoder::Zstd(encoder))
|
||||
}
|
||||
|
||||
|
@ -392,16 +464,6 @@ impl ContentEncoder {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
fn new_brotli_compressor() -> Box<brotli::CompressorWriter<Writer>> {
|
||||
Box::new(brotli::CompressorWriter::new(
|
||||
Writer::new(),
|
||||
32 * 1024, // 32 KiB buffer
|
||||
3, // BROTLI_PARAM_QUALITY
|
||||
22, // BROTLI_PARAM_LGWIN
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, Display)]
|
||||
#[non_exhaustive]
|
||||
pub enum EncoderError {
|
||||
|
|
|
@ -4,10 +4,11 @@ use std::{
|
|||
future::Future,
|
||||
marker::PhantomData,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use actix_http::encoding::Encoder;
|
||||
use actix_http::{encoding::Encoder, header::ContentEncoding};
|
||||
use actix_service::{Service, Transform};
|
||||
use actix_utils::future::{ok, Either, Ready};
|
||||
use futures_core::ready;
|
||||
|
@ -54,6 +55,20 @@ use crate::{
|
|||
/// .wrap(middleware::Compress::default())
|
||||
/// .default_service(web::to(|| async { HttpResponse::Ok().body("hello world") }));
|
||||
/// ```
|
||||
/// You can also set compression level for supported algorithms
|
||||
/// ```
|
||||
/// use actix_web::{middleware, web, App, HttpResponse};
|
||||
///
|
||||
/// let app = App::new()
|
||||
/// .wrap(
|
||||
/// middleware::Compress::new()
|
||||
/// .set_gzip_level(3)
|
||||
/// .set_deflate_level(1)
|
||||
/// .set_brotli_level(7)
|
||||
/// .set_zstd_level(10),
|
||||
/// )
|
||||
/// .default_service(web::to(|| async { HttpResponse::Ok().body("hello world") }));
|
||||
/// ```
|
||||
///
|
||||
/// Pre-compressed Gzip file being served from disk with correct headers added to bypass middleware:
|
||||
/// ```no_run
|
||||
|
@ -73,7 +88,71 @@ use crate::{
|
|||
/// [feature flags]: ../index.html#crate-features
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[non_exhaustive]
|
||||
pub struct Compress;
|
||||
pub struct Compress {
|
||||
inner: Rc<Inner>,
|
||||
}
|
||||
|
||||
impl Compress {
|
||||
/// Constructs new compress middleware instance with default settings.
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct Inner {
|
||||
deflate: Option<u32>,
|
||||
gzip: Option<u32>,
|
||||
brotli: Option<u32>,
|
||||
zstd: Option<u32>,
|
||||
}
|
||||
|
||||
impl Inner {
|
||||
pub fn level(&self, encoding: &ContentEncoding) -> Option<u32> {
|
||||
match encoding {
|
||||
ContentEncoding::Deflate => self.deflate,
|
||||
ContentEncoding::Gzip => self.gzip,
|
||||
ContentEncoding::Brotli => self.brotli,
|
||||
ContentEncoding::Zstd => self.zstd,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Compress {
|
||||
/// Set deflate compression level.
|
||||
///
|
||||
/// The integer here is on a scale of 0-9.
|
||||
/// When going out of range, level 1 will be used.
|
||||
pub fn set_deflate_level(mut self, value: u32) -> Self {
|
||||
Rc::get_mut(&mut self.inner).unwrap().deflate = Some(value);
|
||||
self
|
||||
}
|
||||
/// Set gzip compression level.
|
||||
///
|
||||
/// The integer here is on a scale of 0-9.
|
||||
/// When going out of range, level 1 will be used.
|
||||
pub fn set_gzip_level(mut self, value: u32) -> Self {
|
||||
Rc::get_mut(&mut self.inner).unwrap().gzip = Some(value);
|
||||
self
|
||||
}
|
||||
/// Set gzip compression level.
|
||||
///
|
||||
/// The integer here is on a scale of 0-11.
|
||||
/// When going out of range, level 3 will be used.
|
||||
pub fn set_brotli_level(mut self, value: u32) -> Self {
|
||||
Rc::get_mut(&mut self.inner).unwrap().brotli = Some(value);
|
||||
self
|
||||
}
|
||||
/// Set gzip compression level.
|
||||
///
|
||||
/// The integer here is on a scale of 0-22.
|
||||
/// When going out of range, level 3 will be used.
|
||||
pub fn set_zstd_level(mut self, value: u32) -> Self {
|
||||
Rc::get_mut(&mut self.inner).unwrap().zstd = Some(value);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, B> Transform<S, ServiceRequest> for Compress
|
||||
where
|
||||
|
@ -87,12 +166,16 @@ where
|
|||
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||
|
||||
fn new_transform(&self, service: S) -> Self::Future {
|
||||
ok(CompressMiddleware { service })
|
||||
ok(CompressMiddleware {
|
||||
service,
|
||||
inner: Rc::clone(&self.inner),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CompressMiddleware<S> {
|
||||
service: S,
|
||||
inner: Rc<Inner>,
|
||||
}
|
||||
|
||||
impl<S, B> Service<ServiceRequest> for CompressMiddleware<S>
|
||||
|
@ -111,6 +194,7 @@ where
|
|||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||
// negotiate content-encoding
|
||||
let accept_encoding = req.get_header::<AcceptEncoding>();
|
||||
let inner = self.inner.clone();
|
||||
|
||||
let accept_encoding = match accept_encoding {
|
||||
// missing header; fallback to identity
|
||||
|
@ -118,6 +202,7 @@ where
|
|||
return Either::left(CompressResponse {
|
||||
encoding: Encoding::identity(),
|
||||
fut: self.service.call(req),
|
||||
inner,
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
}
|
||||
|
@ -145,6 +230,7 @@ where
|
|||
Some(encoding) => Either::left(CompressResponse {
|
||||
fut: self.service.call(req),
|
||||
encoding,
|
||||
inner,
|
||||
_phantom: PhantomData,
|
||||
}),
|
||||
}
|
||||
|
@ -159,6 +245,7 @@ pin_project! {
|
|||
#[pin]
|
||||
fut: S::Future,
|
||||
encoding: Encoding,
|
||||
inner: Rc<Inner>,
|
||||
_phantom: PhantomData<B>,
|
||||
}
|
||||
}
|
||||
|
@ -181,9 +268,10 @@ where
|
|||
unimplemented!("encoding {} should not be here", enc);
|
||||
}
|
||||
};
|
||||
let level = this.inner.level(&enc);
|
||||
|
||||
Poll::Ready(Ok(resp.map_body(move |head, body| {
|
||||
EitherBody::left(Encoder::response(enc, head, body))
|
||||
EitherBody::left(Encoder::response_with_level(enc, head, body, level))
|
||||
})))
|
||||
}
|
||||
|
||||
|
@ -324,4 +412,27 @@ mod tests {
|
|||
assert!(vary_headers.contains(&HeaderValue::from_static("x-test")));
|
||||
assert!(vary_headers.contains(&HeaderValue::from_static("accept-encoding")));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn custom_compress_level() {
|
||||
const D: &str = "hello world ";
|
||||
const DATA: &str = const_str::repeat!(D, 100);
|
||||
|
||||
let app = test::init_service({
|
||||
App::new().wrap(Compress::new().set_gzip_level(9)).route(
|
||||
"/compress",
|
||||
web::get().to(move || HttpResponse::Ok().body(DATA)),
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
let req = test::TestRequest::default()
|
||||
.uri("/compress")
|
||||
.insert_header((header::ACCEPT_ENCODING, "gzip"))
|
||||
.to_request();
|
||||
let res = test::call_service(&app, req).await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let bytes = test::read_body(res).await;
|
||||
assert_eq!(gzip_decode(bytes), DATA.as_bytes());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue