Skip to content

Commit

Permalink
feat: add support for brotli compression (#300)
Browse files Browse the repository at this point in the history
* feat(compression): Generate perseus engine brotli compressed files. Change the servers configuration to look for the .br files first and if not found defaults to the uncompressed file.
Changed the servers(actix-web, axum, rocket, warp) configuration to add a dflt-server-with-compression feature that enables native server compression.

By default, brotli compress the perseus_engine.js, perseus_engine.wasm, perseus_engine.d.ts and perseus_engine_bg.wasm.d.ts.
This behaviour can be deactivated with the disable_engine_compression flag on the perseus CLI.

* Update packages/perseus-actix-web/src/lib.rs

Co-authored-by: Sam Brew <[email protected]>

* Update packages/perseus-actix-web/src/lib.rs

Co-authored-by: Sam Brew <[email protected]>

* Update packages/perseus-actix-web/src/lib.rs

Co-authored-by: Sam Brew <[email protected]>

* Update packages/perseus-axum/Cargo.toml

Co-authored-by: Sam Brew <[email protected]>

* Update packages/perseus-axum/src/lib.rs

Co-authored-by: Sam Brew <[email protected]>

* Update packages/perseus-cli/src/build.rs

Co-authored-by: Sam Brew <[email protected]>

* Update packages/perseus-cli/src/build.rs

Co-authored-by: Sam Brew <[email protected]>

* Update packages/perseus-cli/src/parse.rs

Co-authored-by: Sam Brew <[email protected]>

* Update packages/perseus-rocket/src/lib.rs

Co-authored-by: Sam Brew <[email protected]>

* Update packages/perseus-rocket/Cargo.toml

Co-authored-by: Sam Brew <[email protected]>

* Update packages/perseus-warp/Cargo.toml

Co-authored-by: Sam Brew <[email protected]>

* Update packages/perseus-warp/src/lib.rs

Co-authored-by: Sam Brew <[email protected]>

* feat(compression): Simplify rocket integration, change flag name from disable_engine_compression to disable_bundle_compression, compress only for deploy and not on debug build

* feat(chore): Resolve conflict on Cargo.toml in perseus-cli

* chore: format all with cargo fmt

---------

Co-authored-by: Julien Teruel <[email protected]>
Co-authored-by: Sam Brew <[email protected]>
  • Loading branch information
3 people authored Dec 12, 2023
1 parent 2d2aa14 commit 8016599
Show file tree
Hide file tree
Showing 12 changed files with 254 additions and 21 deletions.
1 change: 1 addition & 0 deletions packages/perseus-actix-web/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ futures = "0.3"
[features]
# Enables the default server configuration, which provides a convenience function if you're not adding any extra routes
dflt-server = []
dflt-server-with-compression = []

[package.metadata.docs.rs]
rustc-args = ["--cfg=engine"]
Expand Down
79 changes: 72 additions & 7 deletions packages/perseus-actix-web/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
This is the API documentation for the `perseus-actix-web` package, which allows Perseus apps to run on Actix Web. Note that Perseus mostly uses [the book](https://framesurge.sh/perseus/en-US) for
documentation, and this should mostly be used as a secondary reference source. You can also find full usage examples [here](https://github.com/arctic-hen7/framesurge/tree/main/examples).
*/
*/

#![cfg(engine)] // This crate needs to be run with the Perseus CLI
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]

use actix_files::{Files, NamedFile};
use actix_web::CustomizeResponder;
use actix_web::{web, HttpRequest, HttpResponse, Responder};
use perseus::turbine::ApiResponse as PerseusApiResponse;
use perseus::{
Expand Down Expand Up @@ -160,15 +161,49 @@ pub async fn configurer<M: MutableStore + 'static, T: TranslationsManager + 'sta
}

// File handlers (these have to be broken out for Actix)
async fn js_bundle(opts: web::Data<ServerOptions>) -> std::io::Result<NamedFile> {
NamedFile::open(&opts.js_bundle)
async fn js_bundle(
opts: web::Data<ServerOptions>,
) -> std::io::Result<CustomizeResponder<NamedFile>> {
search_for_pre_compressed_version(
&opts.js_bundle,
"application/javascript; charset=utf-8".to_string(),
)
}
async fn wasm_bundle(opts: web::Data<ServerOptions>) -> std::io::Result<NamedFile> {
NamedFile::open(&opts.wasm_bundle)

async fn wasm_bundle(
opts: web::Data<ServerOptions>,
) -> std::io::Result<CustomizeResponder<NamedFile>> {
search_for_pre_compressed_version(&opts.wasm_bundle, "application/wasm".to_string())
}
async fn wasm_js_bundle(opts: web::Data<ServerOptions>) -> std::io::Result<NamedFile> {
NamedFile::open(&opts.wasm_js_bundle)

async fn wasm_js_bundle(
opts: web::Data<ServerOptions>,
) -> std::io::Result<CustomizeResponder<NamedFile>> {
search_for_pre_compressed_version(
&opts.wasm_js_bundle,
"application/javascript; charset=utf-8".to_string(),
)
}

fn search_for_pre_compressed_version(
path: &str,
application_type: String,
) -> std::io::Result<CustomizeResponder<NamedFile>> {
let pre_compressed_path = format!("{}.br", path);
match NamedFile::open(pre_compressed_path) {
Ok(file) => Ok(file
.customize()
.insert_header(("Content-Encoding".to_string(), "br".to_string()))
.insert_header(("Content-Type".to_string(), application_type))),
Err(_) => match NamedFile::open(path) {
Ok(file) => Ok(file
.customize()
.insert_header(("Content-Type".to_string(), application_type))),
Err(e) => Err(e),
},
}
}

async fn static_alias<M: MutableStore, T: TranslationsManager>(
turbine: &'static Turbine<M, T>,
req: HttpRequest,
Expand Down Expand Up @@ -214,3 +249,33 @@ pub async fn dflt_server<M: MutableStore + 'static, T: TranslationsManager + 'st
.await
.expect("Server failed.") // TODO Improve error message here
}

/// Creates and starts the default Perseus server with GZIP compression enabled using Actix Web. This should
/// be run in a `main()` function annotated with `#[tokio::main]` (which
/// requires the `macros` and `rt-multi-thread` features on the `tokio`
/// dependency).
#[cfg(feature = "dflt-server-with-compression")]
pub async fn dflt_server_with_compression<
M: MutableStore + 'static,
T: TranslationsManager + 'static,
>(
turbine: &'static Turbine<M, T>,
opts: ServerOptions,
(host, port): (String, u16),
) {
use actix_web::{App, HttpServer};
use futures::executor::block_on;
// TODO Fix issues here
HttpServer::new(move || {
App::new()
.wrap(actix_web::middleware::Compress::default())
.configure(block_on(configurer(turbine, opts.clone())))
})
.bind((host, port))
.expect(
"Couldn't bind to given address. Maybe something is already running on the selected port?",
)
.run()
.await
.expect("Server failed.") // TODO Improve error message here
}
1 change: 1 addition & 0 deletions packages/perseus-axum/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ tower-http = { version = "0.3", features = [ "fs" ] }
[features]
# Enables the default server configuration, which provides a convenience function if you're not adding any extra routes
dflt-server = []
dflt-server-with-compression = [ "tower-http/compression-gzip" ]

[package.metadata.docs.rs]
rustc-args = ["--cfg=engine"]
Expand Down
49 changes: 44 additions & 5 deletions packages/perseus-axum/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
This is the API documentation for the `perseus-axum` package, which allows Perseus apps to run on Axum. Note that Perseus mostly uses [the book](https://framesurge.sh/perseus/en-US) for
documentation, and this should mostly be used as a secondary reference source. You can also find full usage examples [here](https://github.com/framesurge/perseus/tree/main/examples).
*/
*/

#![cfg(engine)] // This crate needs to be run with the Perseus CLI
#![deny(missing_docs)]
Expand Down Expand Up @@ -37,11 +37,13 @@ use tower_http::services::{ServeDir, ServeFile};

#[derive(Debug)]
struct ApiResponse(PerseusApiResponse);

impl From<PerseusApiResponse> for ApiResponse {
fn from(val: PerseusApiResponse) -> Self {
Self(val)
}
}

impl IntoResponse for ApiResponse {
fn into_response(self) -> Response {
// Very convenient!
Expand All @@ -61,20 +63,25 @@ pub async fn get_router<M: MutableStore + 'static, T: TranslationsManager + 'sta
// --- File handlers ---
.route(
"/.perseus/bundle.js",
get_service(ServeFile::new(opts.js_bundle.clone())).handle_error(handle_fs_error),
get_service(ServeFile::new(opts.js_bundle.clone()).precompressed_br())
.handle_error(handle_fs_error),
)
.route(
"/.perseus/bundle.wasm",
get_service(ServeFile::new(opts.wasm_bundle.clone())).handle_error(handle_fs_error),
get_service(ServeFile::new(opts.wasm_bundle.clone()).precompressed_br())
.handle_error(handle_fs_error),
)
.route(
"/.perseus/bundle.wasm.js",
get_service(ServeFile::new(opts.wasm_js_bundle.clone())).handle_error(handle_fs_error),
get_service(ServeFile::new(opts.wasm_js_bundle.clone()).precompressed_br())
.handle_error(handle_fs_error),
)
.nest_service(
"/.perseus/snippets",
get_service(ServeDir::new(opts.snippets)).handle_error(handle_fs_error),
get_service(ServeDir::new(opts.snippets).precompressed_br())
.handle_error(handle_fs_error),
);

// --- Translation and subsequent load handlers ---
let mut router = router
.route(
Expand Down Expand Up @@ -135,6 +142,7 @@ pub async fn get_router<M: MutableStore + 'static, T: TranslationsManager + 'sta
},
),
);

// --- Static directory and alias handlers ---
if turbine.static_dir.exists() {
router = router.nest_service(
Expand All @@ -148,6 +156,7 @@ pub async fn get_router<M: MutableStore + 'static, T: TranslationsManager + 'sta
get_service(ServeFile::new(static_path)).handle_error(handle_fs_error),
);
}

// --- Initial load handler ---
router.fallback_service(get(move |http_req: Request<Body>| async move {
// Since this is a fallback handler, we have to do everything from the request
Expand Down Expand Up @@ -185,7 +194,37 @@ pub async fn dflt_server<M: MutableStore + 'static, T: TranslationsManager + 'st
let addr: SocketAddr = format!("{}:{}", host, port)
.parse()
.expect("Invalid address provided to bind to.");

let app = get_router(turbine, opts).await;

axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}

/// Creates and starts the default Perseus server with compression using Axum. This should be run
/// in a `main` function annotated with `#[tokio::main]` (which requires the
/// `macros` and `rt-multi-thread` features on the `tokio` dependency).
#[cfg(feature = "dflt-server-with-compression")]
pub async fn dflt_server_with_compression<
M: MutableStore + 'static,
T: TranslationsManager + 'static,
>(
turbine: &'static Turbine<M, T>,
opts: ServerOptions,
(host, port): (String, u16),
) {
use std::net::SocketAddr;

let addr: SocketAddr = format!("{}:{}", host, port)
.parse()
.expect("Invalid address provided to bind to.");

let app = get_router(turbine, opts)
.await
.layer(tower_http::compression::CompressionLayer::new());

axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
Expand Down
1 change: 1 addition & 0 deletions packages/perseus-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ cargo-lock = "8"
minify-js = "=0.4.3" # Be careful changing this, and test extensively!
walkdir = "2"
openssl = { version = "0.10.52", optional = true}
brotlic = "0.8"

[dev-dependencies]
assert_cmd = "2"
Expand Down
24 changes: 24 additions & 0 deletions packages/perseus-cli/src/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ use crate::install::Tools;
use crate::parse::Opts;
use crate::parse::{DeployOpts, ExportOpts, ServeOpts};
use crate::serve;
use brotlic::CompressorWriter;
use fs_extra::copy_items;
use fs_extra::dir::{copy as copy_dir, CopyOptions};
use indicatif::MultiProgress;
use minify_js::{minify, TopLevelMode};
use std::fs;
use std::fs::File;
use std::io::{BufReader, Read, Write};
use std::path::Path;
use std::path::PathBuf;

Expand Down Expand Up @@ -142,6 +145,27 @@ fn deploy_full(
}
.into());
}

// If compression is enabled (which it is by default), we compress each file with Brotli in `dist/pkg`
if !global_opts.disable_bundle_compression {
let target_path = "dist/pkg";
let files_name = ["perseus_engine.js", "perseus_engine_bg.wasm"];

for file_name in files_name {
let file_path = format!("{}/{}", target_path, file_name);
let output_path = format!("{}.br", &file_path);

let input_file = File::open(&file_path).unwrap();
let output_file = File::create(&output_path).unwrap();

let mut output_compressed = CompressorWriter::new(output_file);
let mut buffer = Vec::new();
let mut reader = BufReader::new(input_file);
reader.read_to_end(&mut buffer).unwrap();
output_compressed.write_all(&buffer).unwrap();
}
}

let from = dir.join("dist/pkg"); // Note: this handles snippets and the like
if let Err(err) = copy_dir(&from, output_path.join("dist"), &CopyOptions::new()) {
return Err(DeployError::MoveDirFailed {
Expand Down
4 changes: 4 additions & 0 deletions packages/perseus-cli/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ pub struct Opts {
/// commands are more useful for targeted debugging
#[clap(long, global = true)]
pub verbose: bool,

/// Disable Brotli compression of JS and Wasm bundles (may degrade performance)
#[clap(long, default_value = "false", global = true)]
pub disable_bundle_compression: bool,
}

#[derive(Parser, Clone)]
Expand Down
2 changes: 2 additions & 0 deletions packages/perseus-rocket/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ categories = ["wasm", "web-programming::http-server", "development-tools", "asyn
[dependencies]
perseus = { path = "../perseus", version = "0.4.2"}
rocket = "0.5.0-rc.2"
rocket_async_compression = { version = "0.5.0", optional = true}

[features]
# Enables the default server configuration, which provides a convenience function if you're not adding any extra routes
dflt-server = []
dflt-server-with-compression = [ "rocket_async_compression" ]

[package.metadata.docs.rs]
rustc-args = ["--cfg=engine"]
Expand Down
48 changes: 43 additions & 5 deletions packages/perseus-rocket/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ documentation, and this should mostly be used as a secondary reference source. Y
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]

use std::{io::Cursor, path::Path};

use perseus::{
i18n::TranslationsManager,
path::PathMaybeWithLocale,
Expand All @@ -30,6 +28,7 @@ use rocket::{
tokio::fs::File,
Build, Data, Request, Response, Rocket, Route, State,
};
use std::{io::Cursor, path::Path};

// ----- Newtype wrapper for response implementation -----

Expand Down Expand Up @@ -65,17 +64,24 @@ impl<'r> Responder<'r, 'static> for ApiResponse {

#[get("/bundle.js")]
async fn get_js_bundle(opts: &State<ServerOptions>) -> std::io::Result<NamedFile> {
NamedFile::open(&opts.js_bundle).await
get_pre_compressed(&opts.js_bundle).await
}

#[get("/bundle.wasm")]
async fn get_wasm_bundle(opts: &State<ServerOptions>) -> std::io::Result<NamedFile> {
NamedFile::open(&opts.wasm_bundle).await
get_pre_compressed(&opts.wasm_bundle).await
}

#[get("/bundle.wasm.js")]
async fn get_wasm_js_bundle(opts: &State<ServerOptions>) -> std::io::Result<NamedFile> {
NamedFile::open(&opts.wasm_js_bundle).await
get_pre_compressed(&opts.wasm_js_bundle).await
}

async fn get_pre_compressed(file_name: &str) -> std::io::Result<NamedFile> {
match NamedFile::open(&format!("{}.br", file_name)).await {
Ok(file) => Ok(file),
Err(_) => NamedFile::open(file_name).await,
}
}

// ----- Turbine dependant route handlers -----
Expand Down Expand Up @@ -376,9 +382,41 @@ pub async fn dflt_server<M: MutableStore + 'static, T: TranslationsManager + 'st
address: addr,
..Default::default()
};

app = app.configure(config);

if let Err(err) = app.launch().await {
eprintln!("Error lauching Rocket app: {}.", err);
}
}

/// Creates and starts the default Perseus server with compression using Rocket. This should be
/// run in a `main` function annotated with `#[tokio::main]` (which requires the
/// `macros` and `rt-multi-thread` features on the `tokio` dependency).
#[cfg(feature = "dflt-server-with-compression")]
pub async fn dflt_server_with_compression<
M: MutableStore + 'static,
T: TranslationsManager + 'static,
>(
turbine: &'static Turbine<M, T>,
opts: ServerOptions,
(host, port): (String, u16),
) {
let addr = host.parse().expect("Invalid address provided to bind to.");

let mut app = perseus_base_app(turbine, opts).await;

let config = rocket::Config {
port,
address: addr,
..Default::default()
};

app = app
.configure(config)
.attach(rocket_async_compression_lib::Compression::fairing());

if let Err(err) = app.launch().await {
eprintln!("Error lauching Rocket app: {}.", err);
}
}
1 change: 1 addition & 0 deletions packages/perseus-warp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ warp = { package = "warp-fix-171", version = "0.3" } # Temporary until Warp #171
[features]
# Enables the default server configuration, which provides a convenience function if you're not adding any extra routes
dflt-server = []
dflt-server-with-compression = [ "warp/compression" ]

[package.metadata.docs.rs]
rustc-args = ["--cfg=engine"]
Expand Down
Loading

0 comments on commit 8016599

Please sign in to comment.