From e809b3edb6e869d2b21fa74ada6c8fe8b1df353f Mon Sep 17 00:00:00 2001 From: Will Galebach Date: Mon, 18 Dec 2023 18:44:40 +0000 Subject: [PATCH 1/4] Added functions to serve UI --- Cargo.lock | 32 +++++++++++ Cargo.toml | 1 + src/http.rs | 159 +++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 189 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9430e7c..6ce3d67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -144,6 +144,22 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[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 = "percent-encoding" version = "2.3.0" @@ -308,6 +324,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -348,6 +373,7 @@ dependencies = [ "anyhow", "bincode", "http", + "mime_guess", "rand", "serde", "serde_json", @@ -367,6 +393,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 7f1672a..fcb68e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,4 +12,5 @@ serde_json = "1.0" rand = "0.8" thiserror = "1.0" url = "2.4.1" +mime_guess = "2.0" wit-bindgen = { git = "https://github.com/bytecodealliance/wit-bindgen", rev = "efcc759" } diff --git a/src/http.rs b/src/http.rs index 5b8912a..f47a085 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,8 +1,9 @@ -use crate::kernel_types::Payload; -use crate::{Message, Request as uqRequest, Response as uqResponse}; +use crate::kernel_types::{Payload, VfsAction, VfsRequest, VfsResponse}; +use crate::{get_payload, Address, Message, Request as uqRequest, Response as uqResponse}; pub use http::*; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; +use std::path::Path; use thiserror::Error; // @@ -377,3 +378,155 @@ pub fn send_request_await_response( }), } } + +pub fn get_mime_type(filename: &str) -> String { + let file_path = Path::new(filename); + + let extension = file_path + .extension() + .and_then(|ext| ext.to_str()) + .unwrap_or("octet-stream"); + + mime_guess::from_ext(extension) + .first_or_octet_stream() + .to_string() +} + +// Serve index.html +pub fn serve_index_html(our: &Address, directory: &str) -> anyhow::Result<(), anyhow::Error> { + let drive = our.process.to_string().split(":").collect::>()[1..].join(":"); + + let _ = uqRequest::new() + .target(Address::from_str("our@vfs:sys:uqbar")?) + .ipc(serde_json::to_vec(&VfsRequest { + drive, + action: VfsAction::GetEntry(format!("{}/index.html", directory)), + })?) + .send_and_await_response(5)?; + + let Some(payload) = get_payload() else { + return Err(anyhow::anyhow!("serve_index_html: no index.html payload")); + }; + + let index = String::from_utf8(payload.bytes)?; + + // index.html will be served from the root path of your app + bind_http_static_path( + "/", + true, + false, + Some("text/html".to_string()), + index.to_string().as_bytes().to_vec(), + )?; + + Ok(()) +} + +// Serve static files by binding all of them statically, including index.html +pub fn serve_ui(our: &Address, directory: &str) -> anyhow::Result<(), anyhow::Error> { + serve_index_html(our, directory)?; + + let drive = our.process.to_string().split(":").collect::>()[1..].join(":"); + let leading_path_segment = format!("/{}", directory); // The process name + + let mut queue = VecDeque::new(); + queue.push_back(directory.to_string()); + + while let Some(path) = queue.pop_front() { + let assets_response = uqRequest::new() + .target(Address::from_str("our@vfs:sys:uqbar")?) + .ipc(serde_json::to_vec(&VfsRequest { + drive: drive.clone(), + action: VfsAction::GetEntry(path.clone()), + })?) + .send_and_await_response(5)?; + + let Ok(assets_response) = assets_response else { + return Err(anyhow::anyhow!("serve_ui: no response for path")); + }; + + let assets_ipc = serde_json::from_slice::(&assets_response.ipc())?; + + // Determine if it's a file or a directory + let (is_file, children) = match assets_ipc { + VfsResponse::GetEntry { children, is_file } => (is_file, children), + _ => { + return Err(anyhow::anyhow!( + "serve_ui: unexpected response for path: {:?}", + assets_ipc + )) + } + }; + + if is_file { + // Skip index.html, since we already served it + if format!("{}/index.html", directory) == path { + continue; + } + + // If it's a file, serve it statically + let Some(payload) = get_payload() else { + return Err(anyhow::anyhow!("serve_ui: no payload for {}", path)); + }; + + let content_type = get_mime_type(&path); + + bind_http_static_path( + path.replace(&leading_path_segment, ""), + true, // Must be authenticated + false, // Is not local-only + Some(content_type), + payload.bytes, + )?; + } else { + // If it's a directory, push all of its children onto the queue + for child in children { + if child != path { + queue.push_back(child); + } + } + } + } + + Ok(()) +} + +pub fn handle_ui_asset_request( + our: &Address, + directory: &str, + path: &str, +) -> anyhow::Result<(), anyhow::Error> { + let parts: Vec<&str> = path.split(&our.process.to_string()).collect(); + let after_process = parts.get(1).unwrap_or(&""); + + let target_path = format!("{}/{}", directory, after_process.trim_start_matches('/')); + + let drive = our.process.to_string().split(":").collect::>()[1..].join(":"); + + let _ = uqRequest::new() + .target(Address::from_str("our@vfs:sys:uqbar")?) + .ipc(serde_json::to_vec(&VfsRequest { + drive, + action: VfsAction::GetEntry(target_path), + })?) + .send_and_await_response(5)?; + + let mut headers = HashMap::new(); + let content_type = get_mime_type(&path); + headers.insert("Content-Type".to_string(), content_type); + + uqResponse::new() + .ipc( + serde_json::json!(HttpResponse { + status: 200, + headers, + }) + .to_string() + .as_bytes() + .to_vec(), + ) + .inherit(true) + .send()?; + + Ok(()) +} From bdbd69b1b05f267ea03b7a1bdbd58feb4b792992 Mon Sep 17 00:00:00 2001 From: Will Galebach Date: Wed, 20 Dec 2023 15:53:06 +0000 Subject: [PATCH 2/4] Added send_ws_push --- src/http.rs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/http.rs b/src/http.rs index f47a085..840dcc9 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,5 +1,5 @@ use crate::kernel_types::{Payload, VfsAction, VfsRequest, VfsResponse}; -use crate::{get_payload, Address, Message, Request as uqRequest, Response as uqResponse}; +use crate::{get_payload, Address, ProcessId, Message, Request as uqRequest, Response as uqResponse, Payload as uqPayload}; pub use http::*; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, VecDeque}; @@ -530,3 +530,26 @@ pub fn handle_ui_asset_request( Ok(()) } + +pub fn send_ws_push( + node: String, + channel_id: u32, + message_type: WsMessageType, + payload: uqPayload, +) -> anyhow::Result<()> { + uqRequest::new() + .target(Address::new( + node, + ProcessId::from_str("http_server:sys:uqbar").unwrap(), + )) + .ipc( + serde_json::json!(HttpServerRequest::WebSocketPush { + channel_id, + message_type, + }).to_string().as_bytes().to_vec(), + ) + .payload(payload) + .send()?; + + Ok(()) +} From fb050f0d8c737ca89cec984e4235631dc225946d Mon Sep 17 00:00:00 2001 From: bitful-pannul Date: Wed, 20 Dec 2023 15:26:34 -0300 Subject: [PATCH 3/4] vfs: metadata types --- src/kernel_types.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/kernel_types.rs b/src/kernel_types.rs index 3b2b90f..64e8c27 100644 --- a/src/kernel_types.rs +++ b/src/kernel_types.rs @@ -206,6 +206,7 @@ pub enum VfsAction { RemoveDir, RemoveDirAll, Rename(String), + Metadata, AddZip, Len, SetLen(u64), @@ -219,13 +220,34 @@ pub enum SeekFrom { Current(i64), } +#[derive(Debug, Serialize, Deserialize)] +pub enum FileType { + File, + Directory, + Symlink, + Other, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct FileMetadata { + pub file_type: FileType, + pub len: u64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct DirEntry { + pub path: String, + pub file_type: FileType, +} + #[derive(Debug, Serialize, Deserialize)] pub enum VfsResponse { Ok, Err(VfsError), Read, - ReadDir(Vec), + ReadDir(Vec), ReadToString(String), + Metadata(FileMetadata), Len(u64), Hash([u8; 32]), } From b086852ca5479ebfeea0e1efb60f1dc8d4ae34c7 Mon Sep 17 00:00:00 2001 From: Will Galebach Date: Wed, 20 Dec 2023 22:33:16 +0000 Subject: [PATCH 4/4] Updated UI-serving functions --- src/http.rs | 125 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 72 insertions(+), 53 deletions(-) diff --git a/src/http.rs b/src/http.rs index 840dcc9..772af95 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,5 +1,8 @@ -use crate::kernel_types::{Payload, VfsAction, VfsRequest, VfsResponse}; -use crate::{get_payload, Address, ProcessId, Message, Request as uqRequest, Response as uqResponse, Payload as uqPayload}; +use crate::kernel_types::{FileType, Payload, VfsAction, VfsRequest, VfsResponse}; +use crate::{ + get_payload, Address, Message, Payload as uqPayload, ProcessId, Request as uqRequest, + Response as uqResponse, +}; pub use http::*; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, VecDeque}; @@ -394,13 +397,15 @@ pub fn get_mime_type(filename: &str) -> String { // Serve index.html pub fn serve_index_html(our: &Address, directory: &str) -> anyhow::Result<(), anyhow::Error> { - let drive = our.process.to_string().split(":").collect::>()[1..].join(":"); - let _ = uqRequest::new() .target(Address::from_str("our@vfs:sys:uqbar")?) .ipc(serde_json::to_vec(&VfsRequest { - drive, - action: VfsAction::GetEntry(format!("{}/index.html", directory)), + path: format!( + "/{}/pkg/{}/index.html", + our.package_id().to_string(), + directory + ), + action: VfsAction::Read, })?) .send_and_await_response(5)?; @@ -426,66 +431,79 @@ pub fn serve_index_html(our: &Address, directory: &str) -> anyhow::Result<(), an pub fn serve_ui(our: &Address, directory: &str) -> anyhow::Result<(), anyhow::Error> { serve_index_html(our, directory)?; - let drive = our.process.to_string().split(":").collect::>()[1..].join(":"); - let leading_path_segment = format!("/{}", directory); // The process name + let initial_path = format!("{}/pkg/{}", our.package_id().to_string(), directory); let mut queue = VecDeque::new(); - queue.push_back(directory.to_string()); + queue.push_back(initial_path.clone()); while let Some(path) = queue.pop_front() { - let assets_response = uqRequest::new() + let directory_response = uqRequest::new() .target(Address::from_str("our@vfs:sys:uqbar")?) .ipc(serde_json::to_vec(&VfsRequest { - drive: drive.clone(), - action: VfsAction::GetEntry(path.clone()), + path, + action: VfsAction::ReadDir, })?) .send_and_await_response(5)?; - let Ok(assets_response) = assets_response else { + let Ok(directory_response) = directory_response else { return Err(anyhow::anyhow!("serve_ui: no response for path")); }; - let assets_ipc = serde_json::from_slice::(&assets_response.ipc())?; - - // Determine if it's a file or a directory - let (is_file, children) = match assets_ipc { - VfsResponse::GetEntry { children, is_file } => (is_file, children), + let directory_ipc = serde_json::from_slice::(&directory_response.ipc())?; + + // Determine if it's a file or a directory and handle appropriately + match directory_ipc { + VfsResponse::ReadDir(directory_info) => { + for entry in directory_info { + match entry.file_type { + // If it's a file, serve it statically + FileType::File => { + if format!("{}/index.html", initial_path.trim_start_matches("/")) + == entry.path + { + continue; + } + + let _ = uqRequest::new() + .target(Address::from_str("our@vfs:sys:uqbar")?) + .ipc(serde_json::to_vec(&VfsRequest { + path: entry.path.clone(), + action: VfsAction::Read, + })?) + .send_and_await_response(5)?; + + let Some(payload) = get_payload() else { + return Err(anyhow::anyhow!( + "serve_ui: no payload for {}", + entry.path + )); + }; + + let content_type = get_mime_type(&entry.path); + + bind_http_static_path( + entry.path.replace(&initial_path, ""), + true, // Must be authenticated + false, // Is not local-only + Some(content_type), + payload.bytes, + )?; + } + FileType::Directory => { + // Push the directory onto the queue + queue.push_back(entry.path); + } + _ => {} + } + } + } _ => { return Err(anyhow::anyhow!( "serve_ui: unexpected response for path: {:?}", - assets_ipc + directory_ipc )) } }; - - if is_file { - // Skip index.html, since we already served it - if format!("{}/index.html", directory) == path { - continue; - } - - // If it's a file, serve it statically - let Some(payload) = get_payload() else { - return Err(anyhow::anyhow!("serve_ui: no payload for {}", path)); - }; - - let content_type = get_mime_type(&path); - - bind_http_static_path( - path.replace(&leading_path_segment, ""), - true, // Must be authenticated - false, // Is not local-only - Some(content_type), - payload.bytes, - )?; - } else { - // If it's a directory, push all of its children onto the queue - for child in children { - if child != path { - queue.push_back(child); - } - } - } } Ok(()) @@ -501,13 +519,11 @@ pub fn handle_ui_asset_request( let target_path = format!("{}/{}", directory, after_process.trim_start_matches('/')); - let drive = our.process.to_string().split(":").collect::>()[1..].join(":"); - let _ = uqRequest::new() .target(Address::from_str("our@vfs:sys:uqbar")?) .ipc(serde_json::to_vec(&VfsRequest { - drive, - action: VfsAction::GetEntry(target_path), + path: format!("{}/pkg/{}", our.package_id().to_string(), target_path), + action: VfsAction::Read, })?) .send_and_await_response(5)?; @@ -546,7 +562,10 @@ pub fn send_ws_push( serde_json::json!(HttpServerRequest::WebSocketPush { channel_id, message_type, - }).to_string().as_bytes().to_vec(), + }) + .to_string() + .as_bytes() + .to_vec(), ) .payload(payload) .send()?;