use tide::http::cache::{CacheControl, CacheDirective};
use tide::http::conditional::Vary;
use tide::http::content::{AcceptEncoding, ContentEncoding, Encoding};
use tide::http::{headers, Body, Method};
use tide::{Middleware, Next, Request, Response};
#[cfg(any(feature = "brotli", feature = "deflate", feature = "gzip"))]
use async_compression::Level;
#[cfg(any(feature = "brotli", feature = "deflate", feature = "gzip"))]
use futures_lite::io::BufReader;
#[cfg(feature = "brotli")]
use async_compression::futures::bufread::BrotliEncoder;
#[cfg(feature = "deflate")]
use async_compression::futures::bufread::DeflateEncoder;
#[cfg(feature = "gzip")]
use async_compression::futures::bufread::GzipEncoder;
#[cfg(feature = "regex-check")]
use http_types::content::ContentType;
#[cfg(feature = "regex-check")]
use regex::{Regex, RegexBuilder};
const THRESHOLD: usize = 1024;
#[cfg(feature = "regex-check")]
const CONTENT_TYPE_CHECK_PATTERN: &str = r"^text/|\+(?:json|text|xml)$";
#[cfg(feature = "regex-check")]
const EXTRACT_TYPE_PATTERN: &str = r"^\s*([^;\s]*)(?:;|\s|$)";
#[derive(Clone, Debug)]
pub struct CompressMiddleware {
threshold: usize,
#[cfg(feature = "regex-check")]
content_type_check: Option<Regex>,
#[cfg(feature = "regex-check")]
extract_type_regex: Regex,
#[cfg(feature = "brotli")]
brotli_quality: Level,
#[cfg(any(feature = "gzip", feature = "deflate"))]
deflate_quality: Level,
}
impl Default for CompressMiddleware {
fn default() -> Self {
CompressMiddlewareBuilder::default().into()
}
}
impl CompressMiddleware {
pub fn new() -> Self {
Self::default()
}
pub fn builder() -> CompressMiddlewareBuilder {
CompressMiddlewareBuilder::new()
}
pub fn set_threshold(&mut self, threshold: usize) {
self.threshold = threshold
}
pub fn threshold(&self) -> usize {
self.threshold
}
#[cfg(feature = "regex-check")]
pub fn set_content_type_check(&mut self, content_type_check: Option<Regex>) {
self.content_type_check = content_type_check
}
#[cfg(feature = "regex-check")]
pub fn content_type_check(&self) -> Option<&Regex> {
self.content_type_check.as_ref()
}
}
#[tide::utils::async_trait]
impl<State: Clone + Send + Sync + 'static> Middleware<State> for CompressMiddleware {
async fn handle(&self, req: Request<State>, next: Next<'_, State>) -> tide::Result {
let is_head = req.method() == Method::Head;
let accepts = AcceptEncoding::from_headers(&req)?;
let mut res: Response = next.run(req).await;
if is_head || accepts.is_none() {
return Ok(res);
}
let mut accepts = accepts.expect("checked directly above");
if let Some(cache_control) = CacheControl::from_headers(&res)? {
if cache_control
.iter()
.any(|directive| directive == &CacheDirective::NoTransform)
{
return Ok(res);
}
}
let mut vary = Vary::new();
vary.push(headers::ACCEPT_ENCODING)?;
vary.apply(&mut res);
if let Some(previous_encoding) = ContentEncoding::from_headers(&res)? {
if previous_encoding != Encoding::Identity {
return Ok(res);
}
}
if let Some(body_len) = res.len() {
if body_len < self.threshold {
return Ok(res);
}
}
#[cfg(feature = "regex-check")]
if let Some(ref content_type_check) = self.content_type_check {
if let Some(content_type) = ContentType::from_headers(&res)? {
if let Some(extension_match) = self
.extract_type_regex
.captures(content_type.value().as_str())
.and_then(|captures| captures.get(1))
{
#[cfg(feature = "db-check")]
if !crate::codegen_database::MIME_DB.contains(extension_match.as_str())
&& !content_type_check.is_match(extension_match.as_str())
{
return Ok(res);
}
#[cfg(not(feature = "db-check"))]
if !content_type_check.is_match(extension_match.as_str()) {
return Ok(res);
}
}
}
}
let encoding = accepts.negotiate(&[
#[cfg(feature = "brotli")]
Encoding::Brotli,
#[cfg(feature = "gzip")]
Encoding::Gzip,
#[cfg(feature = "deflate")]
Encoding::Deflate,
Encoding::Identity, ])?;
if encoding == Encoding::Identity {
res.remove_header(headers::CONTENT_ENCODING);
return Ok(res);
}
let body = res.take_body();
res.set_body(get_encoder(
body,
&encoding,
#[cfg(feature = "brotli")]
self.brotli_quality,
#[cfg(any(feature = "gzip", feature = "deflate"))]
self.deflate_quality,
));
encoding.apply(&mut res);
res.remove_header(headers::CONTENT_LENGTH);
Ok(res)
}
}
#[cfg_attr(
not(any(feature = "brotli", feature = "deflate", feature = "gzip")),
allow(unused_variables)
)]
fn get_encoder(
body: Body,
encoding: &ContentEncoding,
#[cfg(feature = "brotli")] brotli_quality: Level,
#[cfg(any(feature = "gzip", feature = "deflate"))] deflate_quality: Level,
) -> Body {
#[cfg(feature = "brotli")]
{
if *encoding == Encoding::Brotli {
return Body::from_reader(
BufReader::new(BrotliEncoder::with_quality(body, brotli_quality)),
None,
);
}
}
#[cfg(feature = "gzip")]
{
if *encoding == Encoding::Gzip {
return Body::from_reader(
BufReader::new(GzipEncoder::with_quality(body, deflate_quality)),
None,
);
}
}
#[cfg(feature = "deflate")]
{
if *encoding == Encoding::Deflate {
return Body::from_reader(
BufReader::new(DeflateEncoder::with_quality(body, deflate_quality)),
None,
);
}
}
body
}
#[derive(Clone, Debug)]
pub struct CompressMiddlewareBuilder {
pub threshold: usize,
#[cfg(feature = "regex-check")]
pub content_type_check: Option<Regex>,
#[cfg(feature = "brotli")]
pub brotli_quality: Level,
#[cfg(any(feature = "gzip", feature = "deflate"))]
pub deflate_quality: Level,
}
impl Default for CompressMiddlewareBuilder {
fn default() -> Self {
Self {
threshold: THRESHOLD,
#[cfg(feature = "regex-check")]
content_type_check: Some(
RegexBuilder::new(CONTENT_TYPE_CHECK_PATTERN)
.case_insensitive(true)
.build()
.expect("Constant regular expression defined in Tide-Compress's source code"),
),
#[cfg(feature = "brotli")]
brotli_quality: Level::Fastest,
#[cfg(any(feature = "gzip", feature = "deflate"))]
deflate_quality: Level::Default,
}
}
}
impl CompressMiddlewareBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn threshold(mut self, threshold: usize) -> Self {
self.threshold = threshold;
self
}
#[cfg(feature = "regex-check")]
pub fn content_type_check(mut self, content_type_check: Option<Regex>) -> Self {
self.content_type_check = content_type_check;
self
}
#[cfg(feature = "brotli")]
pub fn brotli_quality(mut self, quality: Level) -> Self {
self.brotli_quality = quality;
self
}
#[cfg(any(feature = "gzip", feature = "deflate"))]
pub fn deflate_quality(mut self, quality: Level) -> Self {
self.deflate_quality = quality;
self
}
pub fn build(self) -> CompressMiddleware {
self.into()
}
}
impl From<CompressMiddlewareBuilder> for CompressMiddleware {
fn from(builder: CompressMiddlewareBuilder) -> Self {
Self {
threshold: builder.threshold,
#[cfg(feature = "regex-check")]
content_type_check: builder.content_type_check,
#[cfg(feature = "regex-check")]
extract_type_regex: Regex::new(EXTRACT_TYPE_PATTERN)
.expect("Constant regular expression defined in Tide-Compress's source code"),
#[cfg(feature = "brotli")]
brotli_quality: builder.brotli_quality,
#[cfg(any(feature = "gzip", feature = "deflate"))]
deflate_quality: builder.deflate_quality,
}
}
}