mirror of
https://github.com/fafhrd91/actix-web
synced 2024-12-02 19:32:24 +01:00
add setup to commpress middleware
This commit is contained in:
parent
6627109984
commit
21219e0843
@ -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 {
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user