Skip to content

Commit

Permalink
Precompressed files (#156)
Browse files Browse the repository at this point in the history
* Split out content encoding enum

* Support precompressed files

fixup! Support precompressed files

* Add some more tests

* Support precompressed directory files

* Fix pr comments and improve docs

* Fix cargo hack issue

* Update changelog

Co-authored-by: David Pedersen <david.pdrsn@gmail.com>
  • Loading branch information
Nehliin and davidpdrsn committed Nov 15, 2021
1 parent c3a1fcb commit 754aafe
Show file tree
Hide file tree
Showing 17 changed files with 652 additions and 156 deletions.
Binary file added test-files/only_gzipped.txt.gz
Binary file not shown.
1 change: 1 addition & 0 deletions test-files/precompressed.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"This is a test file!"
2 changes: 2 additions & 0 deletions test-files/precompressed.txt.br
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
�"This is a test file!"

Binary file added test-files/precompressed.txt.gz
Binary file not shown.
Binary file added test-files/precompressed.txt.zz
Binary file not shown.
2 changes: 2 additions & 0 deletions tower-http/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

# Unreleased

- `ServeDir` and `ServeFile`: Ability to serve precompressed files ([#156])
- `Trace`: Add `DefaultMakeSpan::level` to make log level of tracing spans easily configurable ([#124])

[#124]: https://github.com/tower-rs/tower-http/pull/124
[#156]: https://github.com/tower-rs/tower-http/pull/156

# 0.1.2 (November 13, 2021)

Expand Down
1 change: 1 addition & 0 deletions tower-http/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ percent-encoding = { version = "2.1.0", optional = true }
[dev-dependencies]
bytes = "1"
flate2 = "1.0"
brotli = "3"
futures = "0.3"
hyper = { version = "0.14", features = ["full"] }
once_cell = "1"
Expand Down
3 changes: 2 additions & 1 deletion tower-http/src/compression/future.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#![allow(unused_imports)]

use super::{body::BodyInner, CompressionBody, Encoding};
use super::{body::BodyInner, CompressionBody};
use crate::compression_utils::{supports_transparent_compression, WrapBody};
use crate::content_encoding::Encoding;
use futures_util::ready;
use http::{header, HeaderMap, HeaderValue, Response};
use http_body::Body;
Expand Down
97 changes: 0 additions & 97 deletions tower-http/src/compression/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,6 @@
//! # }
//! ```

use crate::compression_utils::AcceptEncoding;
use http::{header, HeaderMap, HeaderValue};

mod body;
mod future;
mod layer;
Expand All @@ -75,100 +72,6 @@ pub use self::{
body::CompressionBody, future::ResponseFuture, layer::CompressionLayer, service::Compression,
};

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) enum Encoding {
#[cfg(feature = "compression-gzip")]
Gzip,
#[cfg(feature = "compression-deflate")]
Deflate,
#[cfg(feature = "compression-br")]
Brotli,
Identity,
}

impl Encoding {
fn to_str(self) -> &'static str {
match self {
#[cfg(feature = "compression-gzip")]
Encoding::Gzip => "gzip",
#[cfg(feature = "compression-deflate")]
Encoding::Deflate => "deflate",
#[cfg(feature = "compression-br")]
Encoding::Brotli => "br",
Encoding::Identity => "identity",
}
}

fn into_header_value(self) -> HeaderValue {
HeaderValue::from_static(self.to_str())
}

#[allow(unused_variables)]
fn parse(s: &str, accept: AcceptEncoding) -> Option<Encoding> {
match s {
#[cfg(feature = "compression-gzip")]
"gzip" if accept.gzip() => Some(Encoding::Gzip),
#[cfg(feature = "compression-deflate")]
"deflate" if accept.deflate() => Some(Encoding::Deflate),
#[cfg(feature = "compression-br")]
"br" if accept.br() => Some(Encoding::Brotli),
"identity" => Some(Encoding::Identity),
_ => None,
}
}

// based on https://github.com/http-rs/accept-encoding
fn from_headers(headers: &HeaderMap, accept: AcceptEncoding) -> Self {
let mut preferred_encoding = None;
let mut max_qval = 0.0;

for (encoding, qval) in encodings(headers, accept) {
if (qval - 1.0f32).abs() < 0.01 {
preferred_encoding = Some(encoding);
break;
} else if qval > max_qval {
preferred_encoding = Some(encoding);
max_qval = qval;
}
}

preferred_encoding.unwrap_or(Encoding::Identity)
}
}

// based on https://github.com/http-rs/accept-encoding
fn encodings(headers: &HeaderMap, accept: AcceptEncoding) -> Vec<(Encoding, f32)> {
headers
.get_all(header::ACCEPT_ENCODING)
.iter()
.filter_map(|hval| hval.to_str().ok())
.flat_map(|s| s.split(',').map(str::trim))
.filter_map(|v| {
let mut v = v.splitn(2, ";q=");

let encoding = match Encoding::parse(v.next().unwrap(), accept) {
Some(encoding) => encoding,
None => return None, // ignore unknown encodings
};

let qval = if let Some(qval) = v.next() {
let qval = match qval.parse::<f32>() {
Ok(f) => f,
Err(_) => return None,
};
if qval > 1.0 {
return None; // q-values over 1 are unacceptable
}
qval
} else {
1.0f32
};

Some((encoding, qval))
})
.collect::<Vec<(Encoding, f32)>>()
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
4 changes: 2 additions & 2 deletions tower-http/src/compression/service.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::{CompressionBody, CompressionLayer, Encoding, ResponseFuture};
use crate::compression_utils::AcceptEncoding;
use super::{CompressionBody, CompressionLayer, ResponseFuture};
use crate::{compression_utils::AcceptEncoding, content_encoding::Encoding};
use http::{Request, Response};
use http_body::Body;
use std::task::{Context, Poll};
Expand Down
42 changes: 22 additions & 20 deletions tower-http/src/compression_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use std::{
use tokio::io::AsyncRead;
use tokio_util::io::{poll_read_buf, StreamReader};

use crate::BodyOrIoError;
use crate::{content_encoding::SupportedEncodings, BodyOrIoError};

#[derive(Debug, Clone, Copy)]
pub(crate) struct AcceptEncoding {
Expand All @@ -25,7 +25,7 @@ pub(crate) struct AcceptEncoding {

impl AcceptEncoding {
#[allow(dead_code)]
pub(crate) fn to_header_value(&self) -> Option<HeaderValue> {
pub(crate) fn to_header_value(self) -> Option<HeaderValue> {
let accept = match (self.gzip(), self.deflate(), self.br()) {
(true, true, true) => "gzip,deflate,br",
(true, true, false) => "gzip,deflate",
Expand All @@ -40,7 +40,24 @@ impl AcceptEncoding {
}

#[allow(dead_code)]
pub(crate) fn gzip(&self) -> bool {
pub(crate) fn set_gzip(&mut self, enable: bool) {
self.gzip = enable;
}

#[allow(dead_code)]
pub(crate) fn set_deflate(&mut self, enable: bool) {
self.deflate = enable;
}

#[allow(dead_code)]
pub(crate) fn set_br(&mut self, enable: bool) {
self.br = enable;
}
}

impl SupportedEncodings for AcceptEncoding {
#[allow(dead_code)]
fn gzip(&self) -> bool {
#[cfg(any(feature = "decompression-gzip", feature = "compression-gzip"))]
{
self.gzip
Expand All @@ -52,7 +69,7 @@ impl AcceptEncoding {
}

#[allow(dead_code)]
pub(crate) fn deflate(&self) -> bool {
fn deflate(&self) -> bool {
#[cfg(any(feature = "decompression-deflate", feature = "compression-deflate"))]
{
self.deflate
Expand All @@ -64,7 +81,7 @@ impl AcceptEncoding {
}

#[allow(dead_code)]
pub(crate) fn br(&self) -> bool {
fn br(&self) -> bool {
#[cfg(any(feature = "decompression-br", feature = "compression-br"))]
{
self.br
Expand All @@ -74,21 +91,6 @@ impl AcceptEncoding {
false
}
}

#[allow(dead_code)]
pub(crate) fn set_gzip(&mut self, enable: bool) {
self.gzip = enable;
}

#[allow(dead_code)]
pub(crate) fn set_deflate(&mut self, enable: bool) {
self.deflate = enable;
}

#[allow(dead_code)]
pub(crate) fn set_br(&mut self, enable: bool) {
self.br = enable;
}
}

impl Default for AcceptEncoding {
Expand Down
119 changes: 119 additions & 0 deletions tower-http/src/content_encoding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use std::ffi::OsStr;

use http::{header, HeaderMap, HeaderValue};

pub(crate) trait SupportedEncodings: Copy {
fn gzip(&self) -> bool;
fn deflate(&self) -> bool;
fn br(&self) -> bool;
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) enum Encoding {
#[cfg(any(feature = "fs", feature = "compression-gzip"))]
Gzip,
#[cfg(any(feature = "fs", feature = "compression-deflate"))]
Deflate,
#[cfg(any(feature = "fs", feature = "compression-br"))]
Brotli,
Identity,
}

impl Encoding {
fn to_str(self) -> &'static str {
match self {
#[cfg(any(feature = "fs", feature = "compression-gzip"))]
Encoding::Gzip => "gzip",
#[cfg(any(feature = "fs", feature = "compression-deflate"))]
Encoding::Deflate => "deflate",
#[cfg(any(feature = "fs", feature = "compression-br"))]
Encoding::Brotli => "br",
Encoding::Identity => "identity",
}
}

#[cfg(feature = "fs")]
pub(crate) fn to_file_extension(self) -> Option<&'static OsStr> {
match self {
Encoding::Gzip => Some(OsStr::new(".gz")),
Encoding::Deflate => Some(OsStr::new(".zz")),
Encoding::Brotli => Some(OsStr::new(".br")),
Encoding::Identity => None,
}
}

pub(crate) fn into_header_value(self) -> HeaderValue {
HeaderValue::from_static(self.to_str())
}

#[allow(unused_variables)]
fn parse(s: &str, supported_encoding: impl SupportedEncodings) -> Option<Encoding> {
match s {
#[cfg(any(feature = "fs", feature = "compression-gzip"))]
"gzip" if supported_encoding.gzip() => Some(Encoding::Gzip),
#[cfg(any(feature = "fs", feature = "compression-deflate"))]
"deflate" if supported_encoding.deflate() => Some(Encoding::Deflate),
#[cfg(any(feature = "fs", feature = "compression-br"))]
"br" if supported_encoding.br() => Some(Encoding::Brotli),
"identity" => Some(Encoding::Identity),
_ => None,
}
}

// based on https://github.com/http-rs/accept-encoding
pub(crate) fn from_headers(
headers: &HeaderMap,
supported_encoding: impl SupportedEncodings,
) -> Self {
let mut preferred_encoding = None;
let mut max_qval = 0.0;

for (encoding, qval) in encodings(headers, supported_encoding) {
if (qval - 1.0f32).abs() < 0.01 {
preferred_encoding = Some(encoding);
break;
} else if qval > max_qval {
preferred_encoding = Some(encoding);
max_qval = qval;
}
}

preferred_encoding.unwrap_or(Encoding::Identity)
}
}

// based on https://github.com/http-rs/accept-encoding
fn encodings(
headers: &HeaderMap,
supported_encoding: impl SupportedEncodings,
) -> Vec<(Encoding, f32)> {
headers
.get_all(header::ACCEPT_ENCODING)
.iter()
.filter_map(|hval| hval.to_str().ok())
.flat_map(|s| s.split(',').map(str::trim))
.filter_map(|v| {
let mut v = v.splitn(2, ";q=");

let encoding = match Encoding::parse(v.next().unwrap(), supported_encoding) {
Some(encoding) => encoding,
None => return None, // ignore unknown encodings
};

let qval = if let Some(qval) = v.next() {
let qval = match qval.parse::<f32>() {
Ok(f) => f,
Err(_) => return None,
};
if qval > 1.0 {
return None; // q-values over 1 are unacceptable
}
qval
} else {
1.0f32
};

Some((encoding, qval))
})
.collect::<Vec<(Encoding, f32)>>()
}
1 change: 1 addition & 0 deletions tower-http/src/decompression/future.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use super::{body::BodyInner, DecompressionBody};
use crate::compression_utils::{AcceptEncoding, WrapBody};
use crate::content_encoding::SupportedEncodings;
use futures_util::ready;
use http::{header, Response};
use http_body::Body;
Expand Down
4 changes: 4 additions & 0 deletions tower-http/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,10 @@ pub mod sensitive_headers;
#[cfg_attr(docsrs, doc(cfg(feature = "decompression")))]
pub mod decompression;

// Used for serving precompressed static files as well
#[cfg(any(feature = "compression", feature = "decompression", feature = "fs"))]
mod content_encoding;

#[cfg(any(feature = "compression", feature = "decompression"))]
mod compression_utils;

Expand Down
Loading

0 comments on commit 754aafe

Please sign in to comment.