diff options
author | Sven-Hendrik Haase <svenstaro@gmail.com> | 2019-05-10 13:20:40 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-05-10 13:20:40 +0000 |
commit | 8d1e14464201e6d0655794c1a9c7f0222ff45707 (patch) | |
tree | 19bcaee5e8fe4045147d88b6e3f4585ada053c19 /src | |
parent | Merge pull request #111 from svenstaro/dependabot/cargo/tar-0.4.25 (diff) | |
parent | Undo changes on args.rs (diff) | |
download | miniserve-8d1e14464201e6d0655794c1a9c7f0222ff45707.tar.gz miniserve-8d1e14464201e6d0655794c1a9c7f0222ff45707.zip |
Merge pull request #90 from boastful-squirrel/themed-errors
Themed errors
Diffstat (limited to '')
-rw-r--r-- | src/args.rs | 1 | ||||
-rw-r--r-- | src/auth.rs | 74 | ||||
-rw-r--r-- | src/errors.rs | 12 | ||||
-rw-r--r-- | src/file_upload.rs | 120 | ||||
-rw-r--r-- | src/listing.rs | 90 | ||||
-rw-r--r-- | src/main.rs | 90 | ||||
-rw-r--r-- | src/renderer.rs | 220 | ||||
-rw-r--r-- | src/themes.rs | 15 |
8 files changed, 456 insertions, 166 deletions
diff --git a/src/args.rs b/src/args.rs index a67583a..d291bb9 100644 --- a/src/args.rs +++ b/src/args.rs @@ -164,6 +164,7 @@ pub fn parse_args() -> crate::MiniserveConfig { } } +#[rustfmt::skip] #[cfg(test)] mod tests { use super::*; diff --git a/src/auth.rs b/src/auth.rs index e526923..f2e5fcf 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,9 +1,10 @@ -use actix_web::http::header; +use actix_web::http::{header, StatusCode}; use actix_web::middleware::{Middleware, Response}; use actix_web::{HttpRequest, HttpResponse, Result}; use sha2::{Digest, Sha256, Sha512}; -use crate::errors::{ContextualError}; +use crate::errors::{self, ContextualError}; +use crate::renderer; pub struct Auth; @@ -36,10 +37,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)?; @@ -98,14 +96,25 @@ impl Middleware<crate::MiniserveConfig> for Auth { Ok(auth_req) => auth_req, Err(err) => { let auth_err = ContextualError::HTTPAuthenticationError(Box::new(err)); - return Ok(Response::Done( - HttpResponse::BadRequest().body(auth_err.to_string()), - )); + return Ok(Response::Done(HttpResponse::BadRequest().body( + build_unauthorized_response( + &req, + auth_err, + true, + StatusCode::BAD_REQUEST, + ), + ))); } }; if !match_auth(auth_req, required_auth) { - let new_resp = HttpResponse::Unauthorized().finish(); - return Ok(Response::Done(new_resp)); + return Ok(Response::Done(HttpResponse::Unauthorized().body( + build_unauthorized_response( + &req, + ContextualError::InvalidHTTPCredentials, + true, + StatusCode::UNAUTHORIZED, + ), + ))); } } else { let new_resp = HttpResponse::Unauthorized() @@ -113,7 +122,12 @@ impl Middleware<crate::MiniserveConfig> for Auth { header::WWW_AUTHENTICATE, header::HeaderValue::from_static("Basic realm=\"miniserve\""), ) - .finish(); + .body(build_unauthorized_response( + &req, + ContextualError::InvalidHTTPCredentials, + false, + StatusCode::UNAUTHORIZED, + )); return Ok(Response::Done(new_resp)); } } @@ -121,6 +135,40 @@ impl Middleware<crate::MiniserveConfig> for Auth { } } +/// Builds the unauthorized response body +/// The reason why log_error_chain is optional is to handle cases where the auth pop-up appears and when the user clicks Cancel. +/// In those case, we do not log the error to the terminal since it does not really matter. +fn build_unauthorized_response( + req: &HttpRequest<crate::MiniserveConfig>, + error: ContextualError, + log_error_chain: bool, + error_code: StatusCode, +) -> String { + let error = ContextualError::HTTPAuthenticationError(Box::new(error)); + + if log_error_chain { + errors::log_error_chain(error.to_string()); + } + let return_path = match &req.state().random_route { + Some(random_route) => format!("/{}", random_route), + None => "/".to_string(), + }; + + renderer::render_error( + &error.to_string(), + error_code, + &return_path, + None, + None, + req.state().default_color_scheme, + req.state().default_color_scheme, + false, + false, + ) + .into_string() +} + +#[rustfmt::skip] #[cfg(test)] mod tests { use super::*; @@ -169,7 +217,7 @@ mod tests { 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..68f6d7d 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -64,6 +64,18 @@ pub enum ContextualError { _0 )] HTTPAuthenticationError(Box<ContextualError>), + + /// This error might occur when the HTTP credentials are not correct + #[fail(display = "Invalid credentials for HTTP authentication")] + InvalidHTTPCredentials, + + /// This error might occur when an HTTP request is invalid + #[fail(display = "Invalid HTTP request\ncaused by: {}", _0)] + InvalidHTTPRequestError(String), + + /// This error might occur when trying to access a page that does not exist + #[fail(display = "Route {} could not be found", _0)] + RouteNotFoundError(String), } pub fn log_error_chain(description: String) { diff --git a/src/file_upload.rs b/src/file_upload.rs index 7f9cede..537c90c 100644 --- a/src/file_upload.rs +++ b/src/file_upload.rs @@ -1,9 +1,9 @@ use actix_web::{ - dev, http::header, multipart, FromRequest, FutureResponse, HttpMessage, HttpRequest, - HttpResponse, Query, + dev, + http::{header, StatusCode}, + multipart, FutureResponse, HttpMessage, HttpRequest, HttpResponse, }; use futures::{future, future::FutureResult, Future, Stream}; -use serde::Deserialize; use std::{ fs, io::Write, @@ -11,13 +11,9 @@ use std::{ }; use crate::errors::{self, ContextualError}; +use crate::listing::{self, 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 +31,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 +117,75 @@ 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) { - Ok(query) => { - if let Ok(stripped_path) = query.path.strip_prefix(Component::RootDir) { - stripped_path.to_owned() - } else { - query.path.clone() - } + + let query_params = listing::extract_query_parameters(req); + let color_scheme = query_params.theme.unwrap_or(default_color_scheme); + let upload_path = match query_params.path.clone() { + Some(path) => match path.strip_prefix(Component::RootDir) { + Ok(stripped_path) => stripped_path.to_owned(), + Err(_) => path.clone(), + }, + None => { + let err = ContextualError::InvalidHTTPRequestError( + "Missing query parameter 'path'".to_string(), + ); + return Box::new(create_error_response( + &err.to_string(), + StatusCode::BAD_REQUEST, + &return_path, + query_params.sort, + query_params.order, + color_scheme, + default_color_scheme, + )); } - Err(_) => { + }; + + 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( - "Unspecified parameter path", + &err.to_string(), + StatusCode::INTERNAL_SERVER_ERROR, &return_path, - )) + query_params.sort, + query_params.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() { + let target_dir = match &app_root_dir.clone().join(upload_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(), + StatusCode::BAD_REQUEST, + &return_path, + query_params.sort, + query_params.order, + color_scheme, + default_color_scheme, + )); + } }; let overwrite_files = req.state().overwrite_files; Box::new( @@ -166,7 +200,15 @@ 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(), + StatusCode::INTERNAL_SERVER_ERROR, + &return_path, + query_params.sort, + query_params.order, + color_scheme, + default_color_scheme, + ), }), ) } @@ -174,12 +216,30 @@ pub fn upload_file(req: &HttpRequest<crate::MiniserveConfig>) -> FutureResponse< /// Convenience method for creating response errors, if file upload fails. fn create_error_response( description: &str, + error_code: StatusCode, 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, + error_code, + return_path, + sorting_method, + sorting_order, + color_scheme, + default_color_scheme, + true, + true, + ) + .into_string(), + ), ) } diff --git a/src/listing.rs b/src/listing.rs index 87fd8a8..49802bc 100644 --- a/src/listing.rs +++ b/src/listing.rs @@ -1,3 +1,4 @@ +use actix_web::http::StatusCode; use actix_web::{fs, http, Body, FromRequest, HttpRequest, HttpResponse, Query, Result}; use bytesize::ByteSize; use futures::stream::once; @@ -5,26 +6,27 @@ 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}; -use crate::archive; -use crate::errors; +use crate::archive::{self, CompressionMethod}; +use crate::errors::{self, ContextualError}; use crate::renderer; -use crate::themes; +use crate::themes::ColorScheme; /// Query parameters #[derive(Deserialize)] -struct QueryParameters { - sort: Option<SortingMethod>, - order: Option<SortingOrder>, - download: Option<archive::CompressionMethod>, - theme: Option<themes::ColorScheme>, +pub struct QueryParameters { + pub path: Option<PathBuf>, + pub sort: Option<SortingMethod>, + pub order: Option<SortingOrder>, + pub theme: Option<ColorScheme>, + download: Option<CompressionMethod>, } /// 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 +41,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")] @@ -130,7 +132,7 @@ pub fn directory_listing<S>( skip_symlinks: bool, file_upload: bool, random_route: Option<String>, - default_color_scheme: themes::ColorScheme, + default_color_scheme: ColorScheme, upload_route: String, ) -> Result<HttpResponse, io::Error> { let serve_path = req.path(); @@ -143,23 +145,13 @@ pub fn directory_listing<S>( Err(_) => base.to_path_buf(), }; - 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(), - ) - } else { - (None, None, None, None) - }; + let query_params = extract_query_parameters(req); let mut entries: Vec<Entry> = Vec::new(); 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, @@ -211,7 +203,7 @@ pub fn directory_listing<S>( } } - if let Some(sorting_method) = &sort_method { + if let Some(sorting_method) = query_params.sort { match sorting_method { SortingMethod::Name => entries .sort_by(|e1, e2| alphanumeric_sort::compare_str(e1.name.clone(), e2.name.clone())), @@ -235,15 +227,15 @@ 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) = query_params.order { if let SortingOrder::Descending = sorting_order { entries.reverse() } } - let color_scheme = color_scheme.unwrap_or_else(|| default_color_scheme.clone()); + let color_scheme = query_params.theme.unwrap_or(default_color_scheme); - if let Some(compression_method) = &download { + if let Some(compression_method) = &query_params.download { log::info!( "Creating an archive ({extension}) of {path}...", extension = compression_method.extension(), @@ -267,7 +259,20 @@ 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(), + StatusCode::INTERNAL_SERVER_ERROR, + serve_path, + query_params.sort, + query_params.order, + color_scheme, + default_color_scheme, + false, + true, + ) + .into_string(), + )) } } } else { @@ -279,8 +284,8 @@ pub fn directory_listing<S>( entries, is_root, page_parent, - sort_method, - sort_order, + query_params.sort, + query_params.order, default_color_scheme, color_scheme, file_upload, @@ -291,3 +296,26 @@ pub fn directory_listing<S>( )) } } + +pub fn extract_query_parameters<S>(req: &HttpRequest<S>) -> QueryParameters { + match Query::<QueryParameters>::extract(req) { + Ok(query) => QueryParameters { + sort: query.sort, + order: query.order, + download: query.download.clone(), + theme: query.theme, + path: query.path.clone(), + }, + Err(e) => { + let err = ContextualError::ParseError("query parameters".to_string(), e.to_string()); + errors::log_error_chain(err.to_string()); + QueryParameters { + sort: None, + order: None, + download: None, + theme: None, + path: None, + } + } + } +} diff --git a/src/main.rs b/src/main.rs index ea58fc6..bcfda09 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ #![feature(proc_macro_hygiene)] -use actix_web::http::Method; -use actix_web::{fs, middleware, server, App}; +use actix_web::http::{Method, StatusCode}; +use actix_web::{fs, middleware, server, App, HttpRequest, HttpResponse}; use clap::crate_version; use simplelog::{Config, LevelFilter, TermLogger}; use std::io::{self, Write}; @@ -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 @@ -83,12 +83,9 @@ fn run() -> Result<(), ContextualError> { && miniserve_config .path .symlink_metadata() - .map_err(|e| - ContextualError::IOError( - "Failed to retrieve symlink's metadata".to_string(), - e, - ) - )? + .map_err(|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,10 +244,11 @@ 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(), ) - }), + }) + .default_handler(error_404), ) } }; @@ -274,18 +258,52 @@ 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) + .default_resource(|r| r.method(Method::GET).f(error_404)) } else { // Handle directories app.handler(&full_route, s) + .default_resource(|r| r.method(Method::GET).f(error_404)) } } else { // Handle single files app.resource(&full_route, |r| r.f(listing::file_handler)) + .default_resource(|r| r.method(Method::GET).f(error_404)) } } + +fn error_404(req: &HttpRequest<crate::MiniserveConfig>) -> Result<HttpResponse, io::Error> { + let err_404 = ContextualError::RouteNotFoundError(req.path().to_string()); + let default_color_scheme = req.state().default_color_scheme; + let return_address = match &req.state().random_route { + Some(random_route) => format!("/{}", random_route), + None => "/".to_string(), + }; + + let query_params = listing::extract_query_parameters(req); + let color_scheme = query_params.theme.unwrap_or(default_color_scheme); + + errors::log_error_chain(err_404.to_string()); + + Ok(actix_web::HttpResponse::NotFound().body( + renderer::render_error( + &err_404.to_string(), + StatusCode::NOT_FOUND, + &return_address, + query_params.sort, + query_params.order, + color_scheme, + default_color_scheme, + false, + true, + ) + .into_string(), + )) +} diff --git a/src/renderer.rs b/src/renderer.rs index 69224cf..098cf91 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,3 +1,4 @@ +use actix_web::http::StatusCode; use chrono::{DateTime, Duration, Utc}; use chrono_humanize::{Accuracy, HumanTime, Tense}; use maud::{html, Markup, PreEscaped, DOCTYPE}; @@ -23,8 +24,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, false)) body#drop-container { @if file_upload { div.drag-form { @@ -33,19 +43,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="" {} @@ -57,9 +67,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 { @@ -67,7 +77,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" } } @@ -75,7 +85,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)) } } } @@ -87,12 +97,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! { @@ -104,13 +137,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)) } } } @@ -123,18 +156,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); @@ -152,8 +185,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! { @@ -166,10 +221,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 { @@ -194,10 +249,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); @@ -232,17 +287,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() { @@ -257,7 +312,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 { "⇢" } } } @@ -287,8 +342,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 {{ @@ -326,7 +381,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 {{ @@ -582,6 +637,25 @@ 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 p:nth-of-type(2) {{ + font-weight: bold; + }} + .error-nav {{ + margin-top: 4rem; + }} @media (max-width: 760px) {{ nav {{ padding: 0 2.5rem; @@ -662,7 +736,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)) } @@ -687,15 +762,24 @@ 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, + is_error: bool, +) -> Markup { html! { (DOCTYPE) html { meta charset="utf-8"; 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)) } + @if is_error { + title { (serve_path) } + } else { + title { "Index of " (serve_path) } + } + style { (css(color_scheme)) } @if file_upload { (PreEscaped(r#" <script> @@ -762,11 +846,45 @@ 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, + error_code: StatusCode, + return_address: &str, + sort_method: Option<SortingMethod>, + sort_order: Option<SortingOrder>, + color_scheme: ColorScheme, + default_color_scheme: ColorScheme, + has_referer: bool, + display_back_link: 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_code.to_string(), color_scheme, false, true)) + div.error { + p { (error_code.to_string()) } + @for error in error_description.lines() { + p { (error) } + } + @if display_back_link { + 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, } |