From be81eaefa526fa80e04166e86978e3a95263b4e3 Mon Sep 17 00:00:00 2001 From: Alec Di Vito Date: Thu, 6 Jun 2024 18:42:20 -0400 Subject: feat: Added HTML and Javascript progress bar when uploading files --- src/args.rs | 8 ++ src/config.rs | 4 + src/errors.rs | 1 + src/file_op.rs | 18 +++-- src/renderer.rs | 243 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 261 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/args.rs b/src/args.rs index 95c8bff..a58504b 100644 --- a/src/args.rs +++ b/src/args.rs @@ -165,6 +165,14 @@ pub struct CliArgs { /// When specified via environment variable, a path always neesd to the specified. #[arg(short = 'u', long = "upload-files", value_hint = ValueHint::FilePath, num_args(0..=1), value_delimiter(','), env = "MINISERVE_ALLOWED_UPLOAD_DIR")] pub allowed_upload_dir: Option>, + + /// Configure amount of concurrent uploads when visiting the website. Must have + /// upload-files option enabled for this setting to matter. + /// + /// For example, a value of 4 would mean that the web browser will only upload + /// 4 files at a time to the web server when using the web browser interface. + #[arg(long = "web-upload-files-concurrency", env = "MINISERVE_WEB_UPLOAD_CONCURRENCY", default_value = "0")] + pub web_upload_concurrency: usize, /// Enable creating directories #[arg( diff --git a/src/config.rs b/src/config.rs index 3643502..6e8b89e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -101,6 +101,9 @@ pub struct MiniserveConfig { /// Enable file upload pub file_upload: bool, + /// Max amount of concurrency when uploading multiple files + pub web_upload_concurrency: usize, + /// List of allowed upload directories pub allowed_upload_dir: Vec, @@ -301,6 +304,7 @@ impl MiniserveConfig { show_qrcode: args.qrcode, mkdir_enabled: args.mkdir_enabled, file_upload: args.allowed_upload_dir.is_some(), + web_upload_concurrency: args.web_upload_concurrency, allowed_upload_dir, uploadable_media_type, tar_enabled: args.enable_tar, diff --git a/src/errors.rs b/src/errors.rs index 21f8f12..600834b 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -129,6 +129,7 @@ where let res = fut.await?.map_into_boxed_body(); if (res.status().is_client_error() || res.status().is_server_error()) + && res.request().path() != "/upload" && res .headers() .get(header::CONTENT_TYPE) diff --git a/src/file_op.rs b/src/file_op.rs index e22e3e9..9f5902c 100644 --- a/src/file_op.rs +++ b/src/file_op.rs @@ -4,7 +4,7 @@ use std::io::ErrorKind; use std::path::{Component, Path, PathBuf}; use actix_web::{http::header, web, HttpRequest, HttpResponse}; -use futures::TryFutureExt; +use futures::{StreamExt, TryFutureExt}; use futures::TryStreamExt; use serde::Deserialize; use tokio::fs::File; @@ -20,7 +20,7 @@ use crate::{ /// /// Returns total bytes written to file. async fn save_file( - field: actix_multipart::Field, + field: &mut actix_multipart::Field, file_path: PathBuf, overwrite_files: bool, ) -> Result { @@ -33,8 +33,8 @@ async fn save_file( RuntimeError::InsufficientPermissionsError(file_path.display().to_string()), ), Err(err) => Err(RuntimeError::IoError( - format!("Failed to create {}", file_path.display()), - err, + format!("Failed to create {}", file_path.display()), + err, )), Ok(v) => Ok(v), }?; @@ -164,7 +164,15 @@ async fn handle_multipart( } } - save_file(field, path.join(filename_path), overwrite_files).await + match save_file(&mut field, path.join(filename_path), overwrite_files).await { + Ok(bytes) => Ok(bytes), + Err(err) => { + // Required for file upload. If entire stream is not consumed, javascript + // XML HTTP Request will never complete. + while field.next().await.is_some() {} + Err(err) + }, + } } /// Query parameters used by upload and rm APIs diff --git a/src/renderer.rs b/src/renderer.rs index 3935d98..6c1f393 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -53,7 +53,7 @@ pub fn page( html! { (DOCTYPE) html { - (page_header(&title_path, conf.file_upload, &conf.favicon_route, &conf.css_route)) + (page_header(&title_path, conf.file_upload, conf.web_upload_concurrency, &conf.favicon_route, &conf.css_route)) body #drop-container { @@ -175,6 +175,37 @@ pub fn page( } } } + div.upload_area id="upload_area" { + template id="upload_file_item" { + li.upload_file_item { + div.upload_file_container { + div.upload_file_text { + span.file_upload_percent { "" } + {" - "} + span.file_size { "" } + {" - "} + span.file_name { "" } + } + button.file_cancel_upload { "✖" } + } + div.file_progress_bar {} + } + } + div.upload_container { + div.upload_header { + h4 style="margin:0px" id="upload_title" {} + } + div.upload_action { + p id="upload_action_text" { "Starting upload..." } + button.upload_cancel id="upload_cancel" { "CANCEL" } + } + div.upload_files { + ul.upload_file_list id="upload_file_list" { + + } + } + } + } } } } @@ -571,7 +602,7 @@ fn chevron_down() -> Markup { } /// Partial: page header -fn page_header(title: &str, file_upload: bool, favicon_route: &str, css_route: &str) -> Markup { +fn page_header(title: &str, file_upload: bool, web_file_concurrency: usize, favicon_route: &str, css_route: &str) -> Markup { html! { head { meta charset="utf-8"; @@ -612,9 +643,23 @@ fn page_header(title: &str, file_upload: bool, favicon_route: &str, css_route: & "#)) @if file_upload { - (PreEscaped(r#" - - "#)) + "#)) + } } } } @@ -677,7 +904,7 @@ pub fn render_error( html! { (DOCTYPE) html { - (page_header(&error_code.to_string(), false, &conf.favicon_route, &conf.css_route)) + (page_header(&error_code.to_string(), false, conf.web_upload_concurrency, &conf.favicon_route, &conf.css_route)) body { -- cgit v1.2.3