Skip to content

Commit

Permalink
Clean up 'compression' module and documentation.
Browse files Browse the repository at this point in the history
  • Loading branch information
jebrosen committed Apr 27, 2019
1 parent 6a55aa7 commit 0a3960b
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 198 deletions.
2 changes: 1 addition & 1 deletion contrib/lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ r2d2-memcache = { version = "0.3", optional = true }
time = { version = "0.1.40", optional = true }

# Compression dependencies
brotli = { version = "2.5", optional = true }
brotli = { version = "3.3", optional = true }
flate2 = { version = "1.0", optional = true }

[target.'cfg(debug_assertions)'.dependencies]
Expand Down
123 changes: 39 additions & 84 deletions contrib/lib/src/compression/fairing.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
//! Automatic response compression.
//!
//! See the [`Compression`](compression::fairing::Compression) type for further
//! details.
use rocket::config::{ConfigError, Value};
use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::MediaType;
use rocket::Rocket;
use rocket::{Request, Response};

crate use super::CompressionUtils;

crate struct Context {
crate exclusions: Vec<MediaType>,
struct Context {
exclusions: Vec<MediaType>,
}

impl Context {
crate fn new() -> Context {
impl Default for Context {
fn default() -> Context {
Context {
exclusions: vec![
MediaType::parse_flexible("application/gzip").unwrap(),
Expand All @@ -28,56 +21,30 @@ impl Context {
],
}
}
crate fn with_exclusions(excls: Vec<MediaType>) -> Context {
Context { exclusions: excls }
}
}

/// The Compression type implements brotli and gzip compression for responses in
/// accordance with the Accept-Encoding header. If accepted, brotli compression
/// is preferred over gzip.
///
/// In the brotli compression mode (using the
/// [rust-brotli](https://github.com/dropbox/rust-brotli) crate), quality is set
/// to 2 in order to achieve fast compression with a compression ratio similar
/// to gzip. When appropriate, brotli's text and font compression modes are
/// used.
///
/// In the gzip compression mode (using the
/// [flate2](https://github.com/alexcrichton/flate2-rs) crate), quality is set
/// to the default (9) in order to have good compression ratio.
/// Compresses all responses with Brotli or Gzip compression.
///
/// This fairing does not compress responses that already have a
/// `Content-Encoding` header.
/// Compression is done in the same manner as the [`Compress`](super::Compress)
/// responder.
///
/// This fairing ignores the responses with a `Content-Type` matching any of
/// the following default types:
/// By default, the fairing does not compress responses with a `Content-Type`
/// matching any of the following:
///
/// - application/gzip
/// - application/brotli
/// - image/*
/// - video/*
/// - application/wasm
/// - application/octet-stream
/// - `application/gzip`
/// - `application/zip`
/// - `image/*`
/// - `video/*`
/// - `application/wasm`
/// - `application/octet-stream`
///
/// The excluded types can be changed changing the `compress.exclude` Rocket
/// configuration property.
///
/// # Usage
///
/// To use, add the `brotli_compression` feature, the `gzip_compression`
/// feature, or the `compression` feature (to enable both algorithms) to the
/// `rocket_contrib` dependencies section of your `Cargo.toml`:
///
/// ```toml,ignore
/// [dependencies.rocket_contrib]
/// version = "*"
/// default-features = false
/// features = ["compression"]
/// ```
///
/// Then, ensure that the compression [fairing](/rocket/fairing/) is attached to
/// your Rocket application:
/// Attach the compression [fairing](/rocket/fairing/) to your Rocket
/// application:
///
/// ```rust
/// extern crate rocket;
Expand Down Expand Up @@ -117,7 +84,7 @@ impl Compression {
/// }
/// ```
pub fn fairing() -> Compression {
Compression { 0: () }
Compression(())
}
}

Expand All @@ -130,47 +97,31 @@ impl Fairing for Compression {
}

fn on_attach(&self, rocket: Rocket) -> Result<Rocket, Rocket> {
let mut ctxt = Context::new();
let mut ctxt = Context::default();

match rocket.config().get_table("compress").and_then(|t| {
t.get("exclude")
.ok_or(ConfigError::Missing(String::from("exclude")))
t.get("exclude").ok_or_else(|| ConfigError::Missing(String::from("exclude")))
}) {
Ok(excls) => match excls.as_array() {
Some(excls) => {
let mut error = false;
let mut exclusions_vec = Vec::with_capacity(excls.len());
for e in excls {
match e {
Value::String(s) => match MediaType::parse_flexible(s) {
Some(media_type) => exclusions_vec.push(media_type),
None => {
error = true;
warn_!(
"Exclusions must be valid content types, using default compression exclusions '{:?}'",
ctxt.exclusions
);
break;
}
},
_ => {
error = true;
warn_!(
"Exclusions must be strings, using default compression exclusions '{:?}'",
ctxt.exclusions
);
break;
ctxt.exclusions = excls.iter().flat_map(|ex| {
if let Value::String(s) = ex {
let mt = MediaType::parse_flexible(s);
if mt.is_none() {
warn_!("Ignoring invalid media type '{:?}'", s);
}
mt
} else {
warn_!("Ignoring non-string media type '{:?}'", ex);
None
}
}
if !error {
ctxt = Context::with_exclusions(exclusions_vec);
}
}).collect();
}
None => {
warn_!(
"Exclusions must be an array of strings, using default compression exclusions '{:?}'",
ctxt.exclusions
);
"Exclusions is not an array; using default compression exclusions '{:?}'",
ctxt.exclusions
);
}
},
Err(ConfigError::Missing(_)) => { /* ignore missing */ }
Expand All @@ -187,6 +138,10 @@ impl Fairing for Compression {
}

fn on_response(&self, request: &Request, response: &mut Response) {
CompressionUtils::compress_response(request, response, true);
let context = request
.guard::<::rocket::State<Context>>()
.expect("Compression Context registered in on_attach");

super::CompressionUtils::compress_response(request, response, &context.exclusions);
}
}
99 changes: 59 additions & 40 deletions contrib/lib/src/compression/mod.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,49 @@
//! `Compression` fairing and `Compressed` responder to automatically and
//! on demand respectively compressing responses.
//! Gzip and Brotli response compression.
//!
//! See the [`Compression`](compression::Compression) and
//! [`Compress`](compression::Compress) types for further details.
//!
//! # Enabling
//!
//! This module is only available when one of the `brotli_compression`,
//! `gzip_compression`, or `compression` features is enabled. Enable
//! one of these in `Cargo.toml` as follows:
//!
//! ```toml
//! [dependencies.rocket_contrib]
//! version = "0.4.0"
//! default-features = false
//! features = ["compression"]
//! ```
#[cfg(feature="brotli_compression")] extern crate brotli;
#[cfg(feature="gzip_compression")] extern crate flate2;

mod fairing;
mod responder;

pub use self::fairing::Compression;
pub use self::responder::Compressed;
pub use self::responder::Compress;

use std::io::Read;

crate use self::fairing::Context;
use rocket::http::MediaType;
use rocket::http::hyper::header::{ContentEncoding, Encoding};
use rocket::{Request, Response};
use std::io::Read;

#[cfg(feature = "brotli_compression")]
use brotli::enc::backward_references::BrotliEncoderMode;
use self::brotli::enc::backward_references::BrotliEncoderMode;

#[cfg(feature = "gzip_compression")]
use flate2::read::GzEncoder;
use self::flate2::read::GzEncoder;

crate struct CompressionUtils;
struct CompressionUtils;

impl CompressionUtils {
fn accepts_encoding(request: &Request, encoding: &str) -> bool {
request
.headers()
.get("Accept-Encoding")
.flat_map(|accept| accept.split(","))
.flat_map(|accept| accept.split(','))
.map(|accept| accept.trim())
.any(|accept| accept == encoding)
}
Expand All @@ -44,10 +63,10 @@ impl CompressionUtils {

fn skip_encoding(
content_type: &Option<rocket::http::ContentType>,
context: &rocket::State<Context>,
exclusions: &[MediaType],
) -> bool {
match content_type {
Some(content_type) => context.exclusions.iter().any(|exc_media_type| {
Some(content_type) => exclusions.iter().any(|exc_media_type| {
if exc_media_type.sub() == "*" {
*exc_media_type.top() == *content_type.top()
} else {
Expand All @@ -58,53 +77,53 @@ impl CompressionUtils {
}
}

fn compress_response(request: &Request, response: &mut Response, respect_excludes: bool) {
fn compress_response(request: &Request, response: &mut Response, exclusions: &[MediaType]) {
if CompressionUtils::already_encoded(response) {
return;
}

let content_type = response.content_type();

if respect_excludes {
let context = request
.guard::<::rocket::State<Context>>()
.expect("Compression Context registered in on_attach");

if CompressionUtils::skip_encoding(&content_type, &context) {
return;
}
if CompressionUtils::skip_encoding(&content_type, exclusions) {
return;
}

// Compression is done when the request accepts brotli or gzip encoding
// and the corresponding feature is enabled
if cfg!(feature = "brotli_compression") && CompressionUtils::accepts_encoding(request, "br")
{
if let Some(plain) = response.take_body() {
let content_type_top = content_type.as_ref().map(|ct| ct.top());
let mut params = brotli::enc::BrotliEncoderInitParams();
params.quality = 2;
if content_type_top == Some("text".into()) {
params.mode = BrotliEncoderMode::BROTLI_MODE_TEXT;
} else if content_type_top == Some("font".into()) {
params.mode = BrotliEncoderMode::BROTLI_MODE_FONT;
#[cfg(feature = "brotli_compression")]
{
if let Some(plain) = response.take_body() {
let content_type_top = content_type.as_ref().map(|ct| ct.top());
let mut params = brotli::enc::BrotliEncoderInitParams();
params.quality = 2;
if content_type_top == Some("text".into()) {
params.mode = BrotliEncoderMode::BROTLI_MODE_TEXT;
} else if content_type_top == Some("font".into()) {
params.mode = BrotliEncoderMode::BROTLI_MODE_FONT;
}

let compressor =
brotli::CompressorReader::with_params(plain.into_inner(), 4096, &params);

CompressionUtils::set_body_and_encoding(
response,
compressor,
Encoding::EncodingExt("br".into()),
);
}

let compressor =
brotli::CompressorReader::with_params(plain.into_inner(), 4096, &params);

CompressionUtils::set_body_and_encoding(
response,
compressor,
Encoding::EncodingExt("br".into()),
);
}
} else if cfg!(feature = "gzip_compression")
&& CompressionUtils::accepts_encoding(request, "gzip")
{
if let Some(plain) = response.take_body() {
let compressor = GzEncoder::new(plain.into_inner(), flate2::Compression::default());
#[cfg(feature = "gzip_compression")]
{
if let Some(plain) = response.take_body() {
let compressor = GzEncoder::new(plain.into_inner(), flate2::Compression::default());

CompressionUtils::set_body_and_encoding(response, compressor, Encoding::Gzip);
CompressionUtils::set_body_and_encoding(response, compressor, Encoding::Gzip);
}
}
}
}
Expand Down
Loading

0 comments on commit 0a3960b

Please sign in to comment.