Add direct URL aka path navigation

This commit is contained in:
NGnius (Graham) 2023-10-29 22:06:20 -04:00
parent 9e5494035a
commit bf08bf7000
13 changed files with 143 additions and 46 deletions

26
Cargo.lock generated
View file

@ -733,7 +733,7 @@ dependencies = [
"gloo-render", "gloo-render",
"gloo-storage", "gloo-storage",
"gloo-timers", "gloo-timers",
"gloo-utils", "gloo-utils 0.1.6",
"gloo-worker", "gloo-worker",
] ]
@ -743,7 +743,7 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f" checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f"
dependencies = [ dependencies = [
"gloo-utils", "gloo-utils 0.1.6",
"js-sys", "js-sys",
"serde", "serde",
"wasm-bindgen", "wasm-bindgen",
@ -789,7 +789,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ce5ae65c5d76e2bbd9f274d7dcc00a306a79964305efa275a0ac728caaeb792" checksum = "5ce5ae65c5d76e2bbd9f274d7dcc00a306a79964305efa275a0ac728caaeb792"
dependencies = [ dependencies = [
"gloo-events", "gloo-events",
"gloo-utils", "gloo-utils 0.1.6",
"serde", "serde",
"serde-wasm-bindgen", "serde-wasm-bindgen",
"serde_urlencoded", "serde_urlencoded",
@ -807,7 +807,7 @@ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"gloo-utils", "gloo-utils 0.1.6",
"js-sys", "js-sys",
"pin-project", "pin-project",
"serde", "serde",
@ -834,7 +834,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480" checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480"
dependencies = [ dependencies = [
"gloo-utils", "gloo-utils 0.1.6",
"js-sys", "js-sys",
"serde", "serde",
"serde_json", "serde_json",
@ -866,6 +866,19 @@ dependencies = [
"web-sys", "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]] [[package]]
name = "gloo-worker" name = "gloo-worker"
version = "0.2.1" version = "0.2.1"
@ -875,7 +888,7 @@ dependencies = [
"anymap2", "anymap2",
"bincode", "bincode",
"gloo-console", "gloo-console",
"gloo-utils", "gloo-utils 0.1.6",
"js-sys", "js-sys",
"serde", "serde",
"wasm-bindgen", "wasm-bindgen",
@ -2218,6 +2231,7 @@ dependencies = [
"bytes", "bytes",
"clap", "clap",
"futures", "futures",
"gloo-utils 0.2.0",
"log", "log",
"rand", "rand",
"reqwest", "reqwest",

View file

@ -30,6 +30,7 @@ yew_icons = {version = "0.7", features = [
"FeatherFile", "FeatherFile",
"FeatherRefreshCcw" "FeatherRefreshCcw"
] } ] }
log = "0.4"
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
yew = { version = "0.20", features = [ "csr", "hydration" ] } yew = { version = "0.20", features = [ "csr", "hydration" ] }
@ -42,9 +43,11 @@ web-sys = { version = "0.3", features = [
"RequestMode", "RequestMode",
"Response", "Response",
"Window", "Window",
"Location",
"History",
] } ] }
gloo-utils = "0.2"
wasm-logger = "0.2" wasm-logger = "0.2"
log = "0.4"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
yew = { version = "0.20", features = [ "ssr" ] } yew = { version = "0.20", features = [ "ssr" ] }

View file

@ -54,7 +54,7 @@ pub async fn file(path: web::Path<String>, auth: BasicAuth, query: web::Query<Fi
let req_disposition = query.mode.unwrap_or_else( let req_disposition = query.mode.unwrap_or_else(
|| DeliveryMode::default_from_ext(filepath.extension()) || DeliveryMode::default_from_ext(filepath.extension())
).disposition(); ).disposition();
println!("file PATH: {}", filepath.display()); log::debug!("file PATH: {}", filepath.display());
Ok( Ok(
actix_files::NamedFile::open_async(&filepath).await? actix_files::NamedFile::open_async(&filepath).await?
.prefer_utf8(false) .prefer_utf8(false)
@ -94,13 +94,25 @@ pub async fn dir(path: web::Path<String>, auth: BasicAuth) -> std::io::Result<im
if !args.authenticate(auth.user_id(), auth.password().unwrap_or("")).await { if !args.authenticate(auth.user_id(), auth.password().unwrap_or("")).await {
return Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Basic Authentication failed")) return Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Basic Authentication failed"))
} }
let root = args.dir.unwrap(); let entries = get_dir(&path, &args)?;
let domain = args.domain;
let filepath = root.join(&*path); Ok(
web::Json(entries)
)
}
pub fn get_dir(end_path: &str, args: &super::CliArgs) -> std::io::Result<Vec<FileEntry>> {
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"}; let scheme = if args.ssl {"https"} else {"http"};
println!("dir PATH: {}", filepath.display());
let mut entries = Vec::new(); 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") let external_path = filepath.parent().expect("Path has no parent")
.strip_prefix(&root).expect("path does not start with root path!").to_path_buf(); .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()); let link = format!("{}://{}/api/listdir/{}", scheme, domain, external_path.to_string_lossy());
@ -111,7 +123,6 @@ pub async fn dir(path: web::Path<String>, auth: BasicAuth) -> std::io::Result<im
url: url_escape::encode_path(&link).to_owned().to_string(), url: url_escape::encode_path(&link).to_owned().to_string(),
}); });
} }
//let pseudo_root = std::path::PathBuf::from("/");
// build dir entry json // build dir entry json
for e in filepath.read_dir()? { for e in filepath.read_dir()? {
let entry = e?; let entry = e?;
@ -136,7 +147,5 @@ pub async fn dir(path: web::Path<String>, auth: BasicAuth) -> std::io::Result<im
} }
// sort alphabetically // sort alphabetically
entries.sort_by(|a, b| a.name.cmp(&b.name)); entries.sort_by(|a, b| a.name.cmp(&b.name));
Ok( Ok(entries)
web::Json(entries)
)
} }

View file

@ -4,6 +4,13 @@ use actix_web_httpauth::extractors::basic::BasicAuth;
use bytes::Bytes; use bytes::Bytes;
use futures::stream::{self, Stream, StreamExt}; use futures::stream::{self, Stream, StreamExt};
use serde::Deserialize;
#[derive(Deserialize, Default, Clone)]
pub struct IndexQuery {
path: Option<std::path::PathBuf>,
}
type BoxedError = Box<dyn std::error::Error + 'static>; type BoxedError = Box<dyn std::error::Error + 'static>;
pub struct IndexPage { pub struct IndexPage {
@ -12,8 +19,11 @@ pub struct IndexPage {
} }
impl IndexPage { impl IndexPage {
async fn render(&self) -> impl Stream<Item = Result<Bytes, BoxedError>> + Send { async fn render(&self, query: IndexQuery) -> impl Stream<Item = Result<Bytes, BoxedError>> + Send {
let renderer = yew::ServerRenderer::<crate::ui::App>::new(); let renderer = yew::ServerRenderer::<crate::ui::App>::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 before = self.before.clone();
let after = self.after.clone(); let after = self.after.clone();
@ -36,17 +46,17 @@ impl IndexPage {
} }
#[get("/")] #[get("/")]
pub async fn index_auth(page: web::Data<IndexPage>, auth: BasicAuth) -> std::io::Result<impl Responder> { pub async fn index_auth(page: web::Data<IndexPage>, query: web::Query<IndexQuery>, auth: BasicAuth) -> std::io::Result<impl Responder> {
let args = super::CliArgs::get(); let args = super::CliArgs::get();
if !args.authenticate(auth.user_id(), auth.password().unwrap_or("")).await { if !args.authenticate(auth.user_id(), auth.password().unwrap_or("")).await {
return Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Basic Authentication failed")) return Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Basic Authentication failed"))
} }
Ok(HttpResponse::Ok() Ok(HttpResponse::Ok()
.streaming(page.render().await)) .streaming(page.render((&*query).clone()).await))
} }
#[get("/")] #[get("/")]
pub async fn index_no_auth(page: web::Data<IndexPage>) -> std::io::Result<impl Responder> { pub async fn index_no_auth(page: web::Data<IndexPage>) -> std::io::Result<impl Responder> {
Ok(HttpResponse::Ok() Ok(HttpResponse::Ok()
.streaming(page.render().await)) .streaming(page.render(IndexQuery::default()).await))
} }

View file

@ -13,7 +13,7 @@ use yarrr::api::{
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
let args = yarrr::api::CliArgs::get(); let args = yarrr::api::CliArgs::get();
println!("cli: {:?}", args); log::info!("cli: {:?}", args);
if args.dir.is_some() { if args.dir.is_some() {
HttpServer::new(|| { HttpServer::new(|| {

View file

@ -1,8 +1,20 @@
use yarrr::ui::App;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
fn main() { fn main() {
yew::Renderer::<App>::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::<yarrr::ui::App>::with_props(props).hydrate();
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]

View file

@ -1,10 +1,16 @@
use yew::prelude::*; use yew::prelude::*;
#[derive(Properties, PartialEq)]
pub struct Props {
pub starting_path: Option<std::path::PathBuf>,
pub starting_files: Option<Vec<crate::data::FileEntry>>,
}
#[function_component(App)] #[function_component(App)]
pub fn app() -> Html { pub fn app(props: &Props) -> Html {
html! { html! {
<Suspense fallback={html!{"..."}}> <Suspense fallback={html!{"..."}}>
<super::Landing /> <super::Landing starting_path={props.starting_path.clone()} starting_files={props.starting_files.clone()}/>
</Suspense> </Suspense>
} }
} }

View file

@ -9,7 +9,8 @@ use crate::data::FileEntry;
type FileEntryVec = Vec<FileEntry>; type FileEntryVec = Vec<FileEntry>;
async fn listdir(scheme: &str, domain: &str, path: &str) -> reqwest::Result<FileEntryVec> { async fn listdir(scheme: &str, domain: &str, path: &str) -> reqwest::Result<FileEntryVec> {
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) reqwest::get(&url)
.await? .await?
.json() .json()
@ -34,6 +35,8 @@ pub struct Props {
pub scheme: String, pub scheme: String,
pub domain: String, pub domain: String,
pub path: String, pub path: String,
pub prepared_path: String,
pub prepared_files: Option<FileEntryVec>,
} }
pub struct FileExplorer { pub struct FileExplorer {
@ -45,12 +48,20 @@ impl Component for FileExplorer {
type Message = Msg; type Message = Msg;
type Properties = Props; type Properties = Props;
fn create(_ctx: &Context<Self>) -> Self { fn create(ctx: &Context<Self>) -> 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 { Self {
files: FetchState::NotFetching, files: FetchState::NotFetching,
cwd: "???".to_owned(), cwd: "???".to_owned(),
} }
} }
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool { fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg { match msg {
@ -107,6 +118,7 @@ impl Component for FileExplorer {
</div> </div>
}, },
FetchState::Success(data) => { FetchState::Success(data) => {
log::debug!("cwd ({}) != path ({})? {}", self.cwd, ctx.props().path, self.cwd != ctx.props().path);
if self.cwd != ctx.props().path { if self.cwd != ctx.props().path {
ctx.link().send_message(Msg::Load); ctx.link().send_message(Msg::Load);
} }

View file

@ -20,10 +20,20 @@ pub fn entry(props: &Props) -> Html {
} else { } else {
html! { <Icon icon_id={IconId::FeatherFolder} width={icon_dimension.clone()} height={icon_dimension.clone()}/> } html! { <Icon icon_id={IconId::FeatherFolder} width={icon_dimension.clone()} height={icon_dimension.clone()}/> }
}; };
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! { html! {
<a href={"#"} class={classes!("yarrr-file-entry-link")} onclick={ <a href={navigation_url2} class={classes!("yarrr-file-entry-link")} onclick={onclick_closure}>
move |_| fs_ctx.dispatch(super::FilesystemCtxAction::NavigateTo(path.clone()))
}>
<div class={classes!("yarrr-file-entry-dir")}> <div class={classes!("yarrr-file-entry-dir")}>
<div class={classes!("yarrr-file-entry-icon")}> <div class={classes!("yarrr-file-entry-icon")}>
{icon} {icon}

View file

@ -3,7 +3,7 @@ use std::path::PathBuf;
use yew::prelude::*; use yew::prelude::*;
pub enum FilesystemCtxAction { pub enum FilesystemCtxAction {
NavigateTo(PathBuf), NavigateTo(PathBuf, String),
} }
#[derive(Clone, PartialEq, Eq)] #[derive(Clone, PartialEq, Eq)]
@ -14,9 +14,9 @@ pub struct FilesystemCtx {
pub type FilesystemContext = UseReducerHandle<FilesystemCtx>; pub type FilesystemContext = UseReducerHandle<FilesystemCtx>;
impl FilesystemCtx { impl FilesystemCtx {
pub fn init() -> Self { pub fn init(cwd: &Option<PathBuf>) -> Self {
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<Self>, action: Self::Action) -> Rc<Self> { fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
let cwd_new = match action { 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 }) Rc::new(Self { cwd: cwd_new })
} }

View file

@ -1,24 +1,34 @@
use yew::prelude::*; use yew::prelude::*;
use std::rc::Rc; use std::rc::Rc;
#[derive(Properties, PartialEq)]
pub struct Props {
pub starting_path: Option<std::path::PathBuf>,
pub starting_files: Option<Vec<crate::data::FileEntry>>,
}
#[function_component(Landing)] #[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!( 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"); )?.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() { if server_ctx.root_dir.is_some() {
let scheme = if server_ctx.ssl {"https"} else {"http"}.to_owned(); let scheme = if server_ctx.ssl {"https"} else {"http"}.to_owned();
let domain = server_ctx.domain.clone(); let domain = server_ctx.domain.clone();
let prepared_files = server_ctx.files.clone();
let path = fs_ctx.cwd.to_string_lossy().to_string(); let path = fs_ctx.cwd.to_string_lossy().to_string();
Ok(html! { Ok(html! {
<ContextProvider<Rc<super::ServerCtx>> context={server_ctx}> <ContextProvider<Rc<super::ServerCtx>> context={server_ctx}>
<img src="/banner.png" class={classes!("yarrr-proof-of-purchase")}/> <img src="/banner.png" class={classes!("yarrr-proof-of-purchase")}/>
<ContextProvider<super::dir::FilesystemContext> context={fs_ctx}> <ContextProvider<super::dir::FilesystemContext> context={fs_ctx}>
<super::dir::FileExplorer {scheme} {domain} {path}/> <super::dir::FileExplorer {scheme} {domain} {path} prepared_path={props.starting_path.clone().unwrap_or("".into()).to_string_lossy().to_string()} {prepared_files}/>
</ContextProvider<super::dir::FilesystemContext>> </ContextProvider<super::dir::FilesystemContext>>
</ContextProvider<Rc<super::ServerCtx>>> </ContextProvider<Rc<super::ServerCtx>>>
}) })

View file

@ -4,7 +4,7 @@ mod landing;
mod rant; mod rant;
mod server_ctx; mod server_ctx;
pub use app::App; pub use app::{App, Props as AppProps};
pub use landing::Landing; pub use landing::Landing;
pub use rant::Rant; pub use rant::Rant;
pub use server_ctx::ServerCtx; pub use server_ctx::ServerCtx;

View file

@ -5,14 +5,18 @@ pub struct ServerCtx {
pub domain: String, pub domain: String,
pub root_dir: Option<std::path::PathBuf>, pub root_dir: Option<std::path::PathBuf>,
pub ssl: bool, pub ssl: bool,
pub path: Option<std::path::PathBuf>,
pub files: Option<Vec<crate::data::FileEntry>>,
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub async fn build_server_ctx() -> ServerCtx { pub async fn build_server_ctx(path: Option<std::path::PathBuf>, files: Option<Vec<crate::data::FileEntry>>) -> ServerCtx {
let args = crate::api::CliArgs::get(); let args = crate::api::CliArgs::get();
ServerCtx { ServerCtx {
domain: args.domain, domain: args.domain,
root_dir: args.dir, root_dir: args.dir,
ssl: args.ssl, ssl: args.ssl,
path,
files,
} }
} }