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..772af95 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,8 +1,12 @@ -use crate::kernel_types::Payload; -use crate::{Message, Request as uqRequest, Response as uqResponse}; +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; +use std::collections::{HashMap, VecDeque}; +use std::path::Path; use thiserror::Error; // @@ -377,3 +381,194 @@ 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 _ = uqRequest::new() + .target(Address::from_str("our@vfs:sys:uqbar")?) + .ipc(serde_json::to_vec(&VfsRequest { + path: format!( + "/{}/pkg/{}/index.html", + our.package_id().to_string(), + directory + ), + action: VfsAction::Read, + })?) + .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 initial_path = format!("{}/pkg/{}", our.package_id().to_string(), directory); + + let mut queue = VecDeque::new(); + queue.push_back(initial_path.clone()); + + while let Some(path) = queue.pop_front() { + let directory_response = uqRequest::new() + .target(Address::from_str("our@vfs:sys:uqbar")?) + .ipc(serde_json::to_vec(&VfsRequest { + path, + action: VfsAction::ReadDir, + })?) + .send_and_await_response(5)?; + + let Ok(directory_response) = directory_response else { + return Err(anyhow::anyhow!("serve_ui: no response for path")); + }; + + 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: {:?}", + directory_ipc + )) + } + }; + } + + 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 _ = uqRequest::new() + .target(Address::from_str("our@vfs:sys:uqbar")?) + .ipc(serde_json::to_vec(&VfsRequest { + path: format!("{}/pkg/{}", our.package_id().to_string(), target_path), + action: VfsAction::Read, + })?) + .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(()) +} + +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(()) +} 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]), }