From 66c1c10d39e6ecb212ec4709888493693339c07d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Pej=C5=A1a?= Date: Sun, 24 Mar 2019 10:25:48 +0100 Subject: Implement file upload. --- src/file_upload.rs | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/listing.rs | 1 + src/main.rs | 7 ++++ src/renderer.rs | 8 ++++- 4 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 src/file_upload.rs (limited to 'src') diff --git a/src/file_upload.rs b/src/file_upload.rs new file mode 100644 index 0000000..00cca17 --- /dev/null +++ b/src/file_upload.rs @@ -0,0 +1,100 @@ +use actix_web::{ + dev, error, + http::header::{ContentDisposition, LOCATION}, + multipart, Error, FutureResponse, HttpMessage, HttpRequest, HttpResponse, +}; +use std::io::Write; +use std::path::{Component, PathBuf}; + +use futures::future; +use futures::{Future, Stream}; + +pub fn save_file( + field: multipart::Field, + file_path: PathBuf, +) -> Box> { + let mut file = match std::fs::File::create(file_path) { + Ok(file) => file, + Err(e) => return Box::new(future::err(error::ErrorInternalServerError(e))), + }; + Box::new( + field + .fold(0i64, move |acc, bytes| { + let rt = file + .write_all(bytes.as_ref()) + .map(|_| acc + bytes.len() as i64) + .map_err(|e| error::MultipartError::Payload(error::PayloadError::Io(e))); + future::result(rt) + }) + .map_err(|e| error::ErrorInternalServerError(e)), + ) +} + +pub fn handle_multipart( + item: multipart::MultipartItem, + mut file_path: PathBuf, +) -> Box> { + match item { + multipart::MultipartItem::Field(field) => { + let err = || Box::new(future::err(error::ContentTypeError::ParseError.into())); + let filename = field + .headers() + .get("content-disposition") + .ok_or(err()) + .and_then(|cd| ContentDisposition::from_raw(cd).map_err(|_| err())) + .and_then(|content_disposition| { + content_disposition + .get_filename() + .ok_or(err()) + .map(|cd| String::from(cd)) + }); + match filename { + Ok(f) => { + file_path = file_path.join(f); + // TODO should I allow overriding existing files? + Box::new(save_file(field, file_path).into_stream()) + } + Err(e) => Box::new(e.into_stream()), + } + } + multipart::MultipartItem::Nested(mp) => Box::new( + mp.map_err(error::ErrorInternalServerError) + .map(move |item| handle_multipart(item, file_path.clone())) + .flatten(), + ), + } +} + +pub fn upload_file(req: &HttpRequest) -> FutureResponse { + if req.query().contains_key("path") { + let path_str = req.query()["path"].clone(); + let mut path = PathBuf::from(path_str.clone()); + while path.has_root() { + path = match path.strip_prefix(Component::RootDir) { + Ok(path) => path.to_path_buf(), + //TODO better error response + Err(_) => return Box::new(future::ok(HttpResponse::BadRequest().body(""))), + } + } + // TODO verify that path is under current dir + if let Ok(target_path) = path.join(req.state().path.clone()).canonicalize() { + Box::new( + req.multipart() + .map_err(error::ErrorInternalServerError) + .map(move |item| handle_multipart(item, target_path.clone())) + .flatten() + .collect() + .map(|_| { + HttpResponse::TemporaryRedirect() + .header(LOCATION, path_str) + .finish() + }) + .map_err(|e| e), + ) + } else { + Box::new(future::ok(HttpResponse::BadRequest().body(""))) + } + } else { + Box::new(future::ok(HttpResponse::BadRequest().body(""))) + } +} diff --git a/src/listing.rs b/src/listing.rs index c4daf88..0173176 100644 --- a/src/listing.rs +++ b/src/listing.rs @@ -265,6 +265,7 @@ pub fn directory_listing( page_parent, sort_method, sort_order, + &base.to_string_lossy(), ) .into_string(), )) diff --git a/src/main.rs b/src/main.rs index f662a73..37a3226 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ #![feature(proc_macro_hygiene)] +use actix_web::http::Method; use actix_web::{fs, middleware, server, App}; use clap::crate_version; use simplelog::{Config, LevelFilter, TermLogger}; @@ -13,6 +14,7 @@ mod archive; mod args; mod auth; mod errors; +mod file_upload; mod listing; mod renderer; @@ -195,6 +197,11 @@ fn configure_app(app: App) -> App { let random_route = app.state().random_route.clone().unwrap_or_default(); let full_route = format!("/{}", random_route); + //allow file upload + let app = app.resource("/upload", |r| { + r.method(Method::POST).f(file_upload::upload_file) + }); + if let Some(s) = s { // Handle directories app.handler(&full_route, s) diff --git a/src/renderer.rs b/src/renderer.rs index 66fc714..3039dc7 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -14,12 +14,18 @@ pub fn page( page_parent: Option, sort_method: Option, sort_order: Option, + base: &str, ) -> Markup { html! { (page_header(page_title)) body { span #top { } - h1.title { (page_title) } + h1 { (page_title) } + form action={"/upload?path=" (base)} method="POST" enctype="multipart/form-data" { + p { "Select file to upload" } + input type="file" name="file_to_upload" {} + input type="submit" value="Upload file" {} + } div.download { (archive_button(archive::CompressionMethod::TarGz)) } -- cgit v1.2.3 From f4e22946d01b3eeaec5723faa28c3ce4f6d834c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Pej=C5=A1a?= Date: Sun, 24 Mar 2019 19:57:37 +0100 Subject: Check if file path is under app root dir. --- src/file_upload.rs | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/file_upload.rs b/src/file_upload.rs index 00cca17..442b47c 100644 --- a/src/file_upload.rs +++ b/src/file_upload.rs @@ -69,15 +69,28 @@ pub fn upload_file(req: &HttpRequest) -> FutureResponse< if req.query().contains_key("path") { let path_str = req.query()["path"].clone(); let mut path = PathBuf::from(path_str.clone()); - while path.has_root() { + // serever root path should be valid + let app_root_dir = req.state().path.clone().canonicalize().unwrap(); + // allow file upload only under current dir + if path.has_root() { path = match path.strip_prefix(Component::RootDir) { - Ok(path) => path.to_path_buf(), - //TODO better error response - Err(_) => return Box::new(future::ok(HttpResponse::BadRequest().body(""))), + Ok(dir) => dir.to_path_buf(), + Err(_) => { + return Box::new(future::ok(HttpResponse::BadRequest().body("Invalid path"))) + } } } - // TODO verify that path is under current dir - if let Ok(target_path) = path.join(req.state().path.clone()).canonicalize() { + let target_dir = match app_root_dir.clone().join(path).canonicalize() { + Ok(path) => { + if path.starts_with(&app_root_dir) { + path + } else { + return Box::new(future::ok(HttpResponse::BadRequest().body("Invalid path"))); + } + } + Err(_) => return Box::new(future::ok(HttpResponse::BadRequest().body("Invalid path"))), + }; + if let Ok(target_path) = target_dir.canonicalize() { Box::new( req.multipart() .map_err(error::ErrorInternalServerError) @@ -92,7 +105,7 @@ pub fn upload_file(req: &HttpRequest) -> FutureResponse< .map_err(|e| e), ) } else { - Box::new(future::ok(HttpResponse::BadRequest().body(""))) + Box::new(future::ok(HttpResponse::BadRequest().body("invalid path"))) } } else { Box::new(future::ok(HttpResponse::BadRequest().body(""))) -- cgit v1.2.3 From ea0323de8c90bf96ff46a84db069e8b81f994759 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Pej=C5=A1a?= Date: Mon, 25 Mar 2019 14:23:01 +0100 Subject: Refactoring --- src/file_upload.rs | 84 +++++++++++++++++++++++------------------------------- 1 file changed, 36 insertions(+), 48 deletions(-) (limited to 'src') diff --git a/src/file_upload.rs b/src/file_upload.rs index 442b47c..f72ab8b 100644 --- a/src/file_upload.rs +++ b/src/file_upload.rs @@ -3,11 +3,11 @@ use actix_web::{ http::header::{ContentDisposition, LOCATION}, multipart, Error, FutureResponse, HttpMessage, HttpRequest, HttpResponse, }; -use std::io::Write; -use std::path::{Component, PathBuf}; - -use futures::future; -use futures::{Future, Stream}; +use futures::{future, Future, Stream}; +use std::{ + io::Write, + path::{Component, PathBuf}, +}; pub fn save_file( field: multipart::Field, @@ -66,48 +66,36 @@ pub fn handle_multipart( } pub fn upload_file(req: &HttpRequest) -> FutureResponse { - if req.query().contains_key("path") { - let path_str = req.query()["path"].clone(); - let mut path = PathBuf::from(path_str.clone()); - // serever root path should be valid - let app_root_dir = req.state().path.clone().canonicalize().unwrap(); - // allow file upload only under current dir - if path.has_root() { - path = match path.strip_prefix(Component::RootDir) { - Ok(dir) => dir.to_path_buf(), - Err(_) => { - return Box::new(future::ok(HttpResponse::BadRequest().body("Invalid path"))) - } - } - } - let target_dir = match app_root_dir.clone().join(path).canonicalize() { - Ok(path) => { - if path.starts_with(&app_root_dir) { - path - } else { - return Box::new(future::ok(HttpResponse::BadRequest().body("Invalid path"))); - } - } - Err(_) => return Box::new(future::ok(HttpResponse::BadRequest().body("Invalid path"))), - }; - if let Ok(target_path) = target_dir.canonicalize() { - Box::new( - req.multipart() - .map_err(error::ErrorInternalServerError) - .map(move |item| handle_multipart(item, target_path.clone())) - .flatten() - .collect() - .map(|_| { - HttpResponse::TemporaryRedirect() - .header(LOCATION, path_str) - .finish() - }) - .map_err(|e| e), - ) - } else { - Box::new(future::ok(HttpResponse::BadRequest().body("invalid path"))) - } - } else { - Box::new(future::ok(HttpResponse::BadRequest().body(""))) + if !req.query().contains_key("path") { + return Box::new(future::ok( + HttpResponse::BadRequest().body("Unspecified parameter path"), + )); } + // server root path should be valid so we can unwrap it + let app_root_dir = req.state().path.clone().canonicalize().unwrap(); + + let path_str = req.query()["path"].clone(); + let mut path = PathBuf::from(path_str.clone()); + if let Ok(stripped_path) = path.strip_prefix(Component::RootDir) { + path = stripped_path.to_owned(); + } + + // if target path is under app root directory save file + let target_dir = match &app_root_dir.clone().join(path).canonicalize() { + Ok(path) if path.starts_with(&app_root_dir) => path.clone(), + _ => return Box::new(future::ok(HttpResponse::BadRequest().body("Invalid path"))), + }; + Box::new( + req.multipart() + .map_err(error::ErrorInternalServerError) + .map(move |item| handle_multipart(item, target_dir.clone())) + .flatten() + .collect() + .map(|_| { + HttpResponse::TemporaryRedirect() + .header(LOCATION, path_str) + .finish() + }) + .map_err(|e| e), + ) } -- cgit v1.2.3 From 1e8783e23c3c5e64acc40464329b64e3de6e2a20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Pej=C5=A1a?= Date: Mon, 25 Mar 2019 14:44:45 +0100 Subject: Document file upload. --- src/file_upload.rs | 11 +++++++++-- src/main.rs | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/file_upload.rs b/src/file_upload.rs index f72ab8b..c0ec95f 100644 --- a/src/file_upload.rs +++ b/src/file_upload.rs @@ -9,7 +9,8 @@ use std::{ path::{Component, PathBuf}, }; -pub fn save_file( +/// Create future to save file. +fn save_file( field: multipart::Field, file_path: PathBuf, ) -> Box> { @@ -30,7 +31,8 @@ pub fn save_file( ) } -pub fn handle_multipart( +/// Create new future to handle file as multipart data. +fn handle_multipart( item: multipart::MultipartItem, mut file_path: PathBuf, ) -> Box> { @@ -65,6 +67,11 @@ pub fn handle_multipart( } } +/// Handle incoming request to upload file. +/// Target file path is expected as path parameter in URI and is interpreted as relative from +/// server root directory. Any path which will go outside of this directory is considered +/// invalid. +/// This method returns future. pub fn upload_file(req: &HttpRequest) -> FutureResponse { if !req.query().contains_key("path") { return Box::new(future::ok( diff --git a/src/main.rs b/src/main.rs index 37a3226..fb7e321 100644 --- a/src/main.rs +++ b/src/main.rs @@ -197,7 +197,7 @@ fn configure_app(app: App) -> App { let random_route = app.state().random_route.clone().unwrap_or_default(); let full_route = format!("/{}", random_route); - //allow file upload + // Allow file upload let app = app.resource("/upload", |r| { r.method(Method::POST).f(file_upload::upload_file) }); -- cgit v1.2.3 From c6e5fef650e6b9e286f9d918e3cb730372d78892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Pej=C5=A1a?= Date: Tue, 26 Mar 2019 21:38:57 +0100 Subject: Use proper typed query param. --- src/file_upload.rs | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/src/file_upload.rs b/src/file_upload.rs index c0ec95f..dd4c962 100644 --- a/src/file_upload.rs +++ b/src/file_upload.rs @@ -1,14 +1,21 @@ use actix_web::{ dev, error, http::header::{ContentDisposition, LOCATION}, - multipart, Error, FutureResponse, HttpMessage, HttpRequest, HttpResponse, + multipart, Error, FromRequest, FutureResponse, HttpMessage, HttpRequest, HttpResponse, Query, }; use futures::{future, Future, Stream}; +use serde::Deserialize; use std::{ io::Write, path::{Component, PathBuf}, }; +/// Query parameters +#[derive(Debug, Deserialize)] +struct QueryParameters { + path: PathBuf, +} + /// Create future to save file. fn save_file( field: multipart::Field, @@ -73,22 +80,24 @@ fn handle_multipart( /// invalid. /// This method returns future. pub fn upload_file(req: &HttpRequest) -> FutureResponse { - if !req.query().contains_key("path") { - return Box::new(future::ok( - HttpResponse::BadRequest().body("Unspecified parameter path"), - )); - } - // server root path should be valid so we can unwrap it let app_root_dir = req.state().path.clone().canonicalize().unwrap(); - - let path_str = req.query()["path"].clone(); - let mut path = PathBuf::from(path_str.clone()); - if let Ok(stripped_path) = path.strip_prefix(Component::RootDir) { - path = stripped_path.to_owned(); - } + let path = match Query::::extract(req) { + Ok(query) => { + if let Ok(stripped_path) = query.path.strip_prefix(Component::RootDir) { + stripped_path.to_owned() + } else { + query.path.clone() + } + } + Err(_) => { + return Box::new(future::ok( + HttpResponse::BadRequest().body("Unspecified parameter path"), + )) + } + }; // if target path is under app root directory save file - let target_dir = match &app_root_dir.clone().join(path).canonicalize() { + let target_dir = match &app_root_dir.clone().join(path.clone()).canonicalize() { Ok(path) if path.starts_with(&app_root_dir) => path.clone(), _ => return Box::new(future::ok(HttpResponse::BadRequest().body("Invalid path"))), }; @@ -98,9 +107,9 @@ pub fn upload_file(req: &HttpRequest) -> FutureResponse< .map(move |item| handle_multipart(item, target_dir.clone())) .flatten() .collect() - .map(|_| { + .map(move |_| { HttpResponse::TemporaryRedirect() - .header(LOCATION, path_str) + .header(LOCATION, format!("{}", path.display())) .finish() }) .map_err(|e| e), -- cgit v1.2.3 From d14e17d94964291fda976423c1fe1a772d5af60b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Pej=C5=A1a?= Date: Wed, 27 Mar 2019 09:44:29 +0100 Subject: Add CLI arguments for file uploading. --- src/args.rs | 10 ++++++++++ src/file_upload.rs | 13 +++++++++---- src/listing.rs | 2 ++ src/main.rs | 25 ++++++++++++++++++++----- src/renderer.rs | 3 +++ 5 files changed, 44 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/args.rs b/src/args.rs index 4f0dbf7..7fa5121 100644 --- a/src/args.rs +++ b/src/args.rs @@ -47,6 +47,14 @@ struct CLIArgs { /// Do not follow symbolic links #[structopt(short = "P", long = "no-symlinks")] no_symlinks: bool, + + /// Enable file uploading + #[structopt(short = "u", long = "upload-files")] + file_upload: bool, + + /// Enable overriding existing files during file upload + #[structopt(short = "o", long = "owerride-files")] + override_files: bool, } /// Checks wether an interface is valid, i.e. it can be parsed into an IP address @@ -100,5 +108,7 @@ pub fn parse_args() -> crate::MiniserveConfig { path_explicitly_chosen, no_symlinks: args.no_symlinks, random_route, + override_files: args.override_files, + file_upload: args.file_upload, } } diff --git a/src/file_upload.rs b/src/file_upload.rs index dd4c962..9f87724 100644 --- a/src/file_upload.rs +++ b/src/file_upload.rs @@ -20,7 +20,11 @@ struct QueryParameters { fn save_file( field: multipart::Field, file_path: PathBuf, + override_files: bool, ) -> Box> { + if !override_files && file_path.exists() { + return Box::new(future::err(error::ErrorInternalServerError("file exists"))); + } let mut file = match std::fs::File::create(file_path) { Ok(file) => file, Err(e) => return Box::new(future::err(error::ErrorInternalServerError(e))), @@ -42,6 +46,7 @@ fn save_file( fn handle_multipart( item: multipart::MultipartItem, mut file_path: PathBuf, + override_files: bool, ) -> Box> { match item { multipart::MultipartItem::Field(field) => { @@ -60,15 +65,14 @@ fn handle_multipart( match filename { Ok(f) => { file_path = file_path.join(f); - // TODO should I allow overriding existing files? - Box::new(save_file(field, file_path).into_stream()) + Box::new(save_file(field, file_path, override_files).into_stream()) } Err(e) => Box::new(e.into_stream()), } } multipart::MultipartItem::Nested(mp) => Box::new( mp.map_err(error::ErrorInternalServerError) - .map(move |item| handle_multipart(item, file_path.clone())) + .map(move |item| handle_multipart(item, file_path.clone(), override_files)) .flatten(), ), } @@ -101,10 +105,11 @@ pub fn upload_file(req: &HttpRequest) -> FutureResponse< Ok(path) if path.starts_with(&app_root_dir) => path.clone(), _ => return Box::new(future::ok(HttpResponse::BadRequest().body("Invalid path"))), }; + let override_files = req.state().override_files; Box::new( req.multipart() .map_err(error::ErrorInternalServerError) - .map(move |item| handle_multipart(item, target_dir.clone())) + .map(move |item| handle_multipart(item, target_dir.clone(), override_files)) .flatten() .collect() .map(move |_| { diff --git a/src/listing.rs b/src/listing.rs index 0173176..5fde879 100644 --- a/src/listing.rs +++ b/src/listing.rs @@ -130,6 +130,7 @@ pub fn directory_listing( dir: &fs::Directory, req: &HttpRequest, skip_symlinks: bool, + file_upload: bool, random_route: Option, ) -> Result { let title = format!("Index of {}", req.path()); @@ -265,6 +266,7 @@ pub fn directory_listing( page_parent, sort_method, sort_order, + file_upload, &base.to_string_lossy(), ) .into_string(), diff --git a/src/main.rs b/src/main.rs index fb7e321..f7d0b1b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -44,6 +44,12 @@ pub struct MiniserveConfig { /// Enable random route generation pub random_route: Option, + + /// Enable file upload + pub file_upload: bool, + + /// Enable upload to override existing files + pub override_files: bool, } fn main() { @@ -175,11 +181,12 @@ fn main() { } /// Configures the Actix application -fn configure_app(app: App) -> App { +fn configure_app(mut app: App) -> App { let s = { let path = &app.state().path; let no_symlinks = app.state().no_symlinks; let random_route = app.state().random_route.clone(); + let file_upload = app.state().file_upload.clone(); if path.is_file() { None } else { @@ -188,7 +195,13 @@ fn configure_app(app: App) -> App { .expect("Couldn't create path") .show_files_listing() .files_listing_renderer(move |dir, req| { - listing::directory_listing(dir, req, no_symlinks, random_route.clone()) + listing::directory_listing( + dir, + req, + no_symlinks, + file_upload, + random_route.clone(), + ) }), ) } @@ -198,9 +211,11 @@ fn configure_app(app: App) -> App { let full_route = format!("/{}", random_route); // Allow file upload - let app = app.resource("/upload", |r| { - r.method(Method::POST).f(file_upload::upload_file) - }); + if app.state().file_upload { + app = app.resource("/upload", |r| { + r.method(Method::POST).f(file_upload::upload_file) + }); + } if let Some(s) = s { // Handle directories diff --git a/src/renderer.rs b/src/renderer.rs index 3039dc7..6b3fb9f 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -14,6 +14,7 @@ pub fn page( page_parent: Option, sort_method: Option, sort_order: Option, + file_upload: bool, base: &str, ) -> Markup { html! { @@ -21,11 +22,13 @@ pub fn page( body { span #top { } h1 { (page_title) } + @if file_upload { form action={"/upload?path=" (base)} method="POST" enctype="multipart/form-data" { p { "Select file to upload" } input type="file" name="file_to_upload" {} input type="submit" value="Upload file" {} } + } div.download { (archive_button(archive::CompressionMethod::TarGz)) } -- cgit v1.2.3 From 84b5852aad17961dfa2cb6ea3351b9fa3244fe6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Pej=C5=A1a?= Date: Thu, 28 Mar 2019 11:29:56 +0100 Subject: Fix file upload when used with random route. --- src/file_upload.rs | 6 ++++-- src/listing.rs | 8 +++++++- src/main.rs | 9 ++++++++- src/renderer.rs | 5 +++-- 4 files changed, 22 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/file_upload.rs b/src/file_upload.rs index 9f87724..98e3680 100644 --- a/src/file_upload.rs +++ b/src/file_upload.rs @@ -1,6 +1,6 @@ use actix_web::{ dev, error, - http::header::{ContentDisposition, LOCATION}, + http::header::{ContentDisposition, LOCATION, REFERER}, multipart, Error, FromRequest, FutureResponse, HttpMessage, HttpRequest, HttpResponse, Query, }; use futures::{future, Future, Stream}; @@ -99,6 +99,7 @@ pub fn upload_file(req: &HttpRequest) -> FutureResponse< )) } }; + let return_path = req.headers()[REFERER].clone(); // if target path is under app root directory save file let target_dir = match &app_root_dir.clone().join(path.clone()).canonicalize() { @@ -112,9 +113,10 @@ pub fn upload_file(req: &HttpRequest) -> FutureResponse< .map(move |item| handle_multipart(item, target_dir.clone(), override_files)) .flatten() .collect() + //.map(|s| HttpResponse::Ok().json(s)) .map(move |_| { HttpResponse::TemporaryRedirect() - .header(LOCATION, format!("{}", path.display())) + .header(LOCATION, format!("{}", return_path.to_str().unwrap_or("/"))) .finish() }) .map_err(|e| e), diff --git a/src/listing.rs b/src/listing.rs index 5fde879..4a0927b 100644 --- a/src/listing.rs +++ b/src/listing.rs @@ -132,12 +132,17 @@ pub fn directory_listing( skip_symlinks: bool, file_upload: bool, random_route: Option, + upload_route: String, ) -> Result { let title = format!("Index of {}", req.path()); let base = Path::new(req.path()); let random_route = format!("/{}", random_route.unwrap_or_default()); let is_root = base.parent().is_none() || req.path() == random_route; let page_parent = base.parent().map(|p| p.display().to_string()); + let current_dir = match base.strip_prefix(random_route) { + Ok(c_d) => Path::new("/").join(c_d), + Err(_) => base.to_path_buf(), + }; let (sort_method, sort_order, download) = if let Ok(query) = Query::::extract(req) { @@ -267,7 +272,8 @@ pub fn directory_listing( sort_method, sort_order, file_upload, - &base.to_string_lossy(), + &upload_route, + ¤t_dir.display().to_string(), ) .into_string(), )) diff --git a/src/main.rs b/src/main.rs index f7d0b1b..7ae06bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -182,14 +182,20 @@ fn main() { /// Configures the Actix application fn configure_app(mut app: App) -> App { + let upload_route; let s = { let path = &app.state().path; let no_symlinks = app.state().no_symlinks; let random_route = app.state().random_route.clone(); let file_upload = app.state().file_upload.clone(); + upload_route = match app.state().random_route.clone() { + Some(random_route) => format!("/{}/upload", random_route), + None => format!("/upload"), + }; if path.is_file() { None } else { + let u_r = upload_route.clone(); Some( fs::StaticFiles::new(path) .expect("Couldn't create path") @@ -201,6 +207,7 @@ fn configure_app(mut app: App) -> App { no_symlinks, file_upload, random_route.clone(), + u_r.clone(), ) }), ) @@ -212,7 +219,7 @@ fn configure_app(mut app: App) -> App { // Allow file upload if app.state().file_upload { - app = app.resource("/upload", |r| { + app = app.resource(&upload_route, |r| { r.method(Method::POST).f(file_upload::upload_file) }); } diff --git a/src/renderer.rs b/src/renderer.rs index 6b3fb9f..0a9f42b 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -15,7 +15,8 @@ pub fn page( sort_method: Option, sort_order: Option, file_upload: bool, - base: &str, + upload_route: &str, + current_dir: &str, ) -> Markup { html! { (page_header(page_title)) @@ -23,7 +24,7 @@ pub fn page( span #top { } h1 { (page_title) } @if file_upload { - form action={"/upload?path=" (base)} method="POST" enctype="multipart/form-data" { + form action={(upload_route) "?path=" (current_dir)} method="POST" enctype="multipart/form-data" { p { "Select file to upload" } input type="file" name="file_to_upload" {} input type="submit" value="Upload file" {} -- cgit v1.2.3 From e2ad04f4139d0f0aceb02c43b6913d17d11087ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Pej=C5=A1a?= Date: Thu, 28 Mar 2019 18:32:15 +0100 Subject: Better error handling for file upload. --- src/errors.rs | 17 +++++++++++++++++ src/file_upload.rs | 55 ++++++++++++++++++++++++++++++++---------------------- src/renderer.rs | 11 +++++++++++ 3 files changed, 61 insertions(+), 22 deletions(-) (limited to 'src') diff --git a/src/errors.rs b/src/errors.rs index 2aa5f58..f42cc02 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,6 +1,23 @@ use failure::{Backtrace, Context, Fail}; use std::fmt::{self, Debug, Display}; +/// Kinds of errors which might happen during file upload +#[derive(Debug, Fail)] +pub enum FileUploadErrorKind { + /// This error will occur when file overriding is off and file with same name already exists + #[fail(display = "File with this name already exists")] + FileExist, + /// This error will occur when server will fail to preccess http header during file upload + #[fail(display = "Failed to parse incoming request")] + ParseError, + /// This error will occur when we fail to precess multipart request + #[fail(display = "Failed to process multipart request")] + MultipartError(actix_web::error::MultipartError), + /// This error may occur when trying to write incoming file to disk + #[fail(display = "Failed to create or write to file")] + IOError(std::io::Error), +} + /// Kinds of errors which might happen during the generation of an archive #[derive(Debug, Fail)] pub enum CompressionErrorKind { diff --git a/src/file_upload.rs b/src/file_upload.rs index 98e3680..02478c6 100644 --- a/src/file_upload.rs +++ b/src/file_upload.rs @@ -1,7 +1,8 @@ +use crate::errors::FileUploadErrorKind; +use crate::renderer::file_upload_error; use actix_web::{ - dev, error, - http::header::{ContentDisposition, LOCATION, REFERER}, - multipart, Error, FromRequest, FutureResponse, HttpMessage, HttpRequest, HttpResponse, Query, + dev, http::header, multipart, FromRequest, FutureResponse, HttpMessage, HttpRequest, + HttpResponse, Query, }; use futures::{future, Future, Stream}; use serde::Deserialize; @@ -21,24 +22,26 @@ fn save_file( field: multipart::Field, file_path: PathBuf, override_files: bool, -) -> Box> { +) -> Box> { if !override_files && file_path.exists() { - return Box::new(future::err(error::ErrorInternalServerError("file exists"))); + return Box::new(future::err(FileUploadErrorKind::FileExist)); } let mut file = match std::fs::File::create(file_path) { Ok(file) => file, - Err(e) => return Box::new(future::err(error::ErrorInternalServerError(e))), + Err(e) => { + return Box::new(future::err(FileUploadErrorKind::IOError(e))); + } }; Box::new( field + .map_err(|e| FileUploadErrorKind::MultipartError(e)) .fold(0i64, move |acc, bytes| { let rt = file .write_all(bytes.as_ref()) .map(|_| acc + bytes.len() as i64) - .map_err(|e| error::MultipartError::Payload(error::PayloadError::Io(e))); + .map_err(|e| FileUploadErrorKind::IOError(e)); future::result(rt) - }) - .map_err(|e| error::ErrorInternalServerError(e)), + }), ) } @@ -47,19 +50,21 @@ fn handle_multipart( item: multipart::MultipartItem, mut file_path: PathBuf, override_files: bool, -) -> Box> { +) -> Box> { match item { multipart::MultipartItem::Field(field) => { - let err = || Box::new(future::err(error::ContentTypeError::ParseError.into())); let filename = field .headers() - .get("content-disposition") - .ok_or(err()) - .and_then(|cd| ContentDisposition::from_raw(cd).map_err(|_| err())) + .get(header::CONTENT_DISPOSITION) + .ok_or(FileUploadErrorKind::ParseError) + .and_then(|cd| { + header::ContentDisposition::from_raw(cd) + .map_err(|_| FileUploadErrorKind::ParseError) + }) .and_then(|content_disposition| { content_disposition .get_filename() - .ok_or(err()) + .ok_or(FileUploadErrorKind::ParseError) .map(|cd| String::from(cd)) }); match filename { @@ -67,11 +72,11 @@ fn handle_multipart( file_path = file_path.join(f); Box::new(save_file(field, file_path, override_files).into_stream()) } - Err(e) => Box::new(e.into_stream()), + Err(e) => Box::new(future::err(e).into_stream()), } } multipart::MultipartItem::Nested(mp) => Box::new( - mp.map_err(error::ErrorInternalServerError) + mp.map_err(|e| FileUploadErrorKind::MultipartError(e)) .map(move |item| handle_multipart(item, file_path.clone(), override_files)) .flatten(), ), @@ -99,7 +104,9 @@ pub fn upload_file(req: &HttpRequest) -> FutureResponse< )) } }; - let return_path = req.headers()[REFERER].clone(); + // this is really ugly I will try to think about something smarter + let return_path: String = req.headers()[header::REFERER].clone().to_str().unwrap_or("/").to_owned(); + let r_p2 = return_path.clone(); // if target path is under app root directory save file let target_dir = match &app_root_dir.clone().join(path.clone()).canonicalize() { @@ -109,16 +116,20 @@ pub fn upload_file(req: &HttpRequest) -> FutureResponse< let override_files = req.state().override_files; Box::new( req.multipart() - .map_err(error::ErrorInternalServerError) + .map_err(|e| FileUploadErrorKind::MultipartError(e)) .map(move |item| handle_multipart(item, target_dir.clone(), override_files)) .flatten() .collect() - //.map(|s| HttpResponse::Ok().json(s)) .map(move |_| { HttpResponse::TemporaryRedirect() - .header(LOCATION, format!("{}", return_path.to_str().unwrap_or("/"))) + .header( + header::LOCATION, + format!("{}", return_path.clone()), + ) .finish() }) - .map_err(|e| e), + .or_else(move |e| { + let error_description = format!("{}",e); + future::ok(HttpResponse::BadRequest().body(file_upload_error(&error_description, &r_p2.clone()).into_string())) ) } diff --git a/src/renderer.rs b/src/renderer.rs index 0a9f42b..75d5c56 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -390,3 +390,14 @@ fn humanize_systemtime(src_time: Option) -> Option { .and_then(|from_now| Duration::from_std(from_now).ok()) .map(|duration| HumanTime::from(duration).to_text_en(Accuracy::Rough, Tense::Past)) } + +/// Renders error page when file uploading fails +pub fn file_upload_error(error_description: &str, return_address: &str) -> Markup { + html! { + h1 { "File uploading failed" } + p { (error_description) } + a href=(return_address) { + "back" + } + } +} -- cgit v1.2.3 From 975fb16e5bde3d474bbadc99e2846b1940e6b954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Pej=C5=A1a?= Date: Thu, 28 Mar 2019 18:33:07 +0100 Subject: Improve how file upload handler is added to server. --- src/main.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/main.rs b/src/main.rs index 7ae06bd..935d619 100644 --- a/src/main.rs +++ b/src/main.rs @@ -181,7 +181,7 @@ fn main() { } /// Configures the Actix application -fn configure_app(mut app: App) -> App { +fn configure_app(app: App) -> App { let upload_route; let s = { let path = &app.state().path; @@ -217,16 +217,18 @@ fn configure_app(mut app: App) -> App { let random_route = app.state().random_route.clone().unwrap_or_default(); let full_route = format!("/{}", random_route); - // Allow file upload - if app.state().file_upload { - app = app.resource(&upload_route, |r| { - r.method(Method::POST).f(file_upload::upload_file) - }); - } - if let Some(s) = s { - // Handle directories - app.handler(&full_route, s) + if app.state().file_upload { + // Allow file upload + app.resource(&upload_route, |r| { + r.method(Method::POST).f(file_upload::upload_file) + }) + // Handle directories + .handler(&full_route, s) + } else { + // Handle directories + app.handler(&full_route, s) + } } else { // Handle single files app.resource(&full_route, |r| r.f(listing::file_handler)) -- cgit v1.2.3 From a13b26c1c90707056e5d4e36d67563fc91467871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Pej=C5=A1a?= Date: Thu, 28 Mar 2019 18:49:17 +0100 Subject: Check if we have permissions to create files. --- src/errors.rs | 3 +++ src/file_upload.rs | 14 +++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/errors.rs b/src/errors.rs index f42cc02..1eaa7c7 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -16,6 +16,9 @@ pub enum FileUploadErrorKind { /// This error may occur when trying to write incoming file to disk #[fail(display = "Failed to create or write to file")] IOError(std::io::Error), + /// This error will occur when we he have insuffictent permissions to create new file + #[fail(display = "Insuffitient permissions to create file")] + InsufficientPermissions, } /// Kinds of errors which might happen during the generation of an archive diff --git a/src/file_upload.rs b/src/file_upload.rs index 02478c6..bc4efb1 100644 --- a/src/file_upload.rs +++ b/src/file_upload.rs @@ -7,6 +7,7 @@ use actix_web::{ use futures::{future, Future, Stream}; use serde::Deserialize; use std::{ + fs, io::Write, path::{Component, PathBuf}, }; @@ -67,12 +68,23 @@ fn handle_multipart( .ok_or(FileUploadErrorKind::ParseError) .map(|cd| String::from(cd)) }); + let err = |e: FileUploadErrorKind| Box::new(future::err(e).into_stream()); match filename { Ok(f) => { + match fs::metadata(&file_path) { + Ok(metadata) => { + if !metadata.is_dir() || metadata.permissions().readonly() { + return err(FileUploadErrorKind::InsufficientPermissions); + } + } + Err(_) => { + return err(FileUploadErrorKind::InsufficientPermissions); + } + } file_path = file_path.join(f); Box::new(save_file(field, file_path, override_files).into_stream()) } - Err(e) => Box::new(future::err(e).into_stream()), + Err(e) => err(e), } } multipart::MultipartItem::Nested(mp) => Box::new( -- cgit v1.2.3 From 756a63200cdc48bf17d2997a2cbd537802a5193c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Pej=C5=A1a?= Date: Fri, 29 Mar 2019 17:21:14 +0100 Subject: Fix typos --- src/errors.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/errors.rs b/src/errors.rs index 1eaa7c7..21d9e07 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -4,20 +4,20 @@ use std::fmt::{self, Debug, Display}; /// Kinds of errors which might happen during file upload #[derive(Debug, Fail)] pub enum FileUploadErrorKind { - /// This error will occur when file overriding is off and file with same name already exists + /// This error will occur when file overriding is off and a file with same name already exists #[fail(display = "File with this name already exists")] FileExist, - /// This error will occur when server will fail to preccess http header during file upload + /// This error will occur when the server fails to process the HTTP header during file upload #[fail(display = "Failed to parse incoming request")] ParseError, - /// This error will occur when we fail to precess multipart request + /// This error will occur when we fail to process the multipart request #[fail(display = "Failed to process multipart request")] MultipartError(actix_web::error::MultipartError), - /// This error may occur when trying to write incoming file to disk + /// This error may occur when trying to write the incoming file to disk #[fail(display = "Failed to create or write to file")] IOError(std::io::Error), /// This error will occur when we he have insuffictent permissions to create new file - #[fail(display = "Insuffitient permissions to create file")] + #[fail(display = "Insufficient permissions to create file")] InsufficientPermissions, } -- cgit v1.2.3 From 8f3d5b70416e9275fdd0953181dee3bd6c6b0c92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Pej=C5=A1a?= Date: Fri, 29 Mar 2019 17:47:33 +0100 Subject: Fix syntax error and clippy warnings. --- src/file_upload.rs | 29 +++++++++++++++++------------ src/main.rs | 4 ++-- 2 files changed, 19 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/src/file_upload.rs b/src/file_upload.rs index bc4efb1..88b8802 100644 --- a/src/file_upload.rs +++ b/src/file_upload.rs @@ -35,12 +35,12 @@ fn save_file( }; Box::new( field - .map_err(|e| FileUploadErrorKind::MultipartError(e)) + .map_err(FileUploadErrorKind::MultipartError) .fold(0i64, move |acc, bytes| { let rt = file .write_all(bytes.as_ref()) .map(|_| acc + bytes.len() as i64) - .map_err(|e| FileUploadErrorKind::IOError(e)); + .map_err(FileUploadErrorKind::IOError); future::result(rt) }), ) @@ -66,7 +66,7 @@ fn handle_multipart( content_disposition .get_filename() .ok_or(FileUploadErrorKind::ParseError) - .map(|cd| String::from(cd)) + .map(String::from) }); let err = |e: FileUploadErrorKind| Box::new(future::err(e).into_stream()); match filename { @@ -88,7 +88,7 @@ fn handle_multipart( } } multipart::MultipartItem::Nested(mp) => Box::new( - mp.map_err(|e| FileUploadErrorKind::MultipartError(e)) + mp.map_err(FileUploadErrorKind::MultipartError) .map(move |item| handle_multipart(item, file_path.clone(), override_files)) .flatten(), ), @@ -117,7 +117,11 @@ pub fn upload_file(req: &HttpRequest) -> FutureResponse< } }; // this is really ugly I will try to think about something smarter - let return_path: String = req.headers()[header::REFERER].clone().to_str().unwrap_or("/").to_owned(); + let return_path: String = req.headers()[header::REFERER] + .clone() + .to_str() + .unwrap_or("/") + .to_owned(); let r_p2 = return_path.clone(); // if target path is under app root directory save file @@ -128,20 +132,21 @@ pub fn upload_file(req: &HttpRequest) -> FutureResponse< let override_files = req.state().override_files; Box::new( req.multipart() - .map_err(|e| FileUploadErrorKind::MultipartError(e)) + .map_err(FileUploadErrorKind::MultipartError) .map(move |item| handle_multipart(item, target_dir.clone(), override_files)) .flatten() .collect() .map(move |_| { HttpResponse::TemporaryRedirect() - .header( - header::LOCATION, - format!("{}", return_path.clone()), - ) + .header(header::LOCATION, return_path.to_string()) .finish() }) .or_else(move |e| { - let error_description = format!("{}",e); - future::ok(HttpResponse::BadRequest().body(file_upload_error(&error_description, &r_p2.clone()).into_string())) + let error_description = format!("{}", e); + future::ok( + HttpResponse::BadRequest() + .body(file_upload_error(&error_description, &r_p2.clone()).into_string()), + ) + }), ) } diff --git a/src/main.rs b/src/main.rs index 935d619..c8c13ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -187,10 +187,10 @@ fn configure_app(app: App) -> App { let path = &app.state().path; let no_symlinks = app.state().no_symlinks; let random_route = app.state().random_route.clone(); - let file_upload = app.state().file_upload.clone(); + let file_upload = app.state().file_upload; upload_route = match app.state().random_route.clone() { Some(random_route) => format!("/{}/upload", random_route), - None => format!("/upload"), + None => "/upload".to_string(), }; if path.is_file() { None -- cgit v1.2.3 From 93ea625fe29ea02c5bda6b4f6361134a0fe667f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Pej=C5=A1a?= Date: Fri, 29 Mar 2019 23:33:27 +0100 Subject: Fix typos and indentation. --- src/args.rs | 6 +++--- src/file_upload.rs | 16 ++++++++-------- src/main.rs | 2 +- src/renderer.rs | 10 +++++----- 4 files changed, 17 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/src/args.rs b/src/args.rs index 7fa5121..bb52824 100644 --- a/src/args.rs +++ b/src/args.rs @@ -53,8 +53,8 @@ struct CLIArgs { file_upload: bool, /// Enable overriding existing files during file upload - #[structopt(short = "o", long = "owerride-files")] - override_files: bool, + #[structopt(short = "o", long = "overwrite-files")] + overwrite_files: bool, } /// Checks wether an interface is valid, i.e. it can be parsed into an IP address @@ -108,7 +108,7 @@ pub fn parse_args() -> crate::MiniserveConfig { path_explicitly_chosen, no_symlinks: args.no_symlinks, random_route, - override_files: args.override_files, + overwrite_files: args.overwrite_files, file_upload: args.file_upload, } } diff --git a/src/file_upload.rs b/src/file_upload.rs index 88b8802..273d12c 100644 --- a/src/file_upload.rs +++ b/src/file_upload.rs @@ -22,9 +22,9 @@ struct QueryParameters { fn save_file( field: multipart::Field, file_path: PathBuf, - override_files: bool, + overwrite_files: bool, ) -> Box> { - if !override_files && file_path.exists() { + if !overwrite_files && file_path.exists() { return Box::new(future::err(FileUploadErrorKind::FileExist)); } let mut file = match std::fs::File::create(file_path) { @@ -50,7 +50,7 @@ fn save_file( fn handle_multipart( item: multipart::MultipartItem, mut file_path: PathBuf, - override_files: bool, + overwrite_files: bool, ) -> Box> { match item { multipart::MultipartItem::Field(field) => { @@ -82,14 +82,14 @@ fn handle_multipart( } } file_path = file_path.join(f); - Box::new(save_file(field, file_path, override_files).into_stream()) + Box::new(save_file(field, file_path, overwrite_files).into_stream()) } Err(e) => err(e), } } multipart::MultipartItem::Nested(mp) => Box::new( mp.map_err(FileUploadErrorKind::MultipartError) - .map(move |item| handle_multipart(item, file_path.clone(), override_files)) + .map(move |item| handle_multipart(item, file_path.clone(), overwrite_files)) .flatten(), ), } @@ -124,16 +124,16 @@ pub fn upload_file(req: &HttpRequest) -> FutureResponse< .to_owned(); let r_p2 = return_path.clone(); - // if target path is under app root directory save file + // If the target path is under the app root directory, save the file. let target_dir = match &app_root_dir.clone().join(path.clone()).canonicalize() { Ok(path) if path.starts_with(&app_root_dir) => path.clone(), _ => return Box::new(future::ok(HttpResponse::BadRequest().body("Invalid path"))), }; - let override_files = req.state().override_files; + let overwrite_files = req.state().overwrite_files; Box::new( req.multipart() .map_err(FileUploadErrorKind::MultipartError) - .map(move |item| handle_multipart(item, target_dir.clone(), override_files)) + .map(move |item| handle_multipart(item, target_dir.clone(), overwrite_files)) .flatten() .collect() .map(move |_| { diff --git a/src/main.rs b/src/main.rs index c8c13ea..0ca2fdf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,7 +49,7 @@ pub struct MiniserveConfig { pub file_upload: bool, /// Enable upload to override existing files - pub override_files: bool, + pub overwrite_files: bool, } fn main() { diff --git a/src/renderer.rs b/src/renderer.rs index 75d5c56..cb6d5d5 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -24,11 +24,11 @@ pub fn page( span #top { } h1 { (page_title) } @if file_upload { - form action={(upload_route) "?path=" (current_dir)} method="POST" enctype="multipart/form-data" { - p { "Select file to upload" } - input type="file" name="file_to_upload" {} - input type="submit" value="Upload file" {} - } + form action={(upload_route) "?path=" (current_dir)} method="POST" enctype="multipart/form-data" { + p { "Select file to upload" } + input type="file" name="file_to_upload" {} + input type="submit" value="Upload file" {} + } } div.download { (archive_button(archive::CompressionMethod::TarGz)) -- cgit v1.2.3 From ab711698b8cae4720311a705f3902df21cbe5893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Pej=C5=A1a?= Date: Wed, 3 Apr 2019 15:57:19 +0200 Subject: Allow drag and drop file upload with js. --- src/renderer.rs | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/renderer.rs b/src/renderer.rs index cb6d5d5..5446205 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -20,13 +20,13 @@ pub fn page( ) -> Markup { html! { (page_header(page_title)) - body { + body id="dropContainer" { span #top { } h1 { (page_title) } @if file_upload { - form action={(upload_route) "?path=" (current_dir)} method="POST" enctype="multipart/form-data" { + form id="file_submit" action={(upload_route) "?path=" (current_dir)} method="POST" enctype="multipart/form-data" { p { "Select file to upload" } - input type="file" name="file_to_upload" {} + input type="file" name="file_to_upload" id="fileInput" {} input type="submit" value="Upload file" {} } } @@ -309,6 +309,9 @@ fn css() -> Markup { .download a:not(:last-of-type) { margin-right: 1rem; } + .drag_hover { + box-shadow: inset 0 25px 40px #aae; + } @media (max-width: 600px) { h1 { font-size: 1.375em; @@ -365,6 +368,26 @@ fn page_header(page_title: &str) -> Markup { meta name="viewport" content="width=device-width, initial-scale=1"; title { (page_title) } style { (css()) } + (PreEscaped(r#" + + "#)) } } } -- cgit v1.2.3 From 0b6437d60bb71538698072d281570166962c6a93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Pej=C5=A1a?= Date: Fri, 5 Apr 2019 13:14:18 +0200 Subject: Improve file upload text. --- src/renderer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/renderer.rs b/src/renderer.rs index 5446205..c166bc6 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -25,7 +25,7 @@ pub fn page( h1 { (page_title) } @if file_upload { form id="file_submit" action={(upload_route) "?path=" (current_dir)} method="POST" enctype="multipart/form-data" { - p { "Select file to upload" } + p { "Select file to upload or drag it into the window" } input type="file" name="file_to_upload" id="fileInput" {} input type="submit" value="Upload file" {} } -- cgit v1.2.3