From 04deb1d89c96e7f8aaebbf9b92d1938ccf6effe4 Mon Sep 17 00:00:00 2001 From: soruh Date: Sun, 11 Jun 2023 04:18:07 +0200 Subject: [PATCH] compress html --- Cargo.lock | 23 +++++-- Cargo.toml | 6 +- build.rs | 9 ++- src/http.rs | 168 +++++++++++++++++++++++++++++++++++++--------------- 4 files changed, 149 insertions(+), 57 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 67d6b9f..5065731 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -137,7 +137,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.6.2", "object", "rustc-demangle", ] @@ -203,10 +203,12 @@ name = "centralex" version = "0.1.0" dependencies = [ "bytemuck", + "bytes", "color-eyre", "console-subscriber", "css-minify", "eyre", + "flate2", "futures", "hyper", "minify-html", @@ -368,12 +370,12 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.7.1", ] [[package]] @@ -725,6 +727,15 @@ dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + [[package]] name = "mio" version = "0.8.6" @@ -1209,9 +1220,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ "bytes", "futures-core", diff --git a/Cargo.toml b/Cargo.toml index 1d2ceb1..e9ba1cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,14 +26,16 @@ color-eyre = "0.6.2" tracing-error = "0.2.0" zerocopy = "0.6.1" tokio-stream = { version = "0.1.14", features = ["sync"] } -minify-html = "0.11.1" +flate2 = { version = "1.0.26", optional = true } +bytes = "1.4.0" [build-dependencies] minify-js = { version = "0.5.6", optional = true } minify-html = { version = "0.11.1", optional = true } css-minify = { version = "0.3.1", optional = true } +flate2 = { version = "1.0.26", optional = true } [features] default = ["debug_server"] -debug_server = ["dep:hyper", "minify-html", "dep:minify-js", "dep:css-minify"] +debug_server = ["dep:hyper", "minify-html", "dep:minify-js", "dep:css-minify", "dep:flate2"] tokio_console = ["dep:console-subscriber"] diff --git a/build.rs b/build.rs index 96469b9..d13fe2e 100644 --- a/build.rs +++ b/build.rs @@ -9,6 +9,7 @@ fn main() { #[cfg(feature = "debug_server")] fn pack_debug_page() -> Result<(), Box> { + use flate2::{write::GzEncoder, Compression}; use std::io::Write; use css_minify::optimizations::{Level, Minifier}; @@ -38,8 +39,12 @@ fn pack_debug_page() -> Result<(), Box> { &minify_html::Cfg::spec_compliant(), ); - std::fs::File::create(std::env::var("OUT_DIR").unwrap() + "/minified.html")? - .write_all(&html)?; + let mut encoder = GzEncoder::new( + std::fs::File::create(std::env::var("OUT_DIR").unwrap() + "/minified.html.gz")?, + Compression::best(), + ); + + encoder.write_all(&html)?; Ok(()) } diff --git a/src/http.rs b/src/http.rs index 50024e2..d0e36d7 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,15 +1,17 @@ +use bytes::BytesMut; use futures::Future; -use tokio_stream::StreamExt; - +use hyper::header::{ACCEPT_ENCODING, CACHE_CONTROL, CONTENT_ENCODING, CONTENT_TYPE}; use hyper::rt::Executor; use hyper::service::{make_service_fn, service_fn}; -use hyper::{Body, Method, Response, Server, StatusCode}; +use hyper::{Body, Method, Request, Response, Server, StatusCode}; use std::convert::Infallible; +use std::io::Read; use std::net::SocketAddr; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use tokio::sync::Mutex; use tokio_stream::wrappers::{IntervalStream, WatchStream}; +use tokio_stream::StreamExt; use tracing::error; use zerocopy::{AsBytes, FromBytes, LittleEndian, Unaligned}; @@ -30,6 +32,117 @@ impl + Send + 'static> Executor } } +const COMPRESSED_HTML: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/minified.html.gz")); + +async fn index(req: &Request) -> Result, hyper::http::Error> { + let response = Response::builder(); + + let accepts_gzip = req + .headers() + .get(ACCEPT_ENCODING) + .map_or(false, |accept_encoding| { + accept_encoding + .as_bytes() + .split(|x| *x == b',') + .filter_map(|x| x.split(|x| *x == b';').next()) + .filter_map(|x| std::str::from_utf8(x).ok()) + .any(|x| x.trim() == "gzip") + }); + + if accepts_gzip { + response + .header(CONTENT_ENCODING, "gzip") + .body(Body::from(COMPRESSED_HTML)) + } else { + let (mut sender, body) = Body::channel(); + + spawn("gunzip task", async move { + let mut decoder = + flate2::bufread::GzDecoder::new(std::io::Cursor::new(COMPRESSED_HTML)); + + let mut done = false; + while !done { + let mut chunk = BytesMut::zeroed(256); + + let mut i = 0; + + loop { + let dst = &mut chunk.as_bytes_mut()[i..]; + + if dst.is_empty() { + break; // we are done + } + + match decoder.read(dst) { + Ok(n) => { + if n == 0 { + done = true; + break; + } + + i += n; + } + Err(err) => unreachable!("failed to read from gzip decode: {err}"), + } + } + + chunk.truncate(i); + + if sender.send_data(chunk.freeze()).await.is_err() { + break; + } + } + }); + + response.body(body) + } +} + +async fn data( + _req: &Request, + port_handler: Arc>, +) -> Result, hyper::http::Error> { + let res = Response::builder().header(CACHE_CONTROL, "no-store"); + + match serde_json::to_string(&*port_handler.lock().await) { + Ok(data) => res + .header(CONTENT_TYPE, "application/json") + .body(Body::from(data)), + Err(err) => { + error!(%err, "failed to serialize data for debug server"); + res.status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::from("")) + } + } +} +fn events( + _req: &Request, + change_receiver: tokio::sync::watch::Receiver, +) -> Result, hyper::http::Error> { + Response::builder() + .status(StatusCode::OK) + .header(CACHE_CONTROL, "no-store") + .header(CONTENT_TYPE, "text/event-stream") + .body(Body::wrap_stream({ + WatchStream::new(change_receiver) + .map(|x| ("change", x)) + .merge( + IntervalStream::new(tokio::time::interval(DEBUG_SERVER_PING_INTERVAL)) + .map(|x| ("ping", x.into_std())), + ) + .filter_map(|(kind, time)| { + let timestamp = (SystemTime::now() + time.elapsed()) + .duration_since(UNIX_EPOCH) + .ok()? + .as_secs(); + + Some(Ok::<_, Infallible>(format!( + "event:{kind}\ndata: {timestamp}\n\n" + ))) + }) + })) +} + pub async fn debug_server( addr: SocketAddr, port_handler: Arc>, @@ -43,52 +156,13 @@ pub async fn debug_server( async move { Ok::<_, Infallible>(service_fn(move |req| { let port_handler = port_handler.clone(); - let change_receiver = WatchStream::new(change_receiver.clone()); + let change_receiver = change_receiver.clone(); + async move { match (req.method(), req.uri().path()) { - (&Method::GET, "/") => Ok(Response::new(Body::from(include_str!( - concat!(env!("OUT_DIR"), "/minified.html") - )))), - - (&Method::GET, "/data") => { - let res = Response::builder().header("Cache-Control", "no-store"); - - match serde_json::to_string(&*port_handler.lock().await) { - Ok(data) => res - .header("Content-Type", "application/json") - .body(Body::from(data)), - Err(err) => { - error!(%err, "failed to serialize data for debug server"); - res.status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::from("")) - } - } - } - - (&Method::GET, "/events") => Response::builder() - .status(StatusCode::OK) - .header("Cache-Control", "no-store") - .header("Content-Type", "text/event-stream") - .body(Body::wrap_stream({ - change_receiver - .map(|x| ("change", x)) - .merge( - IntervalStream::new(tokio::time::interval( - DEBUG_SERVER_PING_INTERVAL, - )) - .map(|x| ("ping", x.into_std())), - ) - .filter_map(|(kind, time)| { - let timestamp = (SystemTime::now() + time.elapsed()) - .duration_since(UNIX_EPOCH) - .ok()? - .as_secs(); - - Some(Ok::<_, Infallible>(format!( - "event:{kind}\ndata: {timestamp}\n\n" - ))) - }) - })), + (&Method::GET, "/") => index(&req).await, + (&Method::GET, "/data") => data(&req, port_handler).await, + (&Method::GET, "/events") => events(&req, change_receiver), _ => Response::builder() .status(StatusCode::NOT_FOUND) .body(Body::empty()),