From 6e64a00e531c8438930f668cc8b9d2c8ac23535b Mon Sep 17 00:00:00 2001 From: Benno van den Berg Date: Wed, 19 Jun 2024 15:17:14 +0200 Subject: [PATCH 1/6] Intermediate --- fpx/src/api.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/fpx/src/api.rs b/fpx/src/api.rs index 0efa9e8d9..dec01848e 100644 --- a/fpx/src/api.rs +++ b/fpx/src/api.rs @@ -3,6 +3,7 @@ use crate::events::ServerEvents; use crate::inspector::InspectorService; use axum::extract::FromRef; use axum::response::Html; +use axum::response::IntoResponse; use axum::routing::{any, get}; use url::Url; @@ -67,6 +68,10 @@ pub async fn create_api( .route("/api/inspect/:id", any(handlers::inspect_request_handler)) .route("/api/v1/logs", get(handlers::logs_handler)) .route("/api/ws", get(ws::ws_handler)) - .route("/", get(|| async { Html("Hello, world!") })) + .fallback(default_handler) .with_state(api_state) } + +pub async fn default_handler() -> impl IntoResponse { + "Hello from the embedded UI " +} From 44f5f235ad383c5a5b063e8ee9700dc37e489596 Mon Sep 17 00:00:00 2001 From: Benno van den Berg Date: Thu, 20 Jun 2024 14:29:13 +0200 Subject: [PATCH 2/6] Rather simple and naive file hosting for Studio --- fpx/src/api.rs | 37 ++++++++++++++---------- fpx/src/api/studio.rs | 63 +++++++++++++++++++++++++++++++++++++++++ fpx/src/commands/dev.rs | 2 +- 3 files changed, 86 insertions(+), 16 deletions(-) create mode 100644 fpx/src/api/studio.rs diff --git a/fpx/src/api.rs b/fpx/src/api.rs index dec01848e..98acbab33 100644 --- a/fpx/src/api.rs +++ b/fpx/src/api.rs @@ -2,14 +2,14 @@ use crate::data::Store; use crate::events::ServerEvents; use crate::inspector::InspectorService; use axum::extract::FromRef; -use axum::response::Html; -use axum::response::IntoResponse; use axum::routing::{any, get}; +use http::StatusCode; use url::Url; pub mod client; mod errors; pub mod handlers; +mod studio; mod ws; #[derive(Clone)] @@ -42,7 +42,19 @@ impl FromRef for InspectorService { } /// Create a API and expose it through a axum router. -pub async fn create_api( +pub fn create_api( + base_url: url::Url, + events: ServerEvents, + store: Store, + inspector_service: InspectorService, +) -> axum::Router { + let api_router = api_router(base_url, events, store, inspector_service); + axum::Router::new() + .nest("/api/", api_router) + .fallback(studio::default_handler) +} + +fn api_router( base_url: url::Url, events: ServerEvents, store: Store, @@ -54,24 +66,19 @@ pub async fn create_api( store, inspector_service, }; - axum::Router::new() .route( - "/api/requests/:id", + "/requests/:id", get(handlers::request_get_handler).delete(handlers::request_delete_handler), ) .route( - "/api/inspectors", + "/inspectors", get(handlers::inspector_list_handler).post(handlers::inspector_create_handler), ) - .route("/api/inspect", any(handlers::inspect_request_handler)) - .route("/api/inspect/:id", any(handlers::inspect_request_handler)) - .route("/api/v1/logs", get(handlers::logs_handler)) - .route("/api/ws", get(ws::ws_handler)) - .fallback(default_handler) + .route("/inspect", any(handlers::inspect_request_handler)) + .route("/inspect/:id", any(handlers::inspect_request_handler)) + .route("/v1/logs", get(handlers::logs_handler)) + .route("/ws", get(ws::ws_handler)) + .fallback(StatusCode::NOT_FOUND) .with_state(api_state) } - -pub async fn default_handler() -> impl IntoResponse { - "Hello from the embedded UI " -} diff --git a/fpx/src/api/studio.rs b/fpx/src/api/studio.rs new file mode 100644 index 000000000..a0b8be2f6 --- /dev/null +++ b/fpx/src/api/studio.rs @@ -0,0 +1,63 @@ +use axum::extract::Request; +use axum::response::{IntoResponse, Response}; +use http::StatusCode; +use include_dir::{include_dir, Dir, DirEntry}; + +static STUDIO_DIST: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../frontend/dist"); + +pub async fn default_handler(req: Request) -> Response { + // check if it is a file, if so return the file + // if it is a directory, return the index.html file + // otherwise _always_ fallback index.html + + let path = req.uri().path().trim_start_matches('/'); + + if let Some(entry) = STUDIO_DIST.get_entry(path) { + match entry { + DirEntry::Dir(dir_entry) => { + let index_html = dir_entry + .get_file("index.html") + .expect("index.html should exist in the dist directory") + .contents_utf8() + .unwrap(); + + return ( + StatusCode::OK, + [(http::header::CONTENT_TYPE, "text/html")], + index_html, + ) + .into_response(); + } + DirEntry::File(file_entry) => { + let content = file_entry.contents_utf8().unwrap(); + let content_type = + // some naive content type detection + match file_entry.path().extension().and_then(|ext| ext.to_str()) { + Some("html") => "text/html", + Some("css") => "text/css", + Some("js") => "text/javascript", + Some("ico") => "image/x-icon", + _ => "text/plain", + }; + + return ( + StatusCode::OK, + [(http::header::CONTENT_TYPE, content_type)], + content, + ) + .into_response(); + } + }; + }; + + ( + StatusCode::OK, + [(http::header::CONTENT_TYPE, "text/html")], + STUDIO_DIST + .get_file("index.html") + .expect("index.html should exist in the dist directory") + .contents_utf8() + .unwrap(), + ) + .into_response() +} diff --git a/fpx/src/commands/dev.rs b/fpx/src/commands/dev.rs index c0b38c67f..021d2e764 100644 --- a/fpx/src/commands/dev.rs +++ b/fpx/src/commands/dev.rs @@ -45,7 +45,7 @@ pub async fn handle_command(args: Args) -> Result<()> { ) .await?; - let app = api::create_api(args.base_url.clone(), events, store, inspector_service).await; + let app = api::create_api(args.base_url.clone(), events, store, inspector_service); let listener = tokio::net::TcpListener::bind(&args.listen_address) .await From 0644e60ff81bfe4ff6449bcf66896398ef54d9bc Mon Sep 17 00:00:00 2001 From: Benno van den Berg Date: Fri, 21 Jun 2024 10:35:37 +0200 Subject: [PATCH 3/6] Allow unused fn --- fpx/src/api/studio.rs | 75 ++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 47 deletions(-) diff --git a/fpx/src/api/studio.rs b/fpx/src/api/studio.rs index a0b8be2f6..d9a1ca85d 100644 --- a/fpx/src/api/studio.rs +++ b/fpx/src/api/studio.rs @@ -1,63 +1,44 @@ use axum::extract::Request; -use axum::response::{IntoResponse, Response}; +use axum::response::IntoResponse; use http::StatusCode; -use include_dir::{include_dir, Dir, DirEntry}; +use include_dir::{include_dir, Dir, DirEntry, File}; static STUDIO_DIST: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../frontend/dist"); -pub async fn default_handler(req: Request) -> Response { - // check if it is a file, if so return the file - // if it is a directory, return the index.html file - // otherwise _always_ fallback index.html - +/// A simple handler that serves the frontend Studio from STUDIO_DIST. +pub async fn default_handler(req: Request) -> impl IntoResponse { let path = req.uri().path().trim_start_matches('/'); - if let Some(entry) = STUDIO_DIST.get_entry(path) { - match entry { - DirEntry::Dir(dir_entry) => { - let index_html = dir_entry - .get_file("index.html") - .expect("index.html should exist in the dist directory") - .contents_utf8() - .unwrap(); + // Retrieve the File that according to the path. If it is a directory, see + // if there is an index.html file. Always fallback to the index.html in the + // root. + let file: Option<&File> = STUDIO_DIST + .get_entry(path) + .and_then(|entry| match entry { + DirEntry::Dir(dir_entry) => dir_entry.get_file("index.html"), + DirEntry::File(file_entry) => Some(file_entry), + }) + .or_else(|| STUDIO_DIST.get_file("index.html")); - return ( - StatusCode::OK, - [(http::header::CONTENT_TYPE, "text/html")], - index_html, - ) - .into_response(); - } - DirEntry::File(file_entry) => { - let content = file_entry.contents_utf8().unwrap(); - let content_type = - // some naive content type detection - match file_entry.path().extension().and_then(|ext| ext.to_str()) { - Some("html") => "text/html", - Some("css") => "text/css", - Some("js") => "text/javascript", - Some("ico") => "image/x-icon", - _ => "text/plain", - }; + // Just return 404 if not file was found. + let Some(file) = file else { + return StatusCode::NOT_FOUND.into_response(); + }; - return ( - StatusCode::OK, - [(http::header::CONTENT_TYPE, content_type)], - content, - ) - .into_response(); - } - }; + let content = file.contents_utf8().unwrap(); + // Naive content type detection + let content_type = match file.path().extension().and_then(|ext| ext.to_str()) { + Some("html") => "text/html", + Some("css") => "text/css", + Some("js") => "text/javascript", + Some("ico") => "image/x-icon", + _ => "text/plain", }; ( StatusCode::OK, - [(http::header::CONTENT_TYPE, "text/html")], - STUDIO_DIST - .get_file("index.html") - .expect("index.html should exist in the dist directory") - .contents_utf8() - .unwrap(), + [(http::header::CONTENT_TYPE, content_type)], + content, ) .into_response() } From 02fe4f501ff96cce3ea4867e89289414db7f3d32 Mon Sep 17 00:00:00 2001 From: Benno van den Berg Date: Fri, 21 Jun 2024 10:36:16 +0200 Subject: [PATCH 4/6] No need to parse the file as utf8, just send the content as-is Fix comment --- fpx/src/api/studio.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fpx/src/api/studio.rs b/fpx/src/api/studio.rs index d9a1ca85d..8b612acd3 100644 --- a/fpx/src/api/studio.rs +++ b/fpx/src/api/studio.rs @@ -20,12 +20,13 @@ pub async fn default_handler(req: Request) -> impl IntoResponse { }) .or_else(|| STUDIO_DIST.get_file("index.html")); - // Just return 404 if not file was found. + // Just return 404 if no file was found. let Some(file) = file else { return StatusCode::NOT_FOUND.into_response(); }; - let content = file.contents_utf8().unwrap(); + let content = file.contents(); + // Naive content type detection let content_type = match file.path().extension().and_then(|ext| ext.to_str()) { Some("html") => "text/html", From de5ffb6ea36498e72e23815a01025a0128501954 Mon Sep 17 00:00:00 2001 From: Benno van den Berg Date: Fri, 21 Jun 2024 11:07:00 +0200 Subject: [PATCH 5/6] Add some simple tracing When retrieving a file from a dir_entry, use the _full_ path, not just the name within the dir_entry. Not sure if this is a bug or not --- fpx/src/api/studio.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/fpx/src/api/studio.rs b/fpx/src/api/studio.rs index 8b612acd3..1e77ae8d8 100644 --- a/fpx/src/api/studio.rs +++ b/fpx/src/api/studio.rs @@ -2,25 +2,28 @@ use axum::extract::Request; use axum::response::IntoResponse; use http::StatusCode; use include_dir::{include_dir, Dir, DirEntry, File}; +use tracing::trace; static STUDIO_DIST: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../frontend/dist"); /// A simple handler that serves the frontend Studio from STUDIO_DIST. pub async fn default_handler(req: Request) -> impl IntoResponse { + trace!(uri=?req.uri(), "Serving file from embedded Studio"); + let path = req.uri().path().trim_start_matches('/'); // Retrieve the File that according to the path. If it is a directory, see - // if there is an index.html file. Always fallback to the index.html in the - // root. + // if there is an index.html file. Otherwise, always fallback to the + // index.html in the root. let file: Option<&File> = STUDIO_DIST .get_entry(path) .and_then(|entry| match entry { - DirEntry::Dir(dir_entry) => dir_entry.get_file("index.html"), + DirEntry::Dir(dir_entry) => dir_entry.get_file(dir_entry.path().join("index.html")), DirEntry::File(file_entry) => Some(file_entry), }) .or_else(|| STUDIO_DIST.get_file("index.html")); - // Just return 404 if no file was found. + // If nothing matches _at all_, then return a 404. let Some(file) = file else { return StatusCode::NOT_FOUND.into_response(); }; From 4886bf0d59e2f0e2204bf8e7eaae86390792ab68 Mon Sep 17 00:00:00 2001 From: Benno van den Berg Date: Fri, 21 Jun 2024 11:23:30 +0200 Subject: [PATCH 6/6] Add feature flag to embed studio Enabling this requires that Studio is compiled. Otherwise compilation will fail. --- fpx/Cargo.toml | 3 +++ fpx/src/api/studio.rs | 29 ++++++++++++++++++++++------- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/fpx/Cargo.toml b/fpx/Cargo.toml index 9da6142ca..83dda11e6 100644 --- a/fpx/Cargo.toml +++ b/fpx/Cargo.toml @@ -6,6 +6,9 @@ authors = { workspace = true } license = { workspace = true } repository = { workspace = true } +[features] +embed-studio = [] # When enabled it will embed Studio from frontend/dist + [dependencies] anyhow = { workspace = true } async-trait = { version = "0.1" } diff --git a/fpx/src/api/studio.rs b/fpx/src/api/studio.rs index 1e77ae8d8..2e8d09451 100644 --- a/fpx/src/api/studio.rs +++ b/fpx/src/api/studio.rs @@ -1,25 +1,32 @@ +// Allow unused imports since this will make it easier to work with the +// different features. +#![allow(unused_imports)] + use axum::extract::Request; use axum::response::IntoResponse; use http::StatusCode; -use include_dir::{include_dir, Dir, DirEntry, File}; -use tracing::trace; -static STUDIO_DIST: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../frontend/dist"); +#[cfg(feature = "embed-studio")] +static STUDIO_DIST: include_dir::Dir<'_> = + include_dir::include_dir!("$CARGO_MANIFEST_DIR/../frontend/dist"); /// A simple handler that serves the frontend Studio from STUDIO_DIST. +#[cfg(feature = "embed-studio")] pub async fn default_handler(req: Request) -> impl IntoResponse { - trace!(uri=?req.uri(), "Serving file from embedded Studio"); + tracing::trace!(uri=?req.uri(), "Serving file from embedded Studio"); let path = req.uri().path().trim_start_matches('/'); // Retrieve the File that according to the path. If it is a directory, see // if there is an index.html file. Otherwise, always fallback to the // index.html in the root. - let file: Option<&File> = STUDIO_DIST + let file = STUDIO_DIST .get_entry(path) .and_then(|entry| match entry { - DirEntry::Dir(dir_entry) => dir_entry.get_file(dir_entry.path().join("index.html")), - DirEntry::File(file_entry) => Some(file_entry), + include_dir::DirEntry::Dir(dir_entry) => { + dir_entry.get_file(dir_entry.path().join("index.html")) + } + include_dir::DirEntry::File(file_entry) => Some(file_entry), }) .or_else(|| STUDIO_DIST.get_file("index.html")); @@ -46,3 +53,11 @@ pub async fn default_handler(req: Request) -> impl IntoResponse { ) .into_response() } + +/// The default handler when the feature `embed-studio` is not enabled. +/// +/// For now this will simply return a 404. +#[cfg(not(feature = "embed-studio"))] +pub async fn default_handler() -> impl IntoResponse { + StatusCode::NOT_FOUND.into_response() +}