1
0
Fork 0
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:
Denis Kayshev 2023-01-18 20:44:29 +03:00
parent 6627109984
commit 21219e0843
No known key found for this signature in database
GPG key ID: B22A36F44C0F5E19
2 changed files with 200 additions and 27 deletions

View file

@ -30,6 +30,22 @@ use crate::{
const MAX_CHUNK_SIZE_ENCODE_IN_PLACE: usize = 1024; 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! { pin_project! {
pub struct Encoder<B> { pub struct Encoder<B> {
#[pin] #[pin]
@ -53,6 +69,15 @@ impl<B: MessageBody> Encoder<B> {
} }
pub fn response(encoding: ContentEncoding, head: &mut ResponseHead, body: B) -> Self { 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 // no need to compress an empty body
if matches!(body.size(), BodySize::None) { if matches!(body.size(), BodySize::None) {
return Self::none(); return Self::none();
@ -69,8 +94,9 @@ impl<B: MessageBody> Encoder<B> {
}; };
if should_encode { if should_encode {
let enconding_level = ContentEncodingWithLevel::new(encoding, level);
// wrap body only if encoder is feature-enabled // 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); update_head(encoding, head);
return Encoder { return Encoder {
@ -278,27 +304,73 @@ enum ContentEncoder {
Zstd(ZstdEncoder<'static, Writer>), 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 { impl ContentEncoder {
fn select(encoding: ContentEncoding) -> Option<Self> { fn select(encoding: ContentEncodingWithLevel) -> Option<Self> {
match encoding { match encoding {
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
ContentEncoding::Deflate => Some(ContentEncoder::Deflate(ZlibEncoder::new( ContentEncodingWithLevel::Deflate(level) => Some(ContentEncoder::Deflate(
Writer::new(), ZlibEncoder::new(Writer::new(), flate2::Compression::new(level)),
flate2::Compression::fast(), )),
))),
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
ContentEncoding::Gzip => Some(ContentEncoder::Gzip(GzEncoder::new( ContentEncodingWithLevel::Gzip(level) => Some(ContentEncoder::Gzip(
Writer::new(), GzEncoder::new(Writer::new(), flate2::Compression::new(level)),
flate2::Compression::fast(), )),
))),
#[cfg(feature = "compress-brotli")] #[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")] #[cfg(feature = "compress-zstd")]
ContentEncoding::Zstd => { ContentEncodingWithLevel::Zstd(level) => {
let encoder = ZstdEncoder::new(Writer::new(), 3).ok()?; let encoder = ZstdEncoder::new(Writer::new(), level).ok()?;
Some(ContentEncoder::Zstd(encoder)) 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)] #[derive(Debug, Display)]
#[non_exhaustive] #[non_exhaustive]
pub enum EncoderError { pub enum EncoderError {

View file

@ -4,10 +4,11 @@ use std::{
future::Future, future::Future,
marker::PhantomData, marker::PhantomData,
pin::Pin, pin::Pin,
rc::Rc,
task::{Context, Poll}, task::{Context, Poll},
}; };
use actix_http::encoding::Encoder; use actix_http::{encoding::Encoder, header::ContentEncoding};
use actix_service::{Service, Transform}; use actix_service::{Service, Transform};
use actix_utils::future::{ok, Either, Ready}; use actix_utils::future::{ok, Either, Ready};
use futures_core::ready; use futures_core::ready;
@ -54,6 +55,20 @@ use crate::{
/// .wrap(middleware::Compress::default()) /// .wrap(middleware::Compress::default())
/// .default_service(web::to(|| async { HttpResponse::Ok().body("hello world") })); /// .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: /// Pre-compressed Gzip file being served from disk with correct headers added to bypass middleware:
/// ```no_run /// ```no_run
@ -73,7 +88,71 @@ use crate::{
/// [feature flags]: ../index.html#crate-features /// [feature flags]: ../index.html#crate-features
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
#[non_exhaustive] #[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 impl<S, B> Transform<S, ServiceRequest> for Compress
where where
@ -87,12 +166,16 @@ where
type Future = Ready<Result<Self::Transform, Self::InitError>>; type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future { fn new_transform(&self, service: S) -> Self::Future {
ok(CompressMiddleware { service }) ok(CompressMiddleware {
service,
inner: Rc::clone(&self.inner),
})
} }
} }
pub struct CompressMiddleware<S> { pub struct CompressMiddleware<S> {
service: S, service: S,
inner: Rc<Inner>,
} }
impl<S, B> Service<ServiceRequest> for CompressMiddleware<S> impl<S, B> Service<ServiceRequest> for CompressMiddleware<S>
@ -111,6 +194,7 @@ where
fn call(&self, req: ServiceRequest) -> Self::Future { fn call(&self, req: ServiceRequest) -> Self::Future {
// negotiate content-encoding // negotiate content-encoding
let accept_encoding = req.get_header::<AcceptEncoding>(); let accept_encoding = req.get_header::<AcceptEncoding>();
let inner = self.inner.clone();
let accept_encoding = match accept_encoding { let accept_encoding = match accept_encoding {
// missing header; fallback to identity // missing header; fallback to identity
@ -118,6 +202,7 @@ where
return Either::left(CompressResponse { return Either::left(CompressResponse {
encoding: Encoding::identity(), encoding: Encoding::identity(),
fut: self.service.call(req), fut: self.service.call(req),
inner,
_phantom: PhantomData, _phantom: PhantomData,
}) })
} }
@ -145,6 +230,7 @@ where
Some(encoding) => Either::left(CompressResponse { Some(encoding) => Either::left(CompressResponse {
fut: self.service.call(req), fut: self.service.call(req),
encoding, encoding,
inner,
_phantom: PhantomData, _phantom: PhantomData,
}), }),
} }
@ -159,6 +245,7 @@ pin_project! {
#[pin] #[pin]
fut: S::Future, fut: S::Future,
encoding: Encoding, encoding: Encoding,
inner: Rc<Inner>,
_phantom: PhantomData<B>, _phantom: PhantomData<B>,
} }
} }
@ -181,9 +268,10 @@ where
unimplemented!("encoding {} should not be here", enc); unimplemented!("encoding {} should not be here", enc);
} }
}; };
let level = this.inner.level(&enc);
Poll::Ready(Ok(resp.map_body(move |head, body| { 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("x-test")));
assert!(vary_headers.contains(&HeaderValue::from_static("accept-encoding"))); 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());
}
} }