Add direct URL aka path navigation
This commit is contained in:
parent
9e5494035a
commit
bf08bf7000
13 changed files with 143 additions and 46 deletions
26
Cargo.lock
generated
26
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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" ] }
|
||||
|
|
|
@ -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(
|
||||
|| DeliveryMode::default_from_ext(filepath.extension())
|
||||
).disposition();
|
||||
println!("file PATH: {}", filepath.display());
|
||||
log::debug!("file PATH: {}", filepath.display());
|
||||
Ok(
|
||||
actix_files::NamedFile::open_async(&filepath).await?
|
||||
.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 {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Basic Authentication failed"))
|
||||
}
|
||||
let root = args.dir.unwrap();
|
||||
let domain = args.domain;
|
||||
let filepath = root.join(&*path);
|
||||
let entries = get_dir(&path, &args)?;
|
||||
|
||||
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"};
|
||||
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<String>, auth: BasicAuth) -> std::io::Result<im
|
|||
url: url_escape::encode_path(&link).to_owned().to_string(),
|
||||
});
|
||||
}
|
||||
//let pseudo_root = std::path::PathBuf::from("/");
|
||||
// build dir entry json
|
||||
for e in filepath.read_dir()? {
|
||||
let entry = e?;
|
||||
|
@ -136,7 +147,5 @@ pub async fn dir(path: web::Path<String>, auth: BasicAuth) -> std::io::Result<im
|
|||
}
|
||||
// sort alphabetically
|
||||
entries.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Ok(
|
||||
web::Json(entries)
|
||||
)
|
||||
Ok(entries)
|
||||
}
|
||||
|
|
|
@ -4,6 +4,13 @@ use actix_web_httpauth::extractors::basic::BasicAuth;
|
|||
use bytes::Bytes;
|
||||
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>;
|
||||
|
||||
pub struct IndexPage {
|
||||
|
@ -12,8 +19,11 @@ pub struct IndexPage {
|
|||
}
|
||||
|
||||
impl IndexPage {
|
||||
async fn render(&self) -> impl Stream<Item = Result<Bytes, BoxedError>> + Send {
|
||||
let renderer = yew::ServerRenderer::<crate::ui::App>::new();
|
||||
async fn render(&self, query: IndexQuery) -> impl Stream<Item = Result<Bytes, BoxedError>> + Send {
|
||||
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 after = self.after.clone();
|
||||
|
||||
|
@ -36,17 +46,17 @@ impl IndexPage {
|
|||
}
|
||||
|
||||
#[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();
|
||||
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<IndexPage>) -> std::io::Result<impl Responder> {
|
||||
Ok(HttpResponse::Ok()
|
||||
.streaming(page.render().await))
|
||||
.streaming(page.render(IndexQuery::default()).await))
|
||||
}
|
||||
|
|
|
@ -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(|| {
|
||||
|
|
|
@ -1,8 +1,20 @@
|
|||
use yarrr::ui::App;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
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"))]
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
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)]
|
||||
pub fn app() -> Html {
|
||||
pub fn app(props: &Props) -> Html {
|
||||
html! {
|
||||
<Suspense fallback={html!{"..."}}>
|
||||
<super::Landing />
|
||||
<super::Landing starting_path={props.starting_path.clone()} starting_files={props.starting_files.clone()}/>
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,8 @@ use crate::data::FileEntry;
|
|||
type FileEntryVec = Vec<FileEntry>;
|
||||
|
||||
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)
|
||||
.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<FileEntryVec>,
|
||||
}
|
||||
|
||||
pub struct FileExplorer {
|
||||
|
@ -45,10 +48,18 @@ impl Component for FileExplorer {
|
|||
type Message = Msg;
|
||||
type Properties = Props;
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self {
|
||||
files: FetchState::NotFetching,
|
||||
cwd: "???".to_owned(),
|
||||
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 {
|
||||
files: FetchState::NotFetching,
|
||||
cwd: "???".to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,6 +118,7 @@ impl Component for FileExplorer {
|
|||
</div>
|
||||
},
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -20,10 +20,20 @@ pub fn entry(props: &Props) -> Html {
|
|||
} else {
|
||||
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! {
|
||||
<a href={"#"} class={classes!("yarrr-file-entry-link")} onclick={
|
||||
move |_| fs_ctx.dispatch(super::FilesystemCtxAction::NavigateTo(path.clone()))
|
||||
}>
|
||||
<a href={navigation_url2} class={classes!("yarrr-file-entry-link")} onclick={onclick_closure}>
|
||||
<div class={classes!("yarrr-file-entry-dir")}>
|
||||
<div class={classes!("yarrr-file-entry-icon")}>
|
||||
{icon}
|
||||
|
|
|
@ -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<FilesystemCtx>;
|
||||
|
||||
impl FilesystemCtx {
|
||||
pub fn init() -> Self {
|
||||
pub fn init(cwd: &Option<PathBuf>) -> 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> {
|
||||
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 })
|
||||
}
|
||||
|
|
|
@ -1,24 +1,34 @@
|
|||
use yew::prelude::*;
|
||||
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)]
|
||||
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! {
|
||||
<ContextProvider<Rc<super::ServerCtx>> context={server_ctx}>
|
||||
<img src="/banner.png" class={classes!("yarrr-proof-of-purchase")}/>
|
||||
<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<Rc<super::ServerCtx>>>
|
||||
})
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -5,14 +5,18 @@ pub struct ServerCtx {
|
|||
pub domain: String,
|
||||
pub root_dir: Option<std::path::PathBuf>,
|
||||
pub ssl: bool,
|
||||
pub path: Option<std::path::PathBuf>,
|
||||
pub files: Option<Vec<crate::data::FileEntry>>,
|
||||
}
|
||||
|
||||
#[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();
|
||||
ServerCtx {
|
||||
domain: args.domain,
|
||||
root_dir: args.dir,
|
||||
ssl: args.ssl,
|
||||
path,
|
||||
files,
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue