Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Engine Brotli compressed and optional http servers compression #300

Merged
merged 17 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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