Skip to content

Commit 382709f

Browse files
committed
feat: check and transform share roots, accepting uuids
1 parent 421b473 commit 382709f

10 files changed

Lines changed: 223 additions & 41 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ Filen Relay provides a convenient way to serve your Filen Drive via WebDAV/HTTP/
1010
### On Your Machine
1111

1212
```bash
13-
docker run -e FILEN_RELAY_ADMIN_EMAIL='your-filen-account@email.com' -p 80:80 ghcr.io/FilenCloudDienste/filen-relay:main
13+
docker run -e FILEN_RELAY_ADMIN_EMAIL='your-filen-account@email.com' -p 80:80 ghcr.io/filenclouddienste/filen-relay:main
1414
```
1515

16+
<!-- todo: fix issues here -->
17+
1618
Configuration options (choose one):
1719
- Set `--admin-email` (`FILEN_RELAY_ADMIN_EMAIL`) and `--db-dir` (`FILEN_RELAY_DB_DIR`) options (or environment variables) to create a normal deployment.
1820
- Set `--admin-email` (`FILEN_RELAY_ADMIN_EMAIL`), `--admin-password` (`FILEN_RELAY_ADMIN_PASSWORD`) and `--db-dir` (`FILEN_RELAY_DB_DIR`) to create a deployment where data is stored in the admin's Filen drive. This is useful when the deployments needs to be stateless.

filen-relay/src/api.rs

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,87 @@ pub(crate) async fn logout() -> Result<Response> {
5656
.unwrap())
5757
}
5858

59+
#[derive(Serialize, Deserialize)]
60+
pub(crate) struct CheckedShareRoot {
61+
pub path: String,
62+
pub item_type: ShareRootType,
63+
}
64+
65+
#[derive(Serialize, Deserialize)]
66+
pub(crate) enum ShareRootType {
67+
Root,
68+
File,
69+
Dir,
70+
}
71+
72+
#[post("/api/checkRoot", session: auth::AuthSession)]
73+
pub(crate) async fn check_and_transform_root(
74+
root: String,
75+
) -> Result<CheckedShareRoot, anyhow::Error> {
76+
use super::backend::util::{find_path_for_dir, find_path_for_file};
77+
use filen_sdk_rs::fs::categories::NonRootFileType;
78+
use filen_types::fs::UuidStr;
79+
80+
// check if it is a uuid
81+
if let Ok(uuid) = uuid::Uuid::try_parse(&root) {
82+
dioxus::logger::tracing::info!("Checking root as UUID: {}", uuid);
83+
// try to find a dir with the uuid
84+
match session.filen_client.get_dir(UuidStr::from(&uuid)).await {
85+
Ok(dir) => {
86+
let path = find_path_for_dir(session.filen_client.as_ref(), dir).await?;
87+
Ok(CheckedShareRoot {
88+
path,
89+
item_type: ShareRootType::Dir,
90+
})
91+
}
92+
Err(e1) => {
93+
// try to find a file with the uuid
94+
match session.filen_client.get_file(UuidStr::from(&uuid)).await {
95+
Ok(file) => {
96+
let path = find_path_for_file(session.filen_client.as_ref(), file).await?;
97+
Ok(CheckedShareRoot {
98+
path,
99+
item_type: ShareRootType::File,
100+
})
101+
}
102+
Err(e2) => Err(anyhow::anyhow!(
103+
"Failed to find dir ({}), also failed to find file ({}), for provided UUID",
104+
e1,
105+
e2
106+
)),
107+
}
108+
}
109+
}
110+
} else {
111+
let root = format!("/{}", root.trim_start_matches('/').trim_end_matches('/'));
112+
// not a uuid, try to find a dir with the path
113+
match session.filen_client.find_item_at_path(&root).await {
114+
Ok(item) => match item {
115+
Some(item) => match item {
116+
NonRootFileType::Root(_) => Ok(CheckedShareRoot {
117+
path: root,
118+
item_type: ShareRootType::Root,
119+
}),
120+
NonRootFileType::File(_) => Ok(CheckedShareRoot {
121+
path: root,
122+
item_type: ShareRootType::File,
123+
}),
124+
NonRootFileType::Dir(_) => Ok(CheckedShareRoot {
125+
path: root,
126+
item_type: ShareRootType::Dir,
127+
}),
128+
}
129+
None => Err(anyhow::anyhow!("No item found at provided path")),
130+
},
131+
Err(e) => Err(anyhow::anyhow!(
132+
"Failed to find dir at path ({}), also failed to find file at path ({}), for provided path",
133+
e,
134+
e
135+
)),
136+
}
137+
}
138+
}
139+
59140
#[get("/api/shares", session: auth::AuthSession)]
60141
pub(crate) async fn get_shares() -> Result<Vec<Share>, anyhow::Error> {
61142
Ok(DB
@@ -72,7 +153,10 @@ pub(crate) async fn add_share(
72153
read_only: bool,
73154
password: Option<String>,
74155
) -> Result<(), anyhow::Error> {
75-
let root = format!("/{}", root.trim_start_matches('/'));
156+
let root = check_and_transform_root(root)
157+
.await
158+
.context("Failed to check root")?
159+
.path;
76160
DB.create_share(&Share {
77161
id: ShareId::new(),
78162
root,

filen-relay/src/backend/db.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ use filen_sdk_rs::{
88
use rusqlite::Connection;
99

1010
use crate::{
11-
common::{ShareId, Share},
12-
util::UnwrapOnceLock,
11+
backend::util::UnwrapOnceLock, common::{Share, ShareId}
1312
};
1413

1514
// todo: is it good (or safe) that this needs to be .lock().unwrap() everywhere?

filen-relay/src/backend/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ pub(crate) mod auth;
2929
pub(crate) mod db;
3030
pub(crate) mod rclone_auth_proxy;
3131
pub(crate) mod server_manager;
32+
pub(crate) mod util;
3233

3334
pub(crate) fn serve(args: Args) {
3435
dioxus::serve(move || {

filen-relay/src/backend/server_manager.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ impl ServerManager {
5050
let mut perms = script_file.as_file().metadata()?.permissions();
5151
perms.set_mode(0o755);
5252
script_file.as_file().set_permissions(perms)?;
53-
dbg!(script_file.path());
5453
// todo: can we avoid creating a platform-dependent shell script?
5554

5655
// spawn rclone process

filen-relay/src/backend/util.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
use std::{ops::Deref, sync::OnceLock};
2+
3+
use anyhow::{Context, Result};
4+
use filen_sdk_rs::{
5+
auth::Client,
6+
fs::HasUUID,
7+
io::{RemoteDirectory, RemoteFile},
8+
};
9+
use filen_types::fs::UuidStr;
10+
11+
/// A wrapper around OnceLock that panics if accessed before initialization.
12+
/// This is useful for when you know the value will be initialized and want to avoid
13+
/// explicitly calling unwrap() everywhere.
14+
pub struct UnwrapOnceLock<T>(OnceLock<T>);
15+
16+
impl<T> UnwrapOnceLock<T> {
17+
pub const fn new() -> Self {
18+
UnwrapOnceLock(OnceLock::new())
19+
}
20+
}
21+
22+
impl<T> UnwrapOnceLock<T> {
23+
pub fn init(&self, val: T) {
24+
let _ = self.0.set(val);
25+
}
26+
}
27+
28+
impl<T> Deref for UnwrapOnceLock<T> {
29+
type Target = T;
30+
31+
fn deref(&self) -> &Self::Target {
32+
self.0.get().expect("OnceLock not initialized")
33+
}
34+
}
35+
36+
pub(crate) async fn find_path_for_dir(client: &Client, dir: RemoteDirectory) -> Result<String> {
37+
dbg!("Finding path for dir", dir.uuid());
38+
dbg!("Root dir", client.root().uuid());
39+
dbg!("Parent uuid", dir.parent);
40+
if dir.uuid() == client.root().uuid() {
41+
return Ok(String::new());
42+
}
43+
let parent_uuid = UuidStr::try_from(dir.parent)
44+
.context("Failed to get parent UUID for directory not inside regular file tree")?;
45+
let parent_path = if &parent_uuid == client.root().uuid() {
46+
String::new()
47+
} else {
48+
let parent = client.get_dir(parent_uuid).await.with_context(|| {
49+
format!("Failed to find parent {parent_uuid} while traversing path")
50+
})?;
51+
Box::pin(find_path_for_dir(client, parent)).await?
52+
};
53+
Ok(format!(
54+
"{}/{}",
55+
parent_path,
56+
dir.meta.name().unwrap_or("INVALID_NAME")
57+
))
58+
}
59+
60+
pub(crate) async fn find_path_for_file(client: &Client, file: RemoteFile) -> Result<String> {
61+
let parent_uuid = UuidStr::try_from(file.parent)
62+
.context("Failed to get parent UUID for file not inside regular file tree")?;
63+
let parent = client
64+
.get_dir(parent_uuid)
65+
.await
66+
.with_context(|| format!("Failed to find parent {parent_uuid} while traversing path"))?;
67+
let parent_path = find_path_for_dir(client, parent).await?;
68+
Ok(format!(
69+
"{}/{}",
70+
parent_path,
71+
file.meta.name().unwrap_or("INVALID_NAME")
72+
))
73+
}

filen-relay/src/frontend/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,11 @@ fn Login() -> Element {
310310
checked: if *save_credentials.read() { CheckboxState::Checked } else { CheckboxState::Unchecked },
311311
on_checked_change: move |new_state| save_credentials.set(new_state == CheckboxState::Checked),
312312
}
313-
"Remember me"
313+
label {
314+
r#for: "save_credentials",
315+
class: "cursor-pointer",
316+
"Remember me"
317+
}
314318
}
315319
}
316320
}

filen-relay/src/frontend/shares.rs

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
use crate::components::{
2-
label::Label,
3-
select::{SelectGroupLabel, SelectOption, SelectTrigger},
1+
use crate::{
2+
api::ShareRootType,
3+
components::{
4+
label::Label,
5+
select::{SelectGroupLabel, SelectOption, SelectTrigger},
6+
},
47
};
58
use dioxus::prelude::*;
69
use dioxus_primitives::checkbox::CheckboxState;
@@ -95,6 +98,7 @@ pub(crate) fn ShareCard(share: Share, open_as: ServerType, on_remove: EventHandl
9598
target: "_blank",
9699
"{share.root}"
97100
img { src: open_external_icon, style: "color: #ffffff" }
101+
// todo: display copy icon instead when server type is not web
98102
}
99103
div { class: "flex-1" }
100104
div { class: "flex items-center gap-2",
@@ -160,11 +164,16 @@ pub(crate) fn CreateShareCard(on_create: EventHandler<()>) -> Element {
160164
},
161165
div { class: "flex items-center gap-2 justify-between flex-wrap",
162166
div { class: "flex items-center gap-4 flex-wrap",
163-
Input {
164-
id: "root",
165-
placeholder: "/path/to/share",
166-
value: "{root}",
167-
oninput: move |e: Event<FormData>| root.set(e.value().clone()),
167+
div { class: "flex flex-col gap-1",
168+
Input {
169+
id: "root",
170+
placeholder: "/path/to/share or ID",
171+
value: "{root}",
172+
oninput: move |e: Event<FormData>| root.set(e.value().clone()),
173+
}
174+
if root.len() > 0 {
175+
ShareRootChecker { root }
176+
}
168177
}
169178
div { class: "flex gap-2 items-center",
170179
Checkbox {
@@ -201,3 +210,41 @@ pub(crate) fn CreateShareCard(on_create: EventHandler<()>) -> Element {
201210
}
202211
}
203212
}
213+
214+
#[component]
215+
fn ShareRootChecker(root: ReadSignal<String>) -> Element {
216+
let mut checked_root = use_action(move |root: String| async move {
217+
match crate::api::check_and_transform_root(root).await {
218+
Ok(checked_root) => Ok(checked_root),
219+
Err(e) => {
220+
dioxus::logger::tracing::error!("Failed to check root: {}", e);
221+
Err(e)
222+
}
223+
}
224+
});
225+
use_effect(move || {
226+
checked_root.call(root());
227+
});
228+
match checked_root.value() {
229+
Some(Ok(checked_root)) => {
230+
let item_type = match checked_root.read().item_type {
231+
ShareRootType::File => "file",
232+
ShareRootType::Dir => "directory",
233+
ShareRootType::Root => "root",
234+
};
235+
let path = checked_root.read().path.clone();
236+
rsx! {
237+
div { class: "text-green-400 text-sm flex gap-1",
238+
"Sharing "
239+
span { class: "font-semibold", "{item_type}" }
240+
" at "
241+
span { class: "font-semibold", "{path}" }
242+
}
243+
}
244+
}
245+
Some(Err(_)) => rsx! {
246+
span { class: "text-red-400 text-sm", "Must be a valid path or ID" }
247+
},
248+
None => rsx! {},
249+
}
250+
}

filen-relay/src/main.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ mod backend;
44
mod common;
55
mod components;
66
mod frontend;
7-
mod util;
87

98
#[cfg(feature = "server")]
109
#[derive(clap::Parser, Clone)]

filen-relay/src/util.rs

Lines changed: 0 additions & 26 deletions
This file was deleted.

0 commit comments

Comments
 (0)