From bf08bf700039377ce0164a2a3e09850f1e162738 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sun, 29 Oct 2023 22:06:20 -0400 Subject: [PATCH] Add direct URL aka path navigation --- Cargo.lock | 26 ++++++++++++++++++++------ Cargo.toml | 5 ++++- src/api/get_files.rs | 29 +++++++++++++++++++---------- src/api/get_index.rs | 20 +++++++++++++++----- src/bin/backend.rs | 2 +- src/bin/frontend.rs | 18 +++++++++++++++--- src/ui/app.rs | 10 ++++++++-- src/ui/dir/browser.rs | 22 +++++++++++++++++----- src/ui/dir/entry.rs | 16 +++++++++++++--- src/ui/dir/fs_ctx.rs | 15 +++++++++++---- src/ui/landing.rs | 18 ++++++++++++++---- src/ui/mod.rs | 2 +- src/ui/server_ctx.rs | 6 +++++- 13 files changed, 143 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb56114..d524a3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -733,7 +733,7 @@ dependencies = [ "gloo-render", "gloo-storage", "gloo-timers", - "gloo-utils", + "gloo-utils 0.1.6", "gloo-worker", ] @@ -743,7 +743,7 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f" dependencies = [ - "gloo-utils", + "gloo-utils 0.1.6", "js-sys", "serde", "wasm-bindgen", @@ -789,7 +789,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ce5ae65c5d76e2bbd9f274d7dcc00a306a79964305efa275a0ac728caaeb792" dependencies = [ "gloo-events", - "gloo-utils", + "gloo-utils 0.1.6", "serde", "serde-wasm-bindgen", "serde_urlencoded", @@ -807,7 +807,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-sink", - "gloo-utils", + "gloo-utils 0.1.6", "js-sys", "pin-project", "serde", @@ -834,7 +834,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480" dependencies = [ - "gloo-utils", + "gloo-utils 0.1.6", "js-sys", "serde", "serde_json", @@ -866,6 +866,19 @@ dependencies = [ "web-sys", ] +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "gloo-worker" version = "0.2.1" @@ -875,7 +888,7 @@ dependencies = [ "anymap2", "bincode", "gloo-console", - "gloo-utils", + "gloo-utils 0.1.6", "js-sys", "serde", "wasm-bindgen", @@ -2218,6 +2231,7 @@ dependencies = [ "bytes", "clap", "futures", + "gloo-utils 0.2.0", "log", "rand", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index 0d23d7d..68ce8a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ yew_icons = {version = "0.7", features = [ "FeatherFile", "FeatherRefreshCcw" ] } +log = "0.4" [target.'cfg(target_arch = "wasm32")'.dependencies] yew = { version = "0.20", features = [ "csr", "hydration" ] } @@ -42,9 +43,11 @@ web-sys = { version = "0.3", features = [ "RequestMode", "Response", "Window", + "Location", + "History", ] } +gloo-utils = "0.2" wasm-logger = "0.2" -log = "0.4" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] yew = { version = "0.20", features = [ "ssr" ] } diff --git a/src/api/get_files.rs b/src/api/get_files.rs index 28ff599..ca12aad 100644 --- a/src/api/get_files.rs +++ b/src/api/get_files.rs @@ -54,7 +54,7 @@ pub async fn file(path: web::Path, auth: BasicAuth, query: web::Query, auth: BasicAuth) -> std::io::Result std::io::Result> { + let root = args.dir.as_ref().unwrap(); + let domain = &args.domain; + let filepath = root.join(end_path); let scheme = if args.ssl {"https"} else {"http"}; - println!("dir PATH: {}", filepath.display()); let mut entries = Vec::new(); - if !(path.is_empty() || &*path == "/") && filepath.parent().is_some() { + if let Ok(root_canon) = root.canonicalize() { + if !filepath.canonicalize()?.starts_with(root_canon) { + return Ok(entries); + } + } + if !(end_path.is_empty() || &*end_path == "/") && filepath.parent().is_some() { let external_path = filepath.parent().expect("Path has no parent") .strip_prefix(&root).expect("path does not start with root path!").to_path_buf(); let link = format!("{}://{}/api/listdir/{}", scheme, domain, external_path.to_string_lossy()); @@ -111,7 +123,6 @@ pub async fn dir(path: web::Path, auth: BasicAuth) -> std::io::Result, auth: BasicAuth) -> std::io::Result, +} + type BoxedError = Box; pub struct IndexPage { @@ -12,8 +19,11 @@ pub struct IndexPage { } impl IndexPage { - async fn render(&self) -> impl Stream> + Send { - let renderer = yew::ServerRenderer::::new(); + async fn render(&self, query: IndexQuery) -> impl Stream> + Send { + let renderer = yew::ServerRenderer::::with_props(|| crate::ui::AppProps { + starting_path: query.path.clone(), + starting_files: query.path.map(|start| super::get_files::get_dir(&start.to_string_lossy(), &super::CliArgs::get()).ok()).flatten() + }); let before = self.before.clone(); let after = self.after.clone(); @@ -36,17 +46,17 @@ impl IndexPage { } #[get("/")] -pub async fn index_auth(page: web::Data, auth: BasicAuth) -> std::io::Result { +pub async fn index_auth(page: web::Data, query: web::Query, auth: BasicAuth) -> std::io::Result { let args = super::CliArgs::get(); if !args.authenticate(auth.user_id(), auth.password().unwrap_or("")).await { return Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Basic Authentication failed")) } Ok(HttpResponse::Ok() - .streaming(page.render().await)) + .streaming(page.render((&*query).clone()).await)) } #[get("/")] pub async fn index_no_auth(page: web::Data) -> std::io::Result { Ok(HttpResponse::Ok() - .streaming(page.render().await)) + .streaming(page.render(IndexQuery::default()).await)) } diff --git a/src/bin/backend.rs b/src/bin/backend.rs index 0a5f42c..250305b 100644 --- a/src/bin/backend.rs +++ b/src/bin/backend.rs @@ -13,7 +13,7 @@ use yarrr::api::{ async fn main() -> std::io::Result<()> { let args = yarrr::api::CliArgs::get(); - println!("cli: {:?}", args); + log::info!("cli: {:?}", args); if args.dir.is_some() { HttpServer::new(|| { diff --git a/src/bin/frontend.rs b/src/bin/frontend.rs index 6f2c17e..d4dcfd4 100644 --- a/src/bin/frontend.rs +++ b/src/bin/frontend.rs @@ -1,8 +1,20 @@ -use yarrr::ui::App; - #[cfg(target_arch = "wasm32")] fn main() { - yew::Renderer::::new().hydrate(); + wasm_logger::init(wasm_logger::Config::new(log::Level::Info)); + let url_str = gloo_utils::document() + .location() + .expect("Location init failed") + .href() + .expect("Location.href does not exist"); + let url = reqwest::Url::parse(&url_str) + .expect("Failed to parse URL"); + let props = url.query_pairs() + .filter(|(param, _)| param == "path") + .map(|(_, start_path)| yarrr::ui::AppProps { starting_path: Some(std::path::PathBuf::from(&*start_path)), starting_files: None, }) + .next() + .unwrap_or_else(|| yarrr::ui::AppProps { starting_path: None, starting_files: None, }); + log::debug!("Hydrating yew renderer"); + yew::Renderer::::with_props(props).hydrate(); } #[cfg(not(target_arch = "wasm32"))] diff --git a/src/ui/app.rs b/src/ui/app.rs index 8cf4502..459d616 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -1,10 +1,16 @@ use yew::prelude::*; +#[derive(Properties, PartialEq)] +pub struct Props { + pub starting_path: Option, + pub starting_files: Option>, +} + #[function_component(App)] -pub fn app() -> Html { +pub fn app(props: &Props) -> Html { html! { - + } } diff --git a/src/ui/dir/browser.rs b/src/ui/dir/browser.rs index b765b4b..3369955 100644 --- a/src/ui/dir/browser.rs +++ b/src/ui/dir/browser.rs @@ -9,7 +9,8 @@ use crate::data::FileEntry; type FileEntryVec = Vec; async fn listdir(scheme: &str, domain: &str, path: &str) -> reqwest::Result { - let url = format!("{}://{}/api/listdir/{}", scheme, domain, path.trim_start_matches("/")); + let url = format!("{}://{}/api/listdir/{}", scheme, domain, path.trim_start_matches("/").trim_start_matches("..")); + log::debug!("Directory API ({}): what does {} contain?", url, path); reqwest::get(&url) .await? .json() @@ -34,6 +35,8 @@ pub struct Props { pub scheme: String, pub domain: String, pub path: String, + pub prepared_path: String, + pub prepared_files: Option, } pub struct FileExplorer { @@ -45,10 +48,18 @@ impl Component for FileExplorer { type Message = Msg; type Properties = Props; - fn create(_ctx: &Context) -> Self { - Self { - files: FetchState::NotFetching, - cwd: "???".to_owned(), + fn create(ctx: &Context) -> Self { + log::debug!("prepared_path ({}) == path ({})? {}", ctx.props().prepared_path, ctx.props().path, ctx.props().prepared_path == ctx.props().path); + if ctx.props().prepared_path == ctx.props().path { + Self { + files: ctx.props().prepared_files.as_ref().map(|files| FetchState::Success(files.clone())).unwrap_or(FetchState::NotFetching), + cwd: ctx.props().prepared_path.clone(), + } + } else { + Self { + files: FetchState::NotFetching, + cwd: "???".to_owned(), + } } } @@ -107,6 +118,7 @@ impl Component for FileExplorer { }, FetchState::Success(data) => { + log::debug!("cwd ({}) != path ({})? {}", self.cwd, ctx.props().path, self.cwd != ctx.props().path); if self.cwd != ctx.props().path { ctx.link().send_message(Msg::Load); } diff --git a/src/ui/dir/entry.rs b/src/ui/dir/entry.rs index f5b39a5..8f15710 100644 --- a/src/ui/dir/entry.rs +++ b/src/ui/dir/entry.rs @@ -20,10 +20,20 @@ pub fn entry(props: &Props) -> Html { } else { html! { } }; + let navigation_url = format!("{}://{}/?path={}", + if s_ctx.ssl { "https" } else { "http" }, + s_ctx.domain, + props.entry.path.to_string_lossy()); + let navigation_url2 = navigation_url.clone(); + #[cfg(target_arch = "wasm32")] + let onclick_closure = move |event: web_sys::MouseEvent| { + event.prevent_default(); + fs_ctx.dispatch(super::FilesystemCtxAction::NavigateTo(path.clone(), navigation_url.clone())); + }; + #[cfg(not(target_arch = "wasm32"))] + let onclick_closure = move |_| fs_ctx.dispatch(super::FilesystemCtxAction::NavigateTo(path.clone(), navigation_url.clone())); html! { - +
{icon} diff --git a/src/ui/dir/fs_ctx.rs b/src/ui/dir/fs_ctx.rs index 377cc47..1c553d9 100644 --- a/src/ui/dir/fs_ctx.rs +++ b/src/ui/dir/fs_ctx.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use yew::prelude::*; pub enum FilesystemCtxAction { - NavigateTo(PathBuf), + NavigateTo(PathBuf, String), } #[derive(Clone, PartialEq, Eq)] @@ -14,9 +14,9 @@ pub struct FilesystemCtx { pub type FilesystemContext = UseReducerHandle; impl FilesystemCtx { - pub fn init() -> Self { + pub fn init(cwd: &Option) -> Self { Self { - cwd: PathBuf::from(""), + cwd: cwd.as_ref().map(|x| x.to_owned()).unwrap_or_else(|| PathBuf::from("")), } } } @@ -26,7 +26,14 @@ impl Reducible for FilesystemCtx { fn reduce(self: Rc, action: Self::Action) -> Rc { let cwd_new = match action { - FilesystemCtxAction::NavigateTo(path) => path, + #[cfg_attr(not(target_arch = "wasm32"), allow(unused_variables))] + FilesystemCtxAction::NavigateTo(path, url) => { + #[cfg(target_arch = "wasm32")] + gloo_utils::history() + .push_state_with_url(&wasm_bindgen::JsValue::undefined(), &path.to_string_lossy(), Some(&url)) + .unwrap_or(()); + path + }, }; Rc::new(Self { cwd: cwd_new }) } diff --git a/src/ui/landing.rs b/src/ui/landing.rs index 74a7153..bad7451 100644 --- a/src/ui/landing.rs +++ b/src/ui/landing.rs @@ -1,24 +1,34 @@ use yew::prelude::*; use std::rc::Rc; +#[derive(Properties, PartialEq)] +pub struct Props { + pub starting_path: Option, + pub starting_files: Option>, +} #[function_component(Landing)] -pub fn landing() -> HtmlResult { +pub fn landing(props: &Props) -> HtmlResult { + #[cfg_attr(target_arch = "wasm32", allow(unused_variables))] + let starting_path = props.starting_path.clone(); + #[cfg_attr(target_arch = "wasm32", allow(unused_variables))] + let starting_files = props.starting_files.clone(); let server_ctx = use_prepared_state!( - async move |_| -> super::ServerCtx { super::build_server_ctx().await }, () + async move |_| -> super::ServerCtx { super::build_server_ctx(starting_path, starting_files).await }, () )?.expect("Missing server-provided context"); - let fs_ctx = use_reducer(super::dir::FilesystemCtx::init); + let fs_ctx = use_reducer(|| super::dir::FilesystemCtx::init(&props.starting_path)); if server_ctx.root_dir.is_some() { let scheme = if server_ctx.ssl {"https"} else {"http"}.to_owned(); let domain = server_ctx.domain.clone(); + let prepared_files = server_ctx.files.clone(); let path = fs_ctx.cwd.to_string_lossy().to_string(); Ok(html! { > context={server_ctx}> context={fs_ctx}> - + > >> }) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 0f8c871..19e5883 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -4,7 +4,7 @@ mod landing; mod rant; mod server_ctx; -pub use app::App; +pub use app::{App, Props as AppProps}; pub use landing::Landing; pub use rant::Rant; pub use server_ctx::ServerCtx; diff --git a/src/ui/server_ctx.rs b/src/ui/server_ctx.rs index 93d1863..df42e93 100644 --- a/src/ui/server_ctx.rs +++ b/src/ui/server_ctx.rs @@ -5,14 +5,18 @@ pub struct ServerCtx { pub domain: String, pub root_dir: Option, pub ssl: bool, + pub path: Option, + pub files: Option>, } #[cfg(not(target_arch = "wasm32"))] -pub async fn build_server_ctx() -> ServerCtx { +pub async fn build_server_ctx(path: Option, files: Option>) -> ServerCtx { let args = crate::api::CliArgs::get(); ServerCtx { domain: args.domain, root_dir: args.dir, ssl: args.ssl, + path, + files, } }