Skip to content

Commit 40770ef

Browse files
committed
app option to control if upload is enabled
1 parent c97b218 commit 40770ef

File tree

11 files changed

+138
-58
lines changed

11 files changed

+138
-58
lines changed

README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,30 +28,33 @@ Usage: file-share [OPTIONS] [TARGET_DIR]
2828
Arguments:
2929
[TARGET_DIR]
3030
Path to the directory to share
31-
31+
3232
[default: .]
3333
3434
Options:
3535
-p, --port <PORT>
3636
Port to listen on
37-
37+
3838
[default: 3000]
3939
4040
-q, --qr
4141
Show QR codes that link to the site
4242
4343
-i, --interfaces <INTERFACES>...
4444
IP address(es) of interfaces on which file-share will be available
45-
45+
4646
Accepts comma separated list of both IPv4 and IPv6 addresses
47-
47+
4848
[default: 0.0.0.0,::]
4949
5050
-P, --picker
5151
Open a GUI file picker to choose the target directory
52-
52+
5353
Overrides `TARGET_DIR`
5454
55+
-u, --upload
56+
Allow client to upload files
57+
5558
-h, --help
5659
Print help (see a summary with '-h')
5760

app/src/components/loading.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
use leptos::prelude::*;
2+
3+
pub fn Loading() -> impl IntoView {
4+
view! {
5+
<p>"Loading..."</p>
6+
}
7+
}

app/src/components/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
mod breadcrumbs;
22
mod file_entries;
33
mod folder_download;
4+
mod loading;
45
mod new_folder;
56
mod upload;
67

78
pub use breadcrumbs::Breadcrumbs;
89
pub use file_entries::FileEntries;
910
pub use folder_download::FolderDownloads;
11+
pub use loading::Loading;
1012
pub use new_folder::NewFolderButton;
1113
pub use upload::FileUpload;

app/src/components/upload.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ pub async fn upload_file(data: MultipartData) -> Result<(), ServerFnError> {
2323
use server_fn::ServerFnError::ServerError;
2424
use tokio::{fs::OpenOptions, io::AsyncWriteExt};
2525

26+
use crate::AppConfig;
27+
2628
async fn collect_field_with_name(
2729
data: &mut multer::Multipart<'static>,
2830
name: &str,
@@ -44,14 +46,19 @@ pub async fn upload_file(data: MultipartData) -> Result<(), ServerFnError> {
4446
Ok(buffer)
4547
}
4648

49+
let app_config = expect_context::<AppConfig>();
50+
51+
if !app_config.allow_upload {
52+
return Err(ServerError("Uploads are disabled".into()));
53+
}
54+
4755
let Some(mut data) = data.into_inner() else {
4856
unreachable!("should always return Some on the server side");
4957
};
5058

5159
let base_req_path = {
52-
let base_path = expect_context::<PathBuf>().clone();
5360
let req_path = collect_field_with_name(&mut data, "path").await?;
54-
base_path.join(req_path.trim())
61+
app_config.target_dir.join(req_path.trim())
5562
};
5663

5764
let id = collect_field_with_name(&mut data, "id").await?;

app/src/config.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
use std::path::PathBuf;
2+
3+
#[derive(Debug, Clone)]
4+
pub struct AppConfig {
5+
pub target_dir: PathBuf,
6+
pub allow_upload: bool,
7+
}

app/src/lib.rs

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use std::path::PathBuf;
55

66
mod components;
7+
mod config;
78
mod error_template;
89
mod server;
910
#[cfg(feature = "ssr")]
@@ -16,6 +17,7 @@ use leptos_router::{components::*, hooks::use_params, params::*};
1617
use leptos_router_macro::path;
1718
use urlencoding::decode;
1819

20+
pub use crate::config::AppConfig;
1921
#[cfg(feature = "ssr")]
2022
pub use crate::state::AppState;
2123
use crate::{
@@ -50,15 +52,25 @@ pub fn FilesPage() -> impl IntoView {
5052

5153
let path_signal = Signal::from(path);
5254

55+
let upload_bar = move |allow_upload: bool| {
56+
allow_upload.then(|| {
57+
view! {
58+
<div class="flex flex-wrap gap-2 justify-center items-start pt-2 w-full">
59+
<FileUpload path=path_signal on_upload=move || listing.refetch() />
60+
<div class="flex gap-2 grow">
61+
<NewFolderButton path=path_signal action=create_folder_action />
62+
<FolderDownloads path=path_signal />
63+
</div>
64+
</div>
65+
}
66+
})
67+
};
68+
5369
view! {
54-
<div class="App p-3">
55-
<div class="w-full pt-2 flex flex-wrap items-start justify-center gap-2">
56-
<FileUpload path=path_signal on_upload=move || listing.refetch() />
57-
<div class="flex grow gap-2">
58-
<NewFolderButton path=path_signal action=create_folder_action />
59-
<FolderDownloads path=path_signal />
60-
</div>
61-
</div>
70+
<div class="p-3 App">
71+
<Transition fallback=Loading>
72+
{move || Suspend::new(async move { upload_allowed().await.ok().and_then(upload_bar) })}
73+
</Transition>
6274

6375
<Breadcrumbs path=path_signal />
6476

@@ -69,9 +81,7 @@ pub fn FilesPage() -> impl IntoView {
6981
<span class="hidden md:inline">Last Modified</span>
7082
</div>
7183

72-
<Transition fallback=move || {
73-
view! { <p>"Loading..."</p> }
74-
}>
84+
<Transition fallback=Loading>
7585
{move || Suspend::new(async move {
7686
match listing.await {
7787
Ok(entries) => {
@@ -104,7 +114,7 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
104114
}
105115
}
106116

107-
#[allow(non_snake_case)]
117+
#[component]
108118
pub fn App() -> impl IntoView {
109119
provide_meta_context();
110120

app/src/server.rs

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
use std::path::PathBuf;
22

3+
cfg_if! { if #[cfg(feature = "ssr")] {
4+
use leptos::logging::warn;
5+
use tokio::fs;
6+
7+
use crate::config::AppConfig;
8+
}}
9+
10+
use cfg_if::cfg_if;
311
use leptos::prelude::*;
412
use serde::{Deserialize, Serialize};
5-
#[cfg(feature = "ssr")]
6-
use tokio::fs;
713

814
use crate::utils::SystemTime;
915

16+
#[server(name = UploadAllowed, prefix = "/api", endpoint = "upload_allowed")]
17+
pub async fn upload_allowed() -> Result<bool, ServerFnError> {
18+
Ok(expect_context::<AppConfig>().allow_upload)
19+
}
20+
1021
pub type Entries = Vec<ServerEntry>;
1122

1223
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq)]
@@ -34,11 +45,12 @@ pub async fn list_dir(path: PathBuf) -> Result<Entries, ServerFnError> {
3445
));
3546
}
3647

37-
let base_path = expect_context::<PathBuf>().clone();
48+
let base_path = expect_context::<AppConfig>().target_dir;
3849

39-
let Ok(path) = base_path.join(path).canonicalize() else {
50+
let Ok(path) = base_path.join(&path).canonicalize() else {
51+
warn!("Attempt to access invalid path: {path:?}");
4052
return Err(ServerFnError::ServerError(
41-
"Path must be inside target_dir".into(),
53+
"Requested path not found".into(),
4254
));
4355
};
4456

@@ -70,9 +82,13 @@ pub async fn list_dir(path: PathBuf) -> Result<Entries, ServerFnError> {
7082

7183
#[server(name = NewFolder, prefix = "/api", endpoint = "new_folder")]
7284
pub async fn new_folder(name: String, path: PathBuf) -> Result<(), ServerFnError> {
73-
let base_path = expect_context::<PathBuf>().clone();
85+
let app_config = expect_context::<AppConfig>();
86+
87+
if !app_config.allow_upload {
88+
return Err(ServerFnError::ServerError("Uploads are disabled".into()));
89+
}
7490

75-
let path = base_path.join(path).join(name);
91+
let path = app_config.target_dir.join(path).join(name);
7692

7793
fs::create_dir(path).await?;
7894

app/src/state.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
use std::path::PathBuf;
2-
31
use axum::extract::FromRef;
42
use leptos::prelude::LeptosOptions;
53

4+
use crate::AppConfig;
5+
66
#[derive(FromRef, Clone, Debug)]
77
pub struct AppState {
8-
pub target_dir: PathBuf,
8+
pub app_config: AppConfig,
99
pub leptos_options: LeptosOptions,
1010
}

server/src/config.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,16 @@ pub struct Cli {
2929
/// Overrides `TARGET_DIR`
3030
#[arg(short = 'P', long, default_value = "false")]
3131
pub picker: bool,
32+
33+
/// Allow client to upload files
34+
#[arg(short, long, default_value = "false")]
35+
pub upload: bool,
3236
}
3337

3438
#[derive(Debug, Clone)]
3539
pub struct Config {
3640
pub target_dir: PathBuf,
41+
pub allow_upload: bool,
3742
pub port: u16,
3843
pub qr: bool,
3944
pub interfaces: Vec<IpAddr>,
@@ -64,8 +69,10 @@ pub async fn get_config() -> Result<Config, String> {
6469
IpAddr::V6(Ipv6Addr::UNSPECIFIED),
6570
IpAddr::V4(Ipv4Addr::UNSPECIFIED),
6671
];
72+
73+
let upload = true;
6774
} else {
68-
let Cli { target_dir, port, qr, interfaces, picker } = Cli::parse();
75+
let Cli { target_dir, port, qr, interfaces, picker, upload } = Cli::parse();
6976
let target_dir = if picker {
7077
rfd::AsyncFileDialog::new()
7178
.set_title("Select directory to share")
@@ -87,6 +94,7 @@ pub async fn get_config() -> Result<Config, String> {
8794

8895
Ok(Config {
8996
target_dir,
97+
allow_upload: upload,
9098
port,
9199
qr,
92100
interfaces,

server/src/fileserv.rs

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
mod archive;
22

3-
use std::{collections::HashMap, path::PathBuf};
3+
use std::{
4+
collections::HashMap,
5+
os,
6+
path::{self, PathBuf},
7+
};
48

59
pub use archive::Method;
610
use axum::{
711
body::Body,
812
extract::{Multipart, Path, Query, State},
9-
http::{header, HeaderValue, Request, StatusCode, Uri},
13+
http::{HeaderValue, Request, StatusCode, Uri, header},
1014
response::IntoResponse,
1115
};
12-
use file_share_app::{shell, utils::format_bytes, AppState};
16+
use file_share_app::{AppConfig, AppState, shell, utils::format_bytes};
1317
use leptos::{logging, prelude::provide_context};
1418
use rust_embed::RustEmbed;
1519
use tokio::io::AsyncWriteExt;
@@ -42,8 +46,7 @@ pub async fn file_and_error_handler(
4246
}
4347

4448
let handler = leptos_axum::render_app_to_stream_with_context(
45-
// app_state.leptos_options.clone(),
46-
move || provide_context(app_state.target_dir.clone()),
49+
move || provide_context(app_state.app_config.clone()),
4750
move || shell(app_state.leptos_options.clone()),
4851
);
4952
handler(request).await.into_response()
@@ -52,22 +55,22 @@ pub async fn file_and_error_handler(
5255
/// Handles archive requests.
5356
#[allow(clippy::implicit_hasher)]
5457
pub async fn handle_archive_with_path<'a>(
55-
State(base_dir): State<PathBuf>,
58+
State(AppConfig { target_dir, .. }): State<AppConfig>,
5659
Path(path): Path<String>,
5760
Query(params): Query<HashMap<String, String>>,
5861
) -> impl IntoResponse + use<'a> {
5962
logging::log!("Handling archive with path '{path:?}' and params '{params:?}'");
60-
handle_archive(base_dir, params.get("method"), path).await
63+
handle_archive(target_dir, params.get("method"), path).await
6164
}
6265

6366
/// Handles archive requests.
6467
#[allow(clippy::implicit_hasher)]
6568
pub async fn handle_archive_without_path(
66-
State(base_dir): State<PathBuf>,
69+
State(AppConfig { target_dir, .. }): State<AppConfig>,
6770
Query(params): Query<HashMap<String, String>>,
6871
) -> impl IntoResponse + use<> {
6972
logging::log!("Handling archive without path and with params '{params:?}'");
70-
handle_archive(base_dir, params.get("method"), String::new()).await
73+
handle_archive(target_dir, params.get("method"), String::new()).await
7174
}
7275

7376
#[allow(clippy::unused_async)] // has to be in an async context, but doesn't await directly
@@ -124,19 +127,32 @@ async fn handle_archive(
124127
(headers, Body::from_stream(stream)).into_response()
125128
}
126129

130+
const UPLOAD_DISABLED: (StatusCode, &'static str) =
131+
(StatusCode::FORBIDDEN, "Upload is not enabled");
132+
127133
pub async fn file_upload_with_path(
128-
State(base_dir): State<PathBuf>,
134+
State(AppState { app_config, .. }): State<AppState>,
129135
Path(path): Path<String>,
130136
multipart: Multipart,
131137
) -> impl IntoResponse {
132-
file_upload(base_dir, path, multipart).await
138+
if !app_config.allow_upload {
139+
return UPLOAD_DISABLED.into_response();
140+
}
141+
file_upload(app_config.target_dir, path, multipart)
142+
.await
143+
.into_response()
133144
}
134145

135146
pub async fn file_upload_without_path(
136-
State(base_dir): State<PathBuf>,
147+
State(AppState { app_config, .. }): State<AppState>,
137148
multipart: Multipart,
138149
) -> impl IntoResponse {
139-
file_upload(base_dir, String::new(), multipart).await
150+
if !app_config.allow_upload {
151+
return UPLOAD_DISABLED.into_response();
152+
}
153+
file_upload(app_config.target_dir, String::new(), multipart)
154+
.await
155+
.into_response()
140156
}
141157

142158
pub async fn file_upload(

0 commit comments

Comments
 (0)