aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSven-Hendrik Haase <svenstaro@gmail.com>2019-05-10 13:20:40 +0000
committerGitHub <noreply@github.com>2019-05-10 13:20:40 +0000
commit8d1e14464201e6d0655794c1a9c7f0222ff45707 (patch)
tree19bcaee5e8fe4045147d88b6e3f4585ada053c19
parentMerge pull request #111 from svenstaro/dependabot/cargo/tar-0.4.25 (diff)
parentUndo changes on args.rs (diff)
downloadminiserve-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.rs1
-rw-r--r--src/auth.rs74
-rw-r--r--src/errors.rs12
-rw-r--r--src/file_upload.rs120
-rw-r--r--src/listing.rs90
-rw-r--r--src/main.rs90
-rw-r--r--src/renderer.rs220
-rw-r--r--src/themes.rs15
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,
}