diff options
-rw-r--r-- | src/archive.rs | 43 | ||||
-rw-r--r-- | src/args.rs | 15 | ||||
-rw-r--r-- | src/auth.rs | 20 | ||||
-rw-r--r-- | src/errors.rs | 6 | ||||
-rw-r--r-- | src/file_upload.rs | 123 | ||||
-rw-r--r-- | src/listing.rs | 43 | ||||
-rw-r--r-- | src/main.rs | 47 | ||||
-rw-r--r-- | src/renderer.rs | 200 | ||||
-rw-r--r-- | src/themes.rs | 15 |
9 files changed, 336 insertions, 176 deletions
diff --git a/src/archive.rs b/src/archive.rs index a76446a..02300c5 100644 --- a/src/archive.rs +++ b/src/archive.rs @@ -7,7 +7,7 @@ use std::path::PathBuf; use strum_macros::{Display, EnumIter, EnumString}; use tar::Builder; -use crate::errors::{ContextualError}; +use crate::errors::ContextualError; /// Available compression methods #[derive(Deserialize, Clone, EnumIter, EnumString, Display)] @@ -62,17 +62,11 @@ fn tgz_compress(dir: &PathBuf, skip_symlinks: bool) -> Result<(String, Bytes), C let mut tgz_data = Bytes::new(); let tar_data = tar(src_dir, directory.to_string(), skip_symlinks).map_err(|e| { - ContextualError::ArchiveCreationError( - "tarball".to_string(), - Box::new(e), - ) + ContextualError::ArchiveCreationError("tarball".to_string(), Box::new(e)) })?; let gz_data = gzip(&tar_data).map_err(|e| { - ContextualError::ArchiveCreationError( - "GZIP archive".to_string(), - Box::new(e), - ) + ContextualError::ArchiveCreationError("GZIP archive".to_string(), Box::new(e)) })?; tgz_data.extend_from_slice(&gz_data); @@ -115,10 +109,7 @@ fn tar( })?; let tar_content = tar_builder.into_inner().map_err(|e| { - ContextualError::IOError( - "Failed to finish writing the TAR archive".to_string(), - e, - ) + ContextualError::IOError("Failed to finish writing the TAR archive".to_string(), e) })?; Ok(tar_content) @@ -126,24 +117,14 @@ fn tar( /// Compresses a stream of bytes using the GZIP algorithm, and returns the resulting stream fn gzip(mut data: &[u8]) -> Result<Vec<u8>, ContextualError> { - let mut encoder = Encoder::new(Vec::new()).map_err(|e| { - ContextualError::IOError( - "Failed to create GZIP encoder".to_string(), - e, - ) - })?; - io::copy(&mut data, &mut encoder).map_err(|e| { - ContextualError::IOError( - "Failed to write GZIP data".to_string(), - e, - ) - })?; - let data = encoder.finish().into_result().map_err(|e| { - ContextualError::IOError( - "Failed to write GZIP trailer".to_string(), - e, - ) - })?; + let mut encoder = Encoder::new(Vec::new()) + .map_err(|e| ContextualError::IOError("Failed to create GZIP encoder".to_string(), e))?; + io::copy(&mut data, &mut encoder) + .map_err(|e| ContextualError::IOError("Failed to write GZIP data".to_string(), e))?; + let data = encoder + .finish() + .into_result() + .map_err(|e| ContextualError::IOError("Failed to write GZIP trailer".to_string(), e))?; Ok(data) } diff --git a/src/args.rs b/src/args.rs index 63799a0..d37e429 100644 --- a/src/args.rs +++ b/src/args.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use structopt::StructOpt; use crate::auth; -use crate::errors::{ContextualError}; +use crate::errors::ContextualError; use crate::themes; /// Possible characters for random routes @@ -98,15 +98,13 @@ fn parse_auth(src: &str) -> Result<auth::RequiredAuth, ContextualError> { let hash_bin = if let Ok(hash_bin) = hex::decode(hash_hex) { hash_bin } else { - return Err(ContextualError::InvalidPasswordHash) + return Err(ContextualError::InvalidPasswordHash); }; match second_part { "sha256" => auth::RequiredAuthPassword::Sha256(hash_bin.to_owned()), "sha512" => auth::RequiredAuthPassword::Sha512(hash_bin.to_owned()), - _ => { - return Err(ContextualError::InvalidHashMethod(second_part.to_owned())) - }, + _ => return Err(ContextualError::InvalidHashMethod(second_part.to_owned())), } } else { // To make it Windows-compatible, the password needs to be shorter than 255 characters. @@ -185,7 +183,10 @@ mod tests { } #[rstest_parametrize( - auth_string, username, password, encrypt, + auth_string, + username, + password, + encrypt, case("username:password", "username", "password", "plain"), case("username:sha256:abcd", "username", "abcd", "sha256"), case("username:sha512:abcd", "username", "abcd", "sha512") @@ -205,7 +206,7 @@ mod tests { ), case( "username:blahblah:abcd", - "blahblah is not a valid hashing method. Expected sha256 or sha512" + "Invalid hashing method blahblah. Expected sha256 or sha512" ), case( "username:sha256:invalid", diff --git a/src/auth.rs b/src/auth.rs index 8e6532b..c786d4b 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -3,7 +3,7 @@ use actix_web::middleware::{Middleware, Response}; use actix_web::{HttpRequest, HttpResponse, Result}; use sha2::{Digest, Sha256, Sha512}; -use crate::errors::{ContextualError}; +use crate::errors::ContextualError; pub struct Auth; @@ -36,10 +36,7 @@ pub fn parse_basic_auth( let basic_removed = authorization_header .to_str() .map_err(|e| { - ContextualError::ParseError( - "HTTP authentication header".to_string(), - e.to_string(), - ) + ContextualError::ParseError("HTTP authentication header".to_string(), e.to_string()) })? .replace("Basic ", ""); let decoded = base64::decode(&basic_removed).map_err(ContextualError::Base64DecodeError)?; @@ -75,8 +72,8 @@ pub fn match_auth(basic_auth: BasicAuthParams, required_auth: &RequiredAuth) -> } /// Return `true` if hashing of `password` by `T` algorithm equals to `hash` -pub fn compare_hash<T: Digest>(password: String, hash: &Vec<u8>) -> bool { - get_hash::<T>(password) == *hash +pub fn compare_hash<T: Digest>(password: String, hash: &[u8]) -> bool { + get_hash::<T>(password) == hash } /// Get hash of a `text` @@ -163,13 +160,18 @@ mod tests { } #[rstest_parametrize( - should_pass, param_username, param_password, required_username, required_password, encrypt, + should_pass, + param_username, + param_password, + required_username, + required_password, + encrypt, case(true, "obi", "hello there", "obi", "hello there", "plain"), case(false, "obi", "hello there", "obi", "hi!", "plain"), case(true, "obi", "hello there", "obi", "hello there", "sha256"), case(false, "obi", "hello there", "obi", "hi!", "sha256"), case(true, "obi", "hello there", "obi", "hello there", "sha512"), - case(false, "obi", "hello there", "obi", "hi!", "sha512"), + case(false, "obi", "hello there", "obi", "hi!", "sha512") )] fn test_auth( should_pass: bool, diff --git a/src/errors.rs b/src/errors.rs index b8af25d..8264de0 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -32,7 +32,7 @@ pub enum ContextualError { InvalidAuthFormat, /// This error might occure if the hash method is neither sha256 nor sha512 - #[fail(display = "{} is not a valid hashing method. Expected sha256 or sha512", _0)] + #[fail(display = "Invalid hashing method {}. Expected sha256 or sha512", _0)] InvalidHashMethod(String), /// This error might occur if the HTTP auth hash password is not a valid hex code @@ -64,6 +64,10 @@ pub enum ContextualError { _0 )] HTTPAuthenticationError(Box<ContextualError>), + + /// This error might occur when an HTTP request is invalid + #[fail(display = "Invalid HTTP request\ncaused by: {}", _0)] + InvalidHTTPRequestError(String), } pub fn log_error_chain(description: String) { diff --git a/src/file_upload.rs b/src/file_upload.rs index 7f9cede..46a3a1f 100644 --- a/src/file_upload.rs +++ b/src/file_upload.rs @@ -3,7 +3,6 @@ use actix_web::{ HttpResponse, Query, }; use futures::{future, future::FutureResult, Future, Stream}; -use serde::Deserialize; use std::{ fs, io::Write, @@ -11,13 +10,9 @@ use std::{ }; use crate::errors::{self, ContextualError}; +use crate::listing::{QueryParameters, SortingMethod, SortingOrder}; use crate::renderer; - -/// Query parameters -#[derive(Debug, Deserialize)] -struct QueryParameters { - path: PathBuf, -} +use crate::themes::ColorScheme; /// Create future to save file. fn save_file( @@ -35,7 +30,7 @@ fn save_file( Ok(file) => file, Err(e) => { return Box::new(future::err(ContextualError::IOError( - format!("Failed to create file in {}", file_path.display()), + format!("Failed to create {}", file_path.display()), e, ))); } @@ -121,37 +116,95 @@ fn handle_multipart( /// 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<crate::MiniserveConfig>) -> FutureResponse<HttpResponse> { +pub fn upload_file( + req: &HttpRequest<crate::MiniserveConfig>, + default_color_scheme: ColorScheme, +) -> FutureResponse<HttpResponse> { let return_path = if let Some(header) = req.headers().get(header::REFERER) { header.to_str().unwrap_or("/").to_owned() } else { "/".to_string() }; - let app_root_dir = if let Ok(dir) = req.state().path.canonicalize() { - dir - } else { - return Box::new(create_error_response("Internal server error", &return_path)); - }; - let path = match Query::<QueryParameters>::extract(req) { + + let (path, sort_method, sort_order, color_scheme) = match Query::<QueryParameters>::extract(req) + { Ok(query) => { - if let Ok(stripped_path) = query.path.strip_prefix(Component::RootDir) { - stripped_path.to_owned() + let sort_param = query.sort; + let order_param = query.order; + let theme_param = query.theme.unwrap_or(default_color_scheme); + + if let Some(path) = query.path.clone() { + if let Ok(stripped_path) = path.strip_prefix(Component::RootDir) { + ( + stripped_path.to_owned(), + sort_param, + order_param, + theme_param, + ) + } else { + (path.clone(), sort_param, order_param, theme_param) + } } else { - query.path.clone() + let err = ContextualError::InvalidHTTPRequestError( + "Missing query parameter 'path'".to_string(), + ); + return Box::new(create_error_response( + &err.to_string(), + &return_path, + sort_param, + order_param, + theme_param, + default_color_scheme, + )); } } - Err(_) => { + Err(e) => { + let err = ContextualError::InvalidHTTPRequestError(e.to_string()); return Box::new(create_error_response( - "Unspecified parameter path", + &err.to_string(), &return_path, - )) + None, + None, + default_color_scheme, + default_color_scheme, + )); + } + }; + + let app_root_dir = match req.state().path.canonicalize() { + Ok(dir) => dir, + Err(e) => { + let err = ContextualError::IOError( + "Failed to resolve path served by miniserve".to_string(), + e, + ); + return Box::new(create_error_response( + &err.to_string(), + &return_path, + sort_method, + sort_order, + color_scheme, + default_color_scheme, + )); } }; // If the target path is under the app root directory, save the 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(create_error_response("Invalid path", &return_path)), + _ => { + let err = ContextualError::InvalidHTTPRequestError( + "Invalid value for 'path' parameter".to_string(), + ); + return Box::new(create_error_response( + &err.to_string(), + &return_path, + sort_method, + sort_order, + color_scheme, + default_color_scheme, + )); + } }; let overwrite_files = req.state().overwrite_files; Box::new( @@ -166,7 +219,14 @@ pub fn upload_file(req: &HttpRequest<crate::MiniserveConfig>) -> FutureResponse< .header(header::LOCATION, return_path.to_string()) .finish(), ), - Err(e) => create_error_response(&e.to_string(), &return_path), + Err(e) => create_error_response( + &e.to_string(), + &return_path, + sort_method, + sort_order, + color_scheme, + default_color_scheme, + ), }), ) } @@ -175,11 +235,26 @@ pub fn upload_file(req: &HttpRequest<crate::MiniserveConfig>) -> FutureResponse< fn create_error_response( description: &str, return_path: &str, + sorting_method: Option<SortingMethod>, + sorting_order: Option<SortingOrder>, + color_scheme: ColorScheme, + default_color_scheme: ColorScheme, ) -> FutureResult<HttpResponse, actix_web::error::Error> { errors::log_error_chain(description.to_string()); future::ok( HttpResponse::BadRequest() .content_type("text/html; charset=utf-8") - .body(renderer::render_error(description, return_path).into_string()), + .body( + renderer::render_error( + description, + return_path, + sorting_method, + sorting_order, + color_scheme, + default_color_scheme, + true, + ) + .into_string(), + ), ) } diff --git a/src/listing.rs b/src/listing.rs index a030feb..cadbb99 100644 --- a/src/listing.rs +++ b/src/listing.rs @@ -5,7 +5,7 @@ use htmlescape::encode_minimal as escape_html_entity; use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; use serde::Deserialize; use std::io; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::time::SystemTime; use strum_macros::{Display, EnumString}; @@ -16,15 +16,16 @@ use crate::themes; /// Query parameters #[derive(Deserialize)] -struct QueryParameters { - sort: Option<SortingMethod>, - order: Option<SortingOrder>, +pub struct QueryParameters { + pub path: Option<PathBuf>, + pub sort: Option<SortingMethod>, + pub order: Option<SortingOrder>, + pub theme: Option<themes::ColorScheme>, download: Option<archive::CompressionMethod>, - theme: Option<themes::ColorScheme>, } /// Available sorting methods -#[derive(Deserialize, Clone, EnumString, Display)] +#[derive(Deserialize, Clone, EnumString, Display, Copy)] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum SortingMethod { @@ -39,7 +40,7 @@ pub enum SortingMethod { } /// Available sorting orders -#[derive(Deserialize, Clone, EnumString, Display)] +#[derive(Deserialize, Clone, EnumString, Display, Copy)] pub enum SortingOrder { /// Ascending order #[serde(alias = "asc")] @@ -144,12 +145,7 @@ pub fn directory_listing<S>( let (sort_method, sort_order, download, color_scheme) = if let Ok(query) = Query::<QueryParameters>::extract(req) { - ( - query.sort.clone(), - query.order.clone(), - query.download.clone(), - query.theme.clone(), - ) + (query.sort, query.order, query.download.clone(), query.theme) } else { (None, None, None, None) }; @@ -158,7 +154,7 @@ pub fn directory_listing<S>( for entry in dir.path.read_dir()? { if dir.is_visible(&entry) { - let entry = entry.unwrap(); + let entry = entry?; let p = match entry.path().strip_prefix(&dir.path) { Ok(p) => base.join(p), Err(_) => continue, @@ -210,7 +206,7 @@ pub fn directory_listing<S>( } } - if let Some(sorting_method) = &sort_method { + if let Some(sorting_method) = sort_method { match sorting_method { SortingMethod::Name => entries .sort_by(|e1, e2| alphanumeric_sort::compare_str(e1.name.clone(), e2.name.clone())), @@ -234,13 +230,13 @@ pub fn directory_listing<S>( entries.sort_by(|e1, e2| alphanumeric_sort::compare_str(e1.name.clone(), e2.name.clone())) } - if let Some(sorting_order) = &sort_order { + if let Some(sorting_order) = sort_order { if let SortingOrder::Descending = sorting_order { entries.reverse() } } - let color_scheme = color_scheme.unwrap_or_else(|| default_color_scheme.clone()); + let color_scheme = color_scheme.unwrap_or_else(|| default_color_scheme); if let Some(compression_method) = &download { log::info!( @@ -266,7 +262,18 @@ pub fn directory_listing<S>( errors::log_error_chain(err.to_string()); Ok(HttpResponse::Ok() .status(http::StatusCode::INTERNAL_SERVER_ERROR) - .body(renderer::render_error(&err.to_string(), serve_path).into_string())) + .body( + renderer::render_error( + &err.to_string(), + serve_path, + sort_method, + sort_order, + color_scheme, + default_color_scheme, + false, + ) + .into_string(), + )) } } } else { diff --git a/src/main.rs b/src/main.rs index c1bb0aa..c5c81f2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,7 +19,7 @@ mod listing; mod renderer; mod themes; -use crate::errors::{ContextualError}; +use crate::errors::ContextualError; #[derive(Clone)] /// Configuration of the Miniserve application @@ -84,10 +84,7 @@ fn run() -> Result<(), ContextualError> { .path .symlink_metadata() .map_err(|e| { - ContextualError::IOError( - "Failed to retrieve symlink's metadata".to_string(), - e, - ) + ContextualError::IOError("Failed to retrieve symlink's metadata".to_string(), e) })? .file_type() .is_symlink() @@ -117,10 +114,7 @@ fn run() -> Result<(), ContextualError> { .collect::<Vec<String>>(); let canon_path = miniserve_config.path.canonicalize().map_err(|e| { - ContextualError::IOError( - "Failed to resolve path to be served".to_string(), - e, - ) + ContextualError::IOError("Failed to resolve path to be served".to_string(), e) })?; let path_string = canon_path.to_string_lossy(); @@ -135,20 +129,14 @@ fn run() -> Result<(), ContextualError> { " Invoke with -h|--help to see options or invoke as `miniserve .` to hide this advice." ); print!("Starting server in "); - io::stdout().flush().map_err(|e| { - ContextualError::IOError( - "Failed to write data".to_string(), - e, - ) - })?; + io::stdout() + .flush() + .map_err(|e| ContextualError::IOError("Failed to write data".to_string(), e))?; for c in "3… 2… 1… \n".chars() { print!("{}", c); - io::stdout().flush().map_err(|e| { - ContextualError::IOError( - "Failed to write data".to_string(), - e, - ) - })?; + io::stdout() + .flush() + .map_err(|e| ContextualError::IOError("Failed to write data".to_string(), e))?; thread::sleep(Duration::from_millis(500)); } } @@ -210,12 +198,7 @@ fn run() -> Result<(), ContextualError> { .configure(configure_app) }) .bind(socket_addresses.as_slice()) - .map_err(|e| { - ContextualError::IOError( - "Failed to bind server".to_string(), - e, - ) - })? + .map_err(|e| ContextualError::IOError("Failed to bind server".to_string(), e))? .shutdown_timeout(0) .start(); @@ -239,7 +222,7 @@ fn configure_app(app: App<MiniserveConfig>) -> App<MiniserveConfig> { let path = &app.state().path; let no_symlinks = app.state().no_symlinks; let random_route = app.state().random_route.clone(); - let default_color_scheme = app.state().default_color_scheme.clone(); + let default_color_scheme = app.state().default_color_scheme; let file_upload = app.state().file_upload; upload_route = if let Some(random_route) = app.state().random_route.clone() { format!("/{}/upload", random_route) @@ -261,7 +244,7 @@ fn configure_app(app: App<MiniserveConfig>) -> App<MiniserveConfig> { no_symlinks, file_upload, random_route.clone(), - default_color_scheme.clone(), + default_color_scheme, u_r.clone(), ) }), @@ -274,9 +257,11 @@ fn configure_app(app: App<MiniserveConfig>) -> App<MiniserveConfig> { if let Some(s) = s { if app.state().file_upload { + let default_color_scheme = app.state().default_color_scheme; // Allow file upload - app.resource(&upload_route, |r| { - r.method(Method::POST).f(file_upload::upload_file) + app.resource(&upload_route, move |r| { + r.method(Method::POST) + .f(move |file| file_upload::upload_file(file, default_color_scheme)) }) // Handle directories .handler(&full_route, s) diff --git a/src/renderer.rs b/src/renderer.rs index b292e70..2ed69d0 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -22,8 +22,17 @@ pub fn page( upload_route: &str, current_dir: &str, ) -> Markup { + let upload_action = build_upload_action( + upload_route, + current_dir, + sort_method, + sort_order, + color_scheme, + default_color_scheme, + ); + html! { - (page_header(serve_path, &color_scheme, file_upload)) + (page_header(serve_path, color_scheme, file_upload)) body#drop-container { @if file_upload { div.drag-form { @@ -32,19 +41,19 @@ pub fn page( } } } - (color_scheme_selector(&sort_method, &sort_order, &color_scheme, &default_color_scheme, serve_path)) + (color_scheme_selector(sort_method, sort_order, color_scheme, default_color_scheme, serve_path)) div.container { span#top { } h1.title { "Index of " (serve_path) } div.toolbar { div.download { @for compression_method in CompressionMethod::iter() { - (archive_button(compression_method)) + (archive_button(compression_method, sort_method, sort_order, color_scheme, default_color_scheme)) } } @if file_upload { div.upload { - form id="file_submit" action={(upload_route) "?path=" (current_dir)} method="POST" enctype="multipart/form-data" { + form id="file_submit" action=(upload_action) method="POST" enctype="multipart/form-data" { p { "Select a file to upload or drag it anywhere into the window" } div { input#file-input type="file" name="file_to_upload" required="" {} @@ -56,9 +65,9 @@ pub fn page( } table { thead { - th { (build_link("name", "Name", &sort_method, &sort_order, &color_scheme, &default_color_scheme)) } - th { (build_link("size", "Size", &sort_method, &sort_order, &color_scheme, &default_color_scheme)) } - th { (build_link("date", "Last modification", &sort_method, &sort_order, &color_scheme, &default_color_scheme)) } + th { (build_link("name", "Name", sort_method, sort_order, color_scheme, default_color_scheme)) } + th { (build_link("size", "Size", sort_method, sort_order, color_scheme, default_color_scheme)) } + th { (build_link("date", "Last modification", sort_method, sort_order, color_scheme, default_color_scheme)) } } tbody { @if !is_root { @@ -66,7 +75,7 @@ pub fn page( tr { td colspan="3" { span.root-chevron { (chevron_left()) } - a.root href=(parametrized_link(&parent, &sort_method, &sort_order, &color_scheme, &default_color_scheme)) { + a.root href=(parametrized_link(&parent, sort_method, sort_order, color_scheme, default_color_scheme)) { "Parent directory" } } @@ -74,7 +83,7 @@ pub fn page( } } @for entry in entries { - (entry_row(entry, &sort_method, &sort_order, &color_scheme, &default_color_scheme)) + (entry_row(entry, sort_method, sort_order, color_scheme, default_color_scheme)) } } } @@ -86,12 +95,35 @@ pub fn page( } } +/// Build the action of the upload form +fn build_upload_action( + upload_route: &str, + current_dir: &str, + sort_method: Option<SortingMethod>, + sort_order: Option<SortingOrder>, + color_scheme: ColorScheme, + default_color_scheme: ColorScheme, +) -> String { + let mut upload_action = format!("{}?path={}", upload_route, current_dir); + if let Some(sorting_method) = sort_method { + upload_action = format!("{}&sort={}", upload_action, &sorting_method); + } + if let Some(sorting_order) = sort_order { + upload_action = format!("{}&order={}", upload_action, &sorting_order); + } + if color_scheme != default_color_scheme { + upload_action = format!("{}&theme={}", upload_action, color_scheme.to_slug()); + } + + upload_action +} + /// Partial: color scheme selector fn color_scheme_selector( - sort_method: &Option<SortingMethod>, - sort_order: &Option<SortingOrder>, - active_color_scheme: &ColorScheme, - default_color_scheme: &ColorScheme, + sort_method: Option<SortingMethod>, + sort_order: Option<SortingOrder>, + active_color_scheme: ColorScheme, + default_color_scheme: ColorScheme, serve_path: &str, ) -> Markup { html! { @@ -103,13 +135,13 @@ fn color_scheme_selector( } ul { @for color_scheme in ColorScheme::iter() { - @if active_color_scheme == &color_scheme { + @if active_color_scheme == color_scheme { li.active { - (color_scheme_link(&sort_method, &sort_order, &color_scheme, &default_color_scheme, serve_path)) + (color_scheme_link(sort_method, sort_order, color_scheme, default_color_scheme, serve_path)) } } @else { li { - (color_scheme_link(&sort_method, &sort_order, &color_scheme, &default_color_scheme, serve_path)) + (color_scheme_link(sort_method, sort_order, color_scheme, default_color_scheme, serve_path)) } } } @@ -122,18 +154,18 @@ fn color_scheme_selector( /// Partial: color scheme link fn color_scheme_link( - sort_method: &Option<SortingMethod>, - sort_order: &Option<SortingOrder>, - color_scheme: &ColorScheme, - default_color_scheme: &ColorScheme, + sort_method: Option<SortingMethod>, + sort_order: Option<SortingOrder>, + color_scheme: ColorScheme, + default_color_scheme: ColorScheme, serve_path: &str, ) -> Markup { let link = parametrized_link( serve_path, - &sort_method, - &sort_order, - &color_scheme, - &default_color_scheme, + sort_method, + sort_order, + color_scheme, + default_color_scheme, ); let title = format!("Switch to {} theme", color_scheme); @@ -151,8 +183,30 @@ fn color_scheme_link( } /// Partial: archive button -fn archive_button(compress_method: CompressionMethod) -> Markup { - let link = format!("?download={}", compress_method); +fn archive_button( + compress_method: CompressionMethod, + sort_method: Option<SortingMethod>, + sort_order: Option<SortingOrder>, + color_scheme: ColorScheme, + default_color_scheme: ColorScheme, +) -> Markup { + let link = + if sort_method.is_none() && sort_order.is_none() && color_scheme == default_color_scheme { + format!("?download={}", compress_method) + } else { + format!( + "{}&download={}", + parametrized_link( + "", + sort_method, + sort_order, + color_scheme, + default_color_scheme + ), + compress_method + ) + }; + let text = format!("Download .{}", compress_method.extension()); html! { @@ -165,10 +219,10 @@ fn archive_button(compress_method: CompressionMethod) -> Markup { /// If they are set, adds query parameters to links to keep them across pages fn parametrized_link( link: &str, - sort_method: &Option<SortingMethod>, - sort_order: &Option<SortingOrder>, - color_scheme: &ColorScheme, - default_color_scheme: &ColorScheme, + sort_method: Option<SortingMethod>, + sort_order: Option<SortingOrder>, + color_scheme: ColorScheme, + default_color_scheme: ColorScheme, ) -> String { if let Some(method) = sort_method { if let Some(order) = sort_order { @@ -193,10 +247,10 @@ fn parametrized_link( fn build_link( name: &str, title: &str, - sort_method: &Option<SortingMethod>, - sort_order: &Option<SortingOrder>, - color_scheme: &ColorScheme, - default_color_scheme: &ColorScheme, + sort_method: Option<SortingMethod>, + sort_order: Option<SortingOrder>, + color_scheme: ColorScheme, + default_color_scheme: ColorScheme, ) -> Markup { let mut link = format!("?sort={}&order=asc", name); let mut help = format!("Sort by {} in ascending order", name); @@ -231,17 +285,17 @@ fn build_link( /// Partial: row for an entry fn entry_row( entry: Entry, - sort_method: &Option<SortingMethod>, - sort_order: &Option<SortingOrder>, - color_scheme: &ColorScheme, - default_color_scheme: &ColorScheme, + sort_method: Option<SortingMethod>, + sort_order: Option<SortingOrder>, + color_scheme: ColorScheme, + default_color_scheme: ColorScheme, ) -> Markup { html! { tr { td { p { @if entry.is_dir() { - a.directory href=(parametrized_link(&entry.link, &sort_method, &sort_order, &color_scheme, &default_color_scheme)) { + a.directory href=(parametrized_link(&entry.link, sort_method, sort_order, color_scheme, default_color_scheme)) { (entry.name) "/" } } @else if entry.is_file() { @@ -256,7 +310,7 @@ fn entry_row( } } } @else if entry.is_symlink() { - a.symlink href=(parametrized_link(&entry.link, &sort_method, &sort_order, &color_scheme, &default_color_scheme)) { + a.symlink href=(parametrized_link(&entry.link, sort_method, sort_order, color_scheme, default_color_scheme)) { (entry.name) span.symlink-symbol { "⇢" } } } @@ -286,8 +340,8 @@ fn entry_row( } /// Partial: CSS -fn css(color_scheme: &ColorScheme) -> Markup { - let theme = color_scheme.clone().get_theme(); +fn css(color_scheme: ColorScheme) -> Markup { + let theme = color_scheme.get_theme(); let css = format!(" html {{ @@ -325,7 +379,7 @@ fn css(color_scheme: &ColorScheme) -> Markup { font-weight: bold; color: {directory_link_color}; }} - a.file, a.file:visited {{ + a.file, a.file:visited, .error-back, .error-back:visited {{ color: {file_link_color}; }} a.symlink, a.symlink:visited {{ @@ -581,6 +635,22 @@ fn css(color_scheme: &ColorScheme) -> Markup { width: 100%; text-align: center; }} + .error {{ + margin: 2rem; + }} + .error p {{ + margin: 1rem 0; + font-size: 0.9rem; + word-break: break-all; + }} + .error p:first-of-type {{ + font-size: 1.25rem; + color: {error_color}; + margin-bottom: 2rem; + }} + .error-nav {{ + margin-top: 4rem; + }} @media (max-width: 760px) {{ nav {{ padding: 0 2.5rem; @@ -661,7 +731,8 @@ fn css(color_scheme: &ColorScheme) -> Markup { drag_border_color = theme.drag_border_color, drag_text_color = theme.drag_text_color, size_background_color = theme.size_background_color, - size_text_color = theme.size_text_color); + size_text_color = theme.size_text_color, + error_color = theme.error_color); (PreEscaped(css)) } @@ -686,7 +757,7 @@ fn chevron_down() -> Markup { } /// Partial: page header -fn page_header(serve_path: &str, color_scheme: &ColorScheme, file_upload: bool) -> Markup { +fn page_header(serve_path: &str, color_scheme: ColorScheme, file_upload: bool) -> Markup { html! { (DOCTYPE) html { @@ -694,7 +765,7 @@ fn page_header(serve_path: &str, color_scheme: &ColorScheme, file_upload: bool) meta http-equiv="X-UA-Compatible" content="IE=edge"; meta name="viewport" content="width=device-width, initial-scale=1"; title { "Index of " (serve_path) } - style { (css(&color_scheme)) } + style { (css(color_scheme)) } @if file_upload { (PreEscaped(r#" <script> @@ -761,11 +832,40 @@ fn humanize_systemtime(src_time: Option<SystemTime>) -> Option<String> { } /// Renders an error on the webpage -pub fn render_error(error_description: &str, return_address: &str) -> Markup { +pub fn render_error( + error_description: &str, + return_address: &str, + sort_method: Option<SortingMethod>, + sort_order: Option<SortingOrder>, + color_scheme: ColorScheme, + default_color_scheme: ColorScheme, + has_referer: bool, +) -> Markup { + let link = if has_referer { + return_address.to_string() + } else { + parametrized_link( + return_address, + sort_method, + sort_order, + color_scheme, + default_color_scheme, + ) + }; + html! { - pre { (error_description) } - a href=(return_address) { - "Go back to file listing" + body { + (page_header("Error", color_scheme, false)) + div.error { + @for error in error_description.lines() { + p { (error) } + } + div.error-nav { + a.error-back href=(link) { + "Go back to file listing" + } + } + } } } } diff --git a/src/themes.rs b/src/themes.rs index a7b619e..65e9ab2 100644 --- a/src/themes.rs +++ b/src/themes.rs @@ -3,7 +3,7 @@ use structopt::clap::arg_enum; use strum_macros::EnumIter; arg_enum! { - #[derive(PartialEq, Deserialize, Clone, EnumIter)] + #[derive(PartialEq, Deserialize, Clone, EnumIter, Copy)] #[serde(rename_all = "lowercase")] pub enum ColorScheme { Archlinux, @@ -17,8 +17,8 @@ impl ColorScheme { /// Returns the URL-compatible name of a color scheme /// This must correspond to the name of the variant, in lowercase /// See https://github.com/svenstaro/miniserve/pull/55 for explanations - pub fn to_slug(&self) -> String { - match &self { + pub fn to_slug(self) -> String { + match self { ColorScheme::Archlinux => "archlinux", ColorScheme::Zenburn => "zenburn", ColorScheme::Monokai => "monokai", @@ -28,8 +28,8 @@ impl ColorScheme { } /// Returns wether a color scheme is dark - pub fn is_dark(&self) -> bool { - match &self { + pub fn is_dark(self) -> bool { + match self { ColorScheme::Archlinux => true, ColorScheme::Zenburn => true, ColorScheme::Monokai => true, @@ -81,6 +81,7 @@ impl ColorScheme { drag_text_color: "#fefefe".to_string(), size_background_color: "#5294e2".to_string(), size_text_color: "#fefefe".to_string(), + error_color: "#e44b4b".to_string(), }, ColorScheme::Zenburn => Theme { background: "#3f3f3f".to_string(), @@ -123,6 +124,7 @@ impl ColorScheme { drag_text_color: "#efefef".to_string(), size_background_color: "#7f9f7f".to_string(), size_text_color: "#efefef".to_string(), + error_color: "#d06565".to_string(), }, ColorScheme::Monokai => Theme { background: "#272822".to_string(), @@ -165,6 +167,7 @@ impl ColorScheme { drag_text_color: "#F8F8F2".to_string(), size_background_color: "#75715E".to_string(), size_text_color: "#F8F8F2".to_string(), + error_color: "#d02929".to_string(), }, ColorScheme::Squirrel => Theme { background: "#FFFFFF".to_string(), @@ -207,6 +210,7 @@ impl ColorScheme { drag_text_color: "#ffffff".to_string(), size_background_color: "#323232".to_string(), size_text_color: "#FFFFFF".to_string(), + error_color: "#d02424".to_string(), }, } } @@ -254,4 +258,5 @@ pub struct Theme { pub drag_text_color: String, pub size_background_color: String, pub size_text_color: String, + pub error_color: String, } |