From 85ef2aa7fd3403a5fd502427d60a89c8d1cd7b18 Mon Sep 17 00:00:00 2001 From: Nilstrieb <48135649+Nilstrieb@users.noreply.github.com> Date: Sun, 6 Mar 2022 16:31:22 +0100 Subject: [PATCH] embedded --- Cargo.lock | 145 +++++++++++++++++++++++++++++---- amqp_dashboard/Cargo.toml | 10 ++- amqp_dashboard/build.rs | 66 +++++++++++++++ amqp_dashboard/src/archive.rs | 149 ++++++++++++++++++++++++++++++++++ amqp_dashboard/src/lib.rs | 71 +++++++++------- test-js/src/open-channel.js | 6 +- 6 files changed, 395 insertions(+), 52 deletions(-) create mode 100644 amqp_dashboard/build.rs create mode 100644 amqp_dashboard/src/archive.rs diff --git a/Cargo.lock b/Cargo.lock index 3eed9b0..595de17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aho-corasick" version = "0.7.18" @@ -43,12 +49,16 @@ name = "amqp_dashboard" version = "0.1.0" dependencies = [ "amqp_core", + "anyhow", "axum", + "mime_guess", "serde", "tokio", "tower", "tower-http", "tracing", + "walkdir", + "zip", ] [[package]] @@ -125,9 +135,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.4.5" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dbbc81d15ddf33148615b778836b525dbae4e0731710294b2c484e80c4858f7" +checksum = "c9f346c92c1e9a71d14fe4aaf7c2a5d9932cc4e5e48d8fb6641524416eb79ddd" dependencies = [ "async-trait", "axum-core", @@ -147,7 +157,6 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-util 0.6.9", "tower", "tower-http", "tower-layer", @@ -192,12 +201,39 @@ version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "bytes" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +[[package]] +name = "bzip2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6afcd980b5f3a45017c57e57a2fcccbb351cc43a356ce117ef760ef8052b89b0" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "cast" version = "0.2.7" @@ -207,6 +243,12 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + [[package]] name = "cfg-if" version = "1.0.0" @@ -254,6 +296,15 @@ dependencies = [ "syn", ] +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + [[package]] name = "criterion" version = "0.3.5" @@ -362,6 +413,18 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "flate2" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" +dependencies = [ + "cfg-if", + "crc32fast", + "libc", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -636,12 +699,32 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + [[package]] name = "mio" version = "0.8.0" @@ -785,6 +868,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" + [[package]] name = "plotters" version = "0.3.1" @@ -1174,6 +1263,16 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -1215,20 +1314,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio-util" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "log", - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-util" version = "0.7.0" @@ -1254,7 +1339,7 @@ dependencies = [ "pin-project", "pin-project-lite", "tokio", - "tokio-util 0.7.0", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -1274,6 +1359,7 @@ dependencies = [ "http-body", "http-range-header", "pin-project-lite", + "tower", "tower-layer", "tower-service", ] @@ -1359,6 +1445,15 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-width" version = "0.1.9" @@ -1570,3 +1665,17 @@ dependencies = [ "itertools", "strong-xml", ] + +[[package]] +name = "zip" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815" +dependencies = [ + "byteorder", + "bzip2", + "crc32fast", + "flate2", + "thiserror", + "time", +] diff --git a/amqp_dashboard/Cargo.toml b/amqp_dashboard/Cargo.toml index a5989c2..084075f 100644 --- a/amqp_dashboard/Cargo.toml +++ b/amqp_dashboard/Cargo.toml @@ -7,9 +7,17 @@ edition = "2021" [dependencies] amqp_core = { path = "../amqp_core" } -axum = "0.4.5" +anyhow = "1.0.55" +axum = "0.4.8" +mime_guess = "2.0.4" serde = { version = "1.0.136", features = ["derive"] } tokio = { version = "1.17.0", features = ["full"] } tower = "0.4.12" tower-http = { version = "0.2.3", features = ["cors"] } tracing = "0.1.31" +zip = "0.5.13" + +[build-dependencies] +anyhow = "1.0.55" +walkdir = "2.3.2" +zip = "0.5.13" diff --git a/amqp_dashboard/build.rs b/amqp_dashboard/build.rs new file mode 100644 index 0000000..5ec2511 --- /dev/null +++ b/amqp_dashboard/build.rs @@ -0,0 +1,66 @@ +use anyhow::{ensure, Context, Result}; +use std::{ + env, + fs::File, + path::{Path, PathBuf}, + process::Command, +}; +use walkdir::WalkDir; +use zip::{write::FileOptions, ZipWriter}; + +// inspired by https://fasterthanli.me/series/dont-shell-out/part-8 + +fn main() -> Result<()> { + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + + let frontend_dir = PathBuf::from(manifest_dir).join("frontend"); + + // this is not always fully correct, but we need to ignore `build` from the rerun or else it + // will always trigger itself + println!( + "cargo:rerun-if-changed={}", + frontend_dir.join("src").display() + ); + + build_frontend(&frontend_dir) +} + +fn build_frontend(path: &Path) -> Result<()> { + let status = Command::new("yarn") + .arg("build") + .current_dir(path) + .status() + .context("run yarn build")?; + + ensure!(status.success(), "Failed to build frontend"); + + let yarn_build_dir = path.join("build"); + + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + let zip_path = out_dir.join("frontend.zip"); + + let mut zw = ZipWriter::new(File::create(&zip_path).unwrap()); + + for entry in WalkDir::new(&yarn_build_dir) { + let entry = entry.context("walk build directory")?; + + let disk_path = entry.path(); + let rel_path = disk_path + .strip_prefix(&yarn_build_dir) + .unwrap() + .to_string_lossy(); + + let meta = entry.metadata().context("entry metadata")?; + + if meta.is_dir() { + zw.add_directory(rel_path, FileOptions::default()).unwrap(); + } else if meta.is_file() { + zw.start_file(rel_path, Default::default()).unwrap(); + std::io::copy(&mut File::open(disk_path).unwrap(), &mut zw).unwrap(); + } else { + println!("cargo:warning=Ignoring entry {}", disk_path.display()); + } + } + + Ok(()) +} diff --git a/amqp_dashboard/src/archive.rs b/amqp_dashboard/src/archive.rs new file mode 100644 index 0000000..47f19ef --- /dev/null +++ b/amqp_dashboard/src/archive.rs @@ -0,0 +1,149 @@ +use axum::{ + body::Body, + http::{header, Request, Response, StatusCode}, +}; +use mime_guess::mime; +use std::{ + collections::HashMap, + fmt::{Debug, Formatter}, + future, + io::Cursor, + path::Path, + task::{Context, Poll}, +}; +use tracing::trace; +use zip::ZipArchive; + +#[derive(Debug, Clone)] +enum StaticFileKind { + File { mime: mime::Mime }, + Directory, +} + +#[derive(Clone)] +struct StaticFile { + data: Vec, + kind: StaticFileKind, +} + +impl Debug for StaticFile { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("StaticFile") + .field("kind", &self.kind) + .field("data", &format!("[{} bytes]", self.data.len())) + .finish() + } +} + +#[derive(Debug, Clone)] +pub struct StaticFileService { + files: HashMap, +} + +impl StaticFileService { + pub fn new(zip: &[u8]) -> Self { + let mut archive = ZipArchive::new(Cursor::new(zip)).unwrap(); + + let mut files = HashMap::with_capacity(archive.len()); + + for i in 0..archive.len() { + let mut file = archive.by_index(i).unwrap(); + let mut data = Vec::with_capacity(usize::try_from(file.size()).unwrap()); + std::io::copy(&mut file, &mut data).unwrap(); + + trace!(name = %file.name(), size = %file.size(),"Unpacking dashboard frontend file"); + + let path = Path::new(file.name()); + + let kind = if file.is_dir() { + StaticFileKind::Directory + } else { + let mime = match path.extension() { + Some(ext) => { + mime_guess::from_ext(&ext.to_string_lossy()).first_or_octet_stream() + } + None => mime::APPLICATION_OCTET_STREAM, + }; + StaticFileKind::File { mime } + }; + + files.insert( + file.name().trim_start_matches('/').to_owned(), + StaticFile { data, kind }, + ); + } + + trace!(?files, "Created StaticFileService"); + + Self { files } + } + + fn call_inner(&mut self, req: Request) -> Result, anyhow::Error> { + trace!(?req, "Got request for static file"); + + let path = req.uri().path().trim_start_matches('/'); + + let entry = self.files.get(path); + + match entry { + Some(file) => { + if let StaticFileKind::File { mime } = &file.kind { + trace!(%path, "Found file"); + Ok(Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, mime.essence_str()) + .body(Body::from(file.data.clone()))?) + } else { + trace!(%path, "Found directory, trying to append index.html"); + + let new_path = if path.is_empty() { + "index.html".to_owned() + } else { + let new_path = path.trim_end_matches('/'); + format!("{}/index.html", new_path) + }; + + match self.files.get(&new_path) { + Some(file) => { + trace!(%new_path, "Found index.html"); + if let StaticFileKind::File { mime } = &file.kind { + Ok(Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, mime.essence_str()) + .body(Body::from(file.data.clone()))?) + } else { + unreachable!() + } + } + None => { + trace!(%new_path, "Did not find index.html"); + Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::empty())?) + } + } + } + } + None => { + trace!(%path, "Did not find file"); + Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::empty())?) + } + } + } +} + +impl tower::Service> for StaticFileService { + type Response = Response; + type Error = anyhow::Error; + type Future = future::Ready>; + + fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: Request) -> Self::Future { + future::ready(self.call_inner(req)) + } +} diff --git a/amqp_dashboard/src/lib.rs b/amqp_dashboard/src/lib.rs index e067b62..9f1ed6d 100644 --- a/amqp_dashboard/src/lib.rs +++ b/amqp_dashboard/src/lib.rs @@ -1,31 +1,42 @@ #![warn(rust_2018_idioms)] +mod archive; + +use crate::archive::StaticFileService; use amqp_core::GlobalData; use axum::{ - body::{boxed, Full}, - http::Method, - response::{Html, IntoResponse, Response}, - routing::get, + http::{Method, StatusCode}, + response::IntoResponse, + routing::{get, get_service}, Json, Router, }; use serde::Serialize; use tower_http::cors::{Any, CorsLayer}; -use tracing::info; +use tracing::{error, info}; -const INDEX_HTML: &str = include_str!("../assets/index.html"); -const SCRIPT_JS: &str = include_str!("../assets/script.js"); -const STYLE_CSS: &str = include_str!("../assets/style.css"); +// const INDEX_HTML: &str = include_str!("../assets/index.html"); +// const SCRIPT_JS: &str = include_str!("../assets/script.js"); +// const STYLE_CSS: &str = include_str!("../assets/style.css"); + +const DATA_ZIP: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/frontend.zip")); pub async fn dashboard(global_data: GlobalData) { let cors = CorsLayer::new() .allow_methods(vec![Method::GET]) .allow_origin(Any); + let static_file_service = + get_service(StaticFileService::new(DATA_ZIP)).handle_error(|error| async move { + error!(?error, "Error in static file service"); + StatusCode::INTERNAL_SERVER_ERROR + }); + let app = Router::new() - .route("/", get(get_index_html)) - .route("/script.js", get(get_script_js)) - .route("/style.css", get(get_style_css)) - .route("/api/data", get(move || get_data(global_data)).layer(cors)); + //.route("/", get(get_index_html)) + //.route("/script.js", get(get_script_js)) + //.route("/style.css", get(get_style_css)) + .route("/api/data", get(move || get_data(global_data)).layer(cors)) + .fallback(static_file_service); let socket_addr = "0.0.0.0:8080".parse().unwrap(); @@ -37,24 +48,24 @@ pub async fn dashboard(global_data: GlobalData) { .unwrap(); } -async fn get_index_html() -> impl IntoResponse { - info!("Requesting index.html"); - Html(INDEX_HTML) -} - -async fn get_script_js() -> Response { - Response::builder() - .header("content-type", "application/javascript") - .body(boxed(Full::from(SCRIPT_JS))) - .unwrap() -} - -async fn get_style_css() -> Response { - Response::builder() - .header("content-type", "text/css") - .body(boxed(Full::from(STYLE_CSS))) - .unwrap() -} +//async fn get_index_html() -> impl IntoResponse { +// info!("Requesting index.html"); +// Html(INDEX_HTML) +//} +// +//async fn get_script_js() -> Response { +// Response::builder() +// .header("content-type", "application/javascript") +// .body(boxed(Full::from(SCRIPT_JS))) +// .unwrap() +//} +// +//async fn get_style_css() -> Response { +// Response::builder() +// .header("content-type", "text/css") +// .body(boxed(Full::from(STYLE_CSS))) +// .unwrap() +//} #[derive(Serialize)] struct Data { diff --git a/test-js/src/open-channel.js b/test-js/src/open-channel.js index 2746cf8..0e636ec 100644 --- a/test-js/src/open-channel.js +++ b/test-js/src/open-channel.js @@ -6,7 +6,7 @@ const channel = await connection.createChannel(); console.log('Successfully opened channel'); -//await channel.close(); -//await connection.close(); -// +await channel.close(); +await connection.close(); + console.log('Successfully shut down connection');