diff options
-rw-r--r-- | CHANGELOG.md | 2 | ||||
-rw-r--r-- | data/style.scss | 23 | ||||
-rw-r--r-- | src/file_upload.rs | 233 | ||||
-rw-r--r-- | src/main.rs | 4 |
4 files changed, 122 insertions, 140 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index d897915..8b1a520 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] - ReleaseDate - Fix serving files with backslashes in their names [#578](https://github.com/svenstaro/miniserve/pull/578) (thanks @Jikstra) - Fix behavior of downloading symlinks by upgrading to actix-web 4 [#582](https://github.com/svenstaro/miniserve/pull/582)[#462](https://github.com/svenstaro/miniserve/issues/462) (thanks @aliemjay) +- List directory if index file not found [#583](https://github.com/svenstaro/miniserve/pull/583)[#275](https://github.com/svenstaro/miniserve/pull/583) (thanks @aliemjay) +- Add special colors for visited links [#521](https://github.com/svenstaro/miniserve/pull/521) (thanks @raffomania) ## [0.15.0] - 2021-08-27 - Add hardened systemd template unit file to `packaging/miniserve@.service` diff --git a/data/style.scss b/data/style.scss index 7051d22..0a69222 100644 --- a/data/style.scss +++ b/data/style.scss @@ -69,17 +69,22 @@ a:hover { text-decoration: underline; } -a.directory, -a.directory:visited { +a.directory { font-weight: bold; color: var(--directory_link_color); + + &:visited { + color: var(--directory_link_color_visited); + } } a.file, -a.file:visited, -.error-back, -.error-back:visited { +.error-back { color: var(--file_link_color); + + &:visited { + color: var(--file_link_color_visited) + } } a.directory:hover { @@ -524,7 +529,9 @@ th span.active span { --background: #ffffff; --text_color: #323232; --directory_link_color: #d02474; + --directory_link_color_visited: #9b1985; --file_link_color: #0086b3; + --file_link_color_visited: #974ec2; --table_background: #ffffff; --table_text_color: #323232; --table_header_background: #323232; @@ -568,7 +575,9 @@ th span.active span { --background: #383c4a; --text_color: #fefefe; --directory_link_color: #03a9f4; + --directory_link_color_visited: #0388f4; --file_link_color: #ea95ff; + --file_link_color_visited: #a595ff; --table_background: #353946; --table_text_color: #eeeeee; --table_header_background: #5294e2; @@ -612,7 +621,9 @@ th span.active span { --background: #3f3f3f; --text_color: #efefef; --directory_link_color: #f0dfaf; + --directory_link_color_visited: #ebc390; --file_link_color: #87d6d5; + --file_link_color_visited: #a7b9ec; --table_background: #4a4949; --table_text_color: #efefef; --table_header_background: #7f9f7f; @@ -656,7 +667,9 @@ th span.active span { --background: #272822; --text_color: #f8f8f2; --directory_link_color: #f92672; + --directory_link_color_visited: #bc39a7; --file_link_color: #a6e22e; + --file_link_color_visited: #4cb936; --table_background: #3b3a32; --table_text_color: #f8f8f0; --table_header_background: #75715e; diff --git a/src/file_upload.rs b/src/file_upload.rs index 5faa67f..6fa99ef 100644 --- a/src/file_upload.rs +++ b/src/file_upload.rs @@ -2,11 +2,10 @@ use actix_web::{ http::{header, StatusCode}, HttpRequest, HttpResponse, }; -use futures::{future, Future, FutureExt, Stream, TryStreamExt}; +use futures::TryStreamExt; use std::{ io::Write, path::{Component, PathBuf}, - pin::Pin, }; use crate::errors::{self, ContextualError}; @@ -14,88 +13,62 @@ use crate::listing::{self, SortingMethod, SortingOrder}; use crate::renderer; /// Create future to save file. -fn save_file( +async fn save_file( field: actix_multipart::Field, file_path: PathBuf, overwrite_files: bool, -) -> Pin<Box<dyn Future<Output = Result<i64, ContextualError>>>> { +) -> Result<u64, ContextualError> { if !overwrite_files && file_path.exists() { - return Box::pin(future::err(ContextualError::DuplicateFileError)); + return Err(ContextualError::DuplicateFileError); } - let mut file = match std::fs::File::create(&file_path) { - Ok(file) => file, - Err(e) => { - return Box::pin(future::err(ContextualError::IoError( - format!("Failed to create {}", file_path.display()), - e, - ))); - } - }; - Box::pin( - field - .map_err(ContextualError::MultipartError) - .try_fold(0i64, move |acc, bytes| { - let rt = file - .write_all(bytes.as_ref()) - .map(|_| acc + bytes.len() as i64) - .map_err(|e| { - ContextualError::IoError("Failed to write to file".to_string(), e) - }); - future::ready(rt) - }), - ) + let file = std::fs::File::create(&file_path).map_err(|e| { + ContextualError::IoError(format!("Failed to create {}", file_path.display()), e) + })?; + + let (_, written_len) = field + .map_err(ContextualError::MultipartError) + .try_fold((file, 0u64), |(mut file, written_len), bytes| async move { + file.write_all(bytes.as_ref()) + .map_err(|e| ContextualError::IoError("Failed to write to file".to_string(), e))?; + Ok((file, written_len + bytes.len() as u64)) + }) + .await?; + + Ok(written_len) } /// Create new future to handle file as multipart data. -fn handle_multipart( +async fn handle_multipart( field: actix_multipart::Field, - mut file_path: PathBuf, + file_path: PathBuf, overwrite_files: bool, -) -> Pin<Box<dyn Stream<Item = Result<i64, ContextualError>>>> { +) -> Result<u64, ContextualError> { let filename = field - .headers() - .get(header::CONTENT_DISPOSITION) - .ok_or(ContextualError::ParseError) - .and_then(|cd| { - header::ContentDisposition::from_raw(cd).map_err(|_| ContextualError::ParseError) - }) - .and_then(|content_disposition| { - content_disposition - .get_filename() - .ok_or(ContextualError::ParseError) - .map(String::from) - }); - let err = |e: ContextualError| Box::pin(future::err(e).into_stream()); - match filename { - Ok(f) => { - match std::fs::metadata(&file_path) { - Ok(metadata) => { - if !metadata.is_dir() { - return err(ContextualError::InvalidPathError(format!( - "cannot upload file to {}, since it's not a directory", - &file_path.display() - ))); - } else if metadata.permissions().readonly() { - return err(ContextualError::InsufficientPermissionsError( - file_path.display().to_string(), - )); - } - } - Err(_) => { - return err(ContextualError::InsufficientPermissionsError( - file_path.display().to_string(), - )); - } - } - file_path = file_path.join(f); - Box::pin(save_file(field, file_path, overwrite_files).into_stream()) - } - Err(e) => err(e( - "HTTP header".to_string(), - "Failed to retrieve the name of the file to upload".to_string(), + .content_disposition() + .and_then(|cd| cd.get_filename().map(String::from)) + .ok_or_else(|| { + ContextualError::ParseError( + "HTTP header".to_string(), + "Failed to retrieve the name of the file to upload".to_string(), + ) + })?; + + match std::fs::metadata(&file_path) { + Err(_) => Err(ContextualError::InsufficientPermissionsError( + file_path.display().to_string(), )), - } + Ok(metadata) if !metadata.is_dir() => Err(ContextualError::InvalidPathError(format!( + "cannot upload file to {}, since it's not a directory", + &file_path.display() + ))), + Ok(metadata) if metadata.permissions().readonly() => Err( + ContextualError::InsufficientPermissionsError(file_path.display().to_string()), + ), + Ok(_) => Ok(()), + }?; + + save_file(field, file_path.join(filename), overwrite_files).await } /// Handle incoming request to upload file. @@ -104,16 +77,16 @@ fn handle_multipart( /// invalid. /// This method returns future. #[allow(clippy::too_many_arguments)] -pub fn upload_file( +pub async fn upload_file( req: HttpRequest, payload: actix_web::web::Payload, uses_random_route: bool, favicon_route: String, css_route: String, - default_color_scheme: &str, - default_color_scheme_dark: &str, + default_color_scheme: String, + default_color_scheme_dark: String, hide_version_footer: bool, -) -> Pin<Box<dyn Future<Output = Result<HttpResponse, actix_web::Error>>>> { +) -> Result<HttpResponse, actix_web::Error> { let conf = req.app_data::<crate::MiniserveConfig>().unwrap(); let return_path = if let Some(header) = req.headers().get(header::REFERER) { header.to_str().unwrap_or("/").to_owned() @@ -131,7 +104,7 @@ pub fn upload_file( let err = ContextualError::InvalidHttpRequestError( "Missing query parameter 'path'".to_string(), ); - return Box::pin(create_error_response( + return Ok(create_error_response( &err.to_string(), StatusCode::BAD_REQUEST, &return_path, @@ -140,8 +113,8 @@ pub fn upload_file( uses_random_route, &favicon_route, &css_route, - default_color_scheme, - default_color_scheme_dark, + &default_color_scheme, + &default_color_scheme_dark, hide_version_footer, )); } @@ -154,7 +127,7 @@ pub fn upload_file( "Failed to resolve path served by miniserve".to_string(), e, ); - return Box::pin(create_error_response( + return Ok(create_error_response( &err.to_string(), StatusCode::INTERNAL_SERVER_ERROR, &return_path, @@ -163,8 +136,8 @@ pub fn upload_file( uses_random_route, &favicon_route, &css_route, - default_color_scheme, - default_color_scheme_dark, + &default_color_scheme, + &default_color_scheme_dark, hide_version_footer, )); } @@ -177,7 +150,7 @@ pub fn upload_file( let err = ContextualError::InvalidHttpRequestError( "Invalid value for 'path' parameter".to_string(), ); - return Box::pin(create_error_response( + return Ok(create_error_response( &err.to_string(), StatusCode::BAD_REQUEST, &return_path, @@ -186,8 +159,8 @@ pub fn upload_file( uses_random_route, &favicon_route, &css_route, - default_color_scheme, - default_color_scheme_dark, + &default_color_scheme, + &default_color_scheme_dark, hide_version_footer, )); } @@ -196,33 +169,29 @@ pub fn upload_file( let default_color_scheme = conf.default_color_scheme.clone(); let default_color_scheme_dark = conf.default_color_scheme_dark.clone(); - Box::pin( - actix_multipart::Multipart::new(req.headers(), payload) - .map_err(ContextualError::MultipartError) - .map_ok(move |item| handle_multipart(item, target_dir.clone(), overwrite_files)) - .try_flatten() - .try_collect::<Vec<_>>() - .then(move |e| match e { - Ok(_) => future::ok( - HttpResponse::SeeOther() - .append_header((header::LOCATION, return_path)) - .finish(), - ), - Err(e) => create_error_response( - &e.to_string(), - StatusCode::INTERNAL_SERVER_ERROR, - &return_path, - query_params.sort, - query_params.order, - uses_random_route, - &favicon_route, - &css_route, - &default_color_scheme, - &default_color_scheme_dark, - hide_version_footer, - ), - }), - ) + match actix_multipart::Multipart::new(req.headers(), payload) + .map_err(ContextualError::MultipartError) + .and_then(move |field| handle_multipart(field, target_dir.clone(), overwrite_files)) + .try_collect::<Vec<u64>>() + .await + { + Ok(_) => Ok(HttpResponse::SeeOther() + .append_header((header::LOCATION, return_path)) + .finish()), + Err(e) => Ok(create_error_response( + &e.to_string(), + StatusCode::INTERNAL_SERVER_ERROR, + &return_path, + query_params.sort, + query_params.order, + uses_random_route, + &favicon_route, + &css_route, + &default_color_scheme, + &default_color_scheme_dark, + hide_version_footer, + )), + } } /// Convenience method for creating response errors, if file upload fails. @@ -239,27 +208,25 @@ fn create_error_response( default_color_scheme: &str, default_color_scheme_dark: &str, hide_version_footer: bool, -) -> future::Ready<Result<HttpResponse, actix_web::Error>> { +) -> HttpResponse { errors::log_error_chain(description.to_string()); - future::ok( - HttpResponse::BadRequest() - .content_type("text/html; charset=utf-8") - .body( - renderer::render_error( - description, - error_code, - return_path, - sorting_method, - sorting_order, - true, - !uses_random_route, - favicon_route, - css_route, - default_color_scheme, - default_color_scheme_dark, - hide_version_footer, - ) - .into_string(), - ), - ) + HttpResponse::BadRequest() + .content_type("text/html; charset=utf-8") + .body( + renderer::render_error( + description, + error_code, + return_path, + sorting_method, + sorting_order, + true, + !uses_random_route, + favicon_route, + css_route, + default_color_scheme, + default_color_scheme_dark, + hide_version_footer, + ) + .into_string(), + ) } diff --git a/src/main.rs b/src/main.rs index 370d991..398f580 100644 --- a/src/main.rs +++ b/src/main.rs @@ -369,8 +369,8 @@ fn configure_app(app: &mut web::ServiceConfig, conf: &MiniserveConfig) { uses_random_route, favicon_route.clone(), css_route.clone(), - &default_color_scheme, - &default_color_scheme_dark, + default_color_scheme.clone(), + default_color_scheme_dark.clone(), hide_version_footer, ) })), |