From 1a796655f56c775c82902a9897102bdf2db93c2c Mon Sep 17 00:00:00 2001 From: boastful-squirrel Date: Sat, 20 Apr 2019 22:56:44 +0200 Subject: Set upload input as required --- src/renderer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/renderer.rs b/src/renderer.rs index c1dcdb2..06f7bec 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -47,7 +47,7 @@ pub fn page( form id="file_submit" action={(upload_route) "?path=" (current_dir)} 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" {} + input#file-input type="file" name="file_to_upload" required="" {} button type="submit" { "Upload file" } } } -- cgit v1.2.3 From ddf37da5e528f52094fcf285b6c250fe4aacfda2 Mon Sep 17 00:00:00 2001 From: boastful-squirrel Date: Sun, 21 Apr 2019 15:52:20 +0200 Subject: Improved upload error display --- src/renderer.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'src') diff --git a/src/renderer.rs b/src/renderer.rs index 06f7bec..be7a51f 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -763,8 +763,7 @@ fn humanize_systemtime(src_time: Option) -> Option { /// Renders error page when file uploading fails pub fn file_upload_error(error_description: &str, return_address: &str) -> Markup { html! { - h1 { "File uploading failed" } - p { (error_description) } + pre { (error_description) } a href=(return_address) { "Go back to file listing" } -- cgit v1.2.3 From 3234ba71d2f5a7aa30065fb9e74216a09fce1464 Mon Sep 17 00:00:00 2001 From: boastful-squirrel Date: Sun, 21 Apr 2019 16:00:47 +0200 Subject: Print error on the web page rather than on the terminal --- src/listing.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'src') diff --git a/src/listing.rs b/src/listing.rs index 6aa1eac..ccb188c 100644 --- a/src/listing.rs +++ b/src/listing.rs @@ -10,7 +10,6 @@ use std::time::SystemTime; use strum_macros::{Display, EnumString}; use crate::archive; -use crate::errors; use crate::renderer; use crate::themes; @@ -263,10 +262,9 @@ pub fn directory_listing( .body(Body::Streaming(Box::new(once(Ok(content)))))) } Err(err) => { - errors::print_error_chain(err); Ok(HttpResponse::Ok() .status(http::StatusCode::INTERNAL_SERVER_ERROR) - .body("")) + .body(err.to_string())) } } } else { -- cgit v1.2.3 From 6df18a5f6724b5ca010424b4dbf748a7bc78ef4e Mon Sep 17 00:00:00 2001 From: boastful-squirrel Date: Sun, 21 Apr 2019 16:14:22 +0200 Subject: cargo fmt --- src/listing.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/listing.rs b/src/listing.rs index ccb188c..a9e4f5f 100644 --- a/src/listing.rs +++ b/src/listing.rs @@ -261,11 +261,9 @@ pub fn directory_listing( .chunked() .body(Body::Streaming(Box::new(once(Ok(content)))))) } - Err(err) => { - Ok(HttpResponse::Ok() - .status(http::StatusCode::INTERNAL_SERVER_ERROR) - .body(err.to_string())) - } + Err(err) => Ok(HttpResponse::Ok() + .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .body(err.to_string())), } } else { Ok(HttpResponse::Ok() -- cgit v1.2.3 From 8af3ff10e2347da70c35eb45046f8a04843f7256 Mon Sep 17 00:00:00 2001 From: boastful-squirrel Date: Sun, 21 Apr 2019 16:27:34 +0200 Subject: Rework error system + avoid panics in main() --- src/archive.rs | 106 ++++++++++++++++++++++------------------- src/args.rs | 15 +++--- src/auth.rs | 29 +++++++----- src/errors.rs | 135 +++++++++++++++++++++++++---------------------------- src/file_upload.rs | 57 +++++++++++++++------- src/main.rs | 87 ++++++++++++++++++++++++++-------- 6 files changed, 251 insertions(+), 178 deletions(-) (limited to 'src') diff --git a/src/archive.rs b/src/archive.rs index 4703c0d..00d2901 100644 --- a/src/archive.rs +++ b/src/archive.rs @@ -1,6 +1,5 @@ use actix_web::http::ContentEncoding; use bytes::Bytes; -use failure::ResultExt; use libflate::gzip::Encoder; use serde::Deserialize; use std::io; @@ -8,7 +7,7 @@ use std::path::PathBuf; use strum_macros::{Display, EnumIter, EnumString}; use tar::Builder; -use crate::errors; +use crate::errors::{ContextualError, ContextualErrorKind}; /// Available compression methods #[derive(Deserialize, Clone, EnumIter, EnumString, Display)] @@ -47,45 +46,47 @@ pub fn create_archive( method: &CompressionMethod, dir: &PathBuf, skip_symlinks: bool, -) -> Result<(String, Bytes), errors::CompressionError> { +) -> Result<(String, Bytes), ContextualError> { match method { CompressionMethod::TarGz => tgz_compress(&dir, skip_symlinks), } } /// Compresses a given folder in .tar.gz format, and returns the result as a stream of bytes -fn tgz_compress( - dir: &PathBuf, - skip_symlinks: bool, -) -> Result<(String, Bytes), errors::CompressionError> { +fn tgz_compress(dir: &PathBuf, skip_symlinks: bool) -> Result<(String, Bytes), ContextualError> { let src_dir = dir.display().to_string(); let inner_folder = match dir.file_name() { Some(directory_name) => match directory_name.to_str() { Some(directory) => directory, None => { - return Err(errors::CompressionError::new( - errors::CompressionErrorKind::InvalidUTF8DirectoryName, - )) + // https://doc.rust-lang.org/std/ffi/struct.OsStr.html#method.to_str + return Err(ContextualError::new(ContextualErrorKind::InvalidPathError( + "Directory name contains invalid UTF-8 characters".to_string(), + ))); } }, None => { - return Err(errors::CompressionError::new( - errors::CompressionErrorKind::InvalidDirectoryName, - )) + // https://doc.rust-lang.org/std/path/struct.Path.html#method.file_name + return Err(ContextualError::new(ContextualErrorKind::InvalidPathError( + "Directory name terminates in \"..\"".to_string(), + ))); } }; let dst_filename = format!("{}.tar", inner_folder); let dst_tgz_filename = format!("{}.gz", dst_filename); - let tar_content = tar(src_dir, inner_folder.to_string(), skip_symlinks).context( - errors::CompressionErrorKind::TarBuildingError { - message: "an error occured while writing the TAR archive".to_string(), - }, - )?; - let gz_data = gzip(&tar_content).context(errors::CompressionErrorKind::GZipBuildingError { - message: "an error occured while writing the GZIP archive".to_string(), + let tar_content = tar(src_dir, inner_folder.to_string(), skip_symlinks).map_err(|e| { + ContextualError::new(ContextualErrorKind::ArchiveCreationError( + "tarball".to_string(), + Box::new(e), + )) + })?; + let gz_data = gzip(&tar_content).map_err(|e| { + ContextualError::new(ContextualErrorKind::ArchiveCreationError( + "GZIP archive".to_string(), + Box::new(e), + )) })?; - let mut data = Bytes::new(); data.extend_from_slice(&gz_data); @@ -97,44 +98,53 @@ fn tar( src_dir: String, inner_folder: String, skip_symlinks: bool, -) -> Result, errors::CompressionError> { +) -> Result, ContextualError> { let mut tar_builder = Builder::new(Vec::new()); tar_builder.follow_symlinks(!skip_symlinks); // Recursively adds the content of src_dir into the archive stream - tar_builder.append_dir_all(inner_folder, &src_dir).context( - errors::CompressionErrorKind::TarBuildingError { - message: format!( - "failed to append the content of {} to the TAR archive", - &src_dir - ), - }, - )?; + tar_builder + .append_dir_all(inner_folder, &src_dir) + .map_err(|e| { + ContextualError::new(ContextualErrorKind::IOError( + format!( + "Failed to append the content of {} to the TAR archive", + &src_dir + ), + e, + )) + })?; - let tar_content = - tar_builder - .into_inner() - .context(errors::CompressionErrorKind::TarBuildingError { - message: "failed to finish writing the TAR archive".to_string(), - })?; + let tar_content = tar_builder.into_inner().map_err(|e| { + ContextualError::new(ContextualErrorKind::IOError( + "Failed to finish writing the TAR archive".to_string(), + e, + )) + })?; Ok(tar_content) } /// Compresses a stream of bytes using the GZIP algorithm, and returns the resulting stream -fn gzip(mut data: &[u8]) -> Result, errors::CompressionError> { - let mut encoder = - Encoder::new(Vec::new()).context(errors::CompressionErrorKind::GZipBuildingError { - message: "failed to create GZIP encoder".to_string(), - })?; - io::copy(&mut data, &mut encoder).context(errors::CompressionErrorKind::GZipBuildingError { - message: "failed to write GZIP data".to_string(), +fn gzip(mut data: &[u8]) -> Result, ContextualError> { + let mut encoder = Encoder::new(Vec::new()).map_err(|e| { + ContextualError::new(ContextualErrorKind::IOError( + "Failed to create GZIP encoder".to_string(), + e, + )) + })?; + io::copy(&mut data, &mut encoder).map_err(|e| { + ContextualError::new(ContextualErrorKind::IOError( + "Failed to write GZIP data".to_string(), + e, + )) + })?; + let data = encoder.finish().into_result().map_err(|e| { + ContextualError::new(ContextualErrorKind::IOError( + "Failed to write GZIP trailer".to_string(), + e, + )) })?; - let data = encoder.finish().into_result().context( - errors::CompressionErrorKind::GZipBuildingError { - message: "failed to write GZIP trailer".to_string(), - }, - )?; Ok(data) } diff --git a/src/args.rs b/src/args.rs index 825a4ac..8d2e105 100644 --- a/src/args.rs +++ b/src/args.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; use structopt::StructOpt; use crate::auth; +use crate::errors::{ContextualError, ContextualErrorKind}; use crate::themes; /// Possible characters for random routes @@ -76,15 +77,13 @@ fn parse_interface(src: &str) -> Result { } /// Checks wether the auth string is valid, i.e. it follows the syntax username:password -fn parse_auth(src: &str) -> Result<(String, String), String> { +fn parse_auth(src: &str) -> Result<(String, String), ContextualError> { let mut split = src.splitn(2, ':'); let username = match split.next() { Some(username) => username, None => { - return Err( - "Invalid credentials string, expected format is username:password".to_owned(), - ) + return Err(ContextualError::new(ContextualErrorKind::InvalidAuthFormat)); } }; @@ -92,9 +91,7 @@ fn parse_auth(src: &str) -> Result<(String, String), String> { // This allows empty passwords, as the spec does not forbid it Some(password) => password, None => { - return Err( - "Invalid credentials string, expected format is username:password".to_owned(), - ) + return Err(ContextualError::new(ContextualErrorKind::InvalidAuthFormat)); } }; @@ -102,7 +99,9 @@ fn parse_auth(src: &str) -> Result<(String, String), String> { // After 255 characters, Windows will truncate the value. // As for the username, the spec does not mention a limit in length if password.len() > 255 { - return Err("Password length cannot exceed 255 characters".to_owned()); + return Err(ContextualError::new( + ContextualErrorKind::PasswordTooLongError, + )); } Ok((username.to_owned(), password.to_owned())) diff --git a/src/auth.rs b/src/auth.rs index 10e7a4a..8cedaae 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -2,12 +2,9 @@ use actix_web::http::header; use actix_web::middleware::{Middleware, Response}; use actix_web::{HttpRequest, HttpResponse, Result}; -pub struct Auth; +use crate::errors::{ContextualError, ContextualErrorKind}; -/// HTTP Basic authentication errors -pub enum BasicAuthError { - Base64DecodeError, -} +pub struct Auth; #[derive(Clone, Debug)] /// HTTP Basic authentication parameters @@ -19,9 +16,17 @@ pub struct BasicAuthParams { /// Decode a HTTP basic auth string into a tuple of username and password. pub fn parse_basic_auth( authorization_header: &header::HeaderValue, -) -> Result { - let basic_removed = authorization_header.to_str().unwrap().replace("Basic ", ""); - let decoded = base64::decode(&basic_removed).map_err(|_| BasicAuthError::Base64DecodeError)?; +) -> Result { + let basic_removed = authorization_header + .to_str() + .map_err(|e| { + ContextualError::new(ContextualErrorKind::ParseError( + "HTTP authentication header".to_string(), + e.to_string(), + )) + })? + .replace("Basic ", ""); + let decoded = base64::decode(&basic_removed).map_err(ContextualErrorKind::Base64DecodeError)?; let decoded_str = String::from_utf8_lossy(&decoded); let credentials: Vec<&str> = decoded_str.splitn(2, ':').collect(); @@ -44,11 +49,11 @@ impl Middleware for Auth { if let Some(auth_headers) = req.headers().get(header::AUTHORIZATION) { let auth_req = match parse_basic_auth(auth_headers) { Ok(auth_req) => auth_req, - Err(BasicAuthError::Base64DecodeError) => { + Err(err) => { return Ok(Response::Done(HttpResponse::BadRequest().body(format!( - "Error decoding basic auth base64: '{}'", - auth_headers.to_str().unwrap() - )))); + "An error occured during HTTP authentication\ncaused by: {}", + err + )))) } }; if auth_req.username != required_auth.username diff --git a/src/errors.rs b/src/errors.rs index 21d9e07..f2d185e 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,85 +1,69 @@ use failure::{Backtrace, Context, Fail}; use std::fmt::{self, Debug, Display}; -/// Kinds of errors which might happen during file upload #[derive(Debug, Fail)] -pub enum FileUploadErrorKind { - /// This error will occur when file overriding is off and a file with same name already exists - #[fail(display = "File with this name already exists")] - FileExist, - /// This error will occur when the server fails to process the HTTP header during file upload - #[fail(display = "Failed to parse incoming request")] - ParseError, - /// This error will occur when we fail to process the multipart request - #[fail(display = "Failed to process multipart request")] +pub enum ContextualErrorKind { + /// Fully customized errors, not inheriting from any error + #[fail(display = "{}", _0)] + CustomError(String), + + /// Any kind of IO errors + #[fail(display = "{}\ncaused by: {}", _0, _1)] + IOError(String, std::io::Error), + + /// MultipartError, which might occur during file upload, when processing the multipart request fails + #[fail(display = "Failed to process multipart request\ncaused by: {}", _0)] MultipartError(actix_web::error::MultipartError), - /// This error may occur when trying to write the incoming file to disk - #[fail(display = "Failed to create or write to file")] - IOError(std::io::Error), - /// This error will occur when we he have insuffictent permissions to create new file - #[fail(display = "Insufficient permissions to create file")] - InsufficientPermissions, -} -/// Kinds of errors which might happen during the generation of an archive -#[derive(Debug, Fail)] -pub enum CompressionErrorKind { - /// This error will occur if the directory name could not be retrieved from the path - /// See https://doc.rust-lang.org/std/path/struct.Path.html#method.file_name - #[fail(display = "Invalid path: directory name terminates in \"..\"")] - InvalidDirectoryName, - /// This error will occur when trying to convert an OSString into a String, if the path - /// contains invalid UTF-8 characters - /// See https://doc.rust-lang.org/std/ffi/struct.OsStr.html#method.to_str - #[fail(display = "Invalid path: directory name contains invalid UTF-8 characters")] - InvalidUTF8DirectoryName, - /// This error might occur while building a TAR archive, or while writing the termination sections - /// See https://docs.rs/tar/0.4.22/tar/struct.Builder.html#method.append_dir_all - /// and https://docs.rs/tar/0.4.22/tar/struct.Builder.html#method.into_inner - #[fail(display = "Failed to create the TAR archive: {}", message)] - TarBuildingError { message: String }, - /// This error might occur while building a GZIP archive, or while writing the GZIP trailer - /// See https://docs.rs/libflate/0.1.21/libflate/gzip/struct.Encoder.html#method.finish - #[fail(display = "Failed to create the GZIP archive: {}", message)] - GZipBuildingError { message: String }, -} + /// This error might occur when decoding the HTTP authentication header. + #[fail( + display = "Failed to decode HTTP authentication header\ncaused by: {}", + _0 + )] + Base64DecodeError(base64::DecodeError), -/// Prints the full chain of error, up to the root cause. -/// If RUST_BACKTRACE is set to 1, also prints the backtrace for each error -pub fn print_error_chain(err: CompressionError) { - log::error!("{}", &err); - print_backtrace(&err); - for cause in Fail::iter_causes(&err) { - log::error!("caused by: {}", cause); - print_backtrace(cause); - } -} + /// Any error related to an invalid path (failed to retrieve entry name, unexpected entry type, etc) + #[fail(display = "Invalid path\ncaused by: {}", _0)] + InvalidPathError(String), -/// Prints the backtrace of an error -/// RUST_BACKTRACE needs to be set to 1 to display the backtrace -fn print_backtrace(err: &dyn Fail) { - if let Some(backtrace) = err.backtrace() { - let backtrace = backtrace.to_string(); - if backtrace != "" { - log::error!("{}", backtrace); - } - } + /// This error might occur if the HTTP credential string does not respect the expected format + #[fail(display = "Invalid format for credentials string. Expected is username:format")] + InvalidAuthFormat, + + /// This error might occur if the HTTP auth password exceeds 255 characters + #[fail(display = "HTTP password length exceeds 255 characters")] + PasswordTooLongError, + + /// This error might occur if the user has unsufficient permissions to create an entry in a given directory + #[fail(display = "Insufficient permissions to create file in {}", _0)] + InsufficientPermissionsError(String), + + /// Any error related to parsing. + #[fail(display = "Failed to parse {}\ncaused by: {}", _0, _1)] + ParseError(String, String), + + /// This error might occur when the creation of an archive fails + #[fail( + display = "An error occured while creating the {}\ncaused by: {}", + _0, _1 + )] + ArchiveCreationError(String, Box), } /// Based on https://boats.gitlab.io/failure/error-errorkind.html -pub struct CompressionError { - inner: Context, +pub struct ContextualError { + inner: Context, } -impl CompressionError { - pub fn new(kind: CompressionErrorKind) -> CompressionError { - CompressionError { +impl ContextualError { + pub fn new(kind: ContextualErrorKind) -> ContextualError { + ContextualError { inner: Context::new(kind), } } } -impl Fail for CompressionError { +impl Fail for ContextualError { fn cause(&self) -> Option<&Fail> { self.inner.cause() } @@ -89,28 +73,35 @@ impl Fail for CompressionError { } } -impl Display for CompressionError { +impl Display for ContextualError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Display::fmt(&self.inner, f) } } -impl Debug for CompressionError { +impl Debug for ContextualError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Debug::fmt(&self.inner, f) } } -impl From> for CompressionError { - fn from(inner: Context) -> CompressionError { - CompressionError { inner } +impl From> for ContextualError { + fn from(inner: Context) -> ContextualError { + ContextualError { inner } } } -impl From for CompressionError { - fn from(kind: CompressionErrorKind) -> CompressionError { - CompressionError { +impl From for ContextualError { + fn from(kind: ContextualErrorKind) -> ContextualError { + ContextualError { inner: Context::new(kind), } } } + +/// This allows to create CustomErrors more simply +impl From for ContextualError { + fn from(msg: String) -> ContextualError { + ContextualError::new(ContextualErrorKind::CustomError(msg)) + } +} diff --git a/src/file_upload.rs b/src/file_upload.rs index 534083c..960f831 100644 --- a/src/file_upload.rs +++ b/src/file_upload.rs @@ -1,5 +1,3 @@ -use crate::errors::FileUploadErrorKind; -use crate::renderer::file_upload_error; use actix_web::{ dev, http::header, multipart, FromRequest, FutureResponse, HttpMessage, HttpRequest, HttpResponse, Query, @@ -12,6 +10,9 @@ use std::{ path::{Component, PathBuf}, }; +use crate::errors::ContextualErrorKind; +use crate::renderer::file_upload_error; + /// Query parameters #[derive(Debug, Deserialize)] struct QueryParameters { @@ -23,24 +24,32 @@ fn save_file( field: multipart::Field, file_path: PathBuf, overwrite_files: bool, -) -> Box> { +) -> Box> { if !overwrite_files && file_path.exists() { - return Box::new(future::err(FileUploadErrorKind::FileExist)); + return Box::new(future::err(ContextualErrorKind::CustomError( + "File already exists, and the overwrite_files option has not been set".to_string(), + ))); } + let mut file = match std::fs::File::create(file_path) { Ok(file) => file, Err(e) => { - return Box::new(future::err(FileUploadErrorKind::IOError(e))); + return Box::new(future::err(ContextualErrorKind::IOError( + "Failed to create file".to_string(), + e, + ))); } }; Box::new( field - .map_err(FileUploadErrorKind::MultipartError) + .map_err(ContextualErrorKind::MultipartError) .fold(0i64, move |acc, bytes| { let rt = file .write_all(bytes.as_ref()) .map(|_| acc + bytes.len() as i64) - .map_err(FileUploadErrorKind::IOError); + .map_err(|e| { + ContextualErrorKind::IOError("Failed to write to file".to_string(), e) + }); future::result(rt) }), ) @@ -51,44 +60,56 @@ fn handle_multipart( item: multipart::MultipartItem, mut file_path: PathBuf, overwrite_files: bool, -) -> Box> { +) -> Box> { match item { multipart::MultipartItem::Field(field) => { let filename = field .headers() .get(header::CONTENT_DISPOSITION) - .ok_or(FileUploadErrorKind::ParseError) + .ok_or(ContextualErrorKind::ParseError) .and_then(|cd| { header::ContentDisposition::from_raw(cd) - .map_err(|_| FileUploadErrorKind::ParseError) + .map_err(|_| ContextualErrorKind::ParseError) }) .and_then(|content_disposition| { content_disposition .get_filename() - .ok_or(FileUploadErrorKind::ParseError) + .ok_or(ContextualErrorKind::ParseError) .map(String::from) }); - let err = |e: FileUploadErrorKind| Box::new(future::err(e).into_stream()); + let err = |e: ContextualErrorKind| Box::new(future::err(e).into_stream()); match filename { Ok(f) => { match fs::metadata(&file_path) { Ok(metadata) => { - if !metadata.is_dir() || metadata.permissions().readonly() { - return err(FileUploadErrorKind::InsufficientPermissions); + if !metadata.is_dir() { + return err(ContextualErrorKind::InvalidPathError(format!( + "cannot upload file to {}, since it's not a directory", + &file_path.display() + ))); + } else if metadata.permissions().readonly() { + return err(ContextualErrorKind::InsufficientPermissionsError( + file_path.display().to_string(), + )); } } Err(_) => { - return err(FileUploadErrorKind::InsufficientPermissions); + return err(ContextualErrorKind::InsufficientPermissionsError( + file_path.display().to_string(), + )); } } file_path = file_path.join(f); Box::new(save_file(field, file_path, overwrite_files).into_stream()) } - Err(e) => err(e), + Err(e) => err(e( + "HTTP header".to_string(), + "Failed to retrieve the name of the file to upload".to_string(), + )), } } multipart::MultipartItem::Nested(mp) => Box::new( - mp.map_err(FileUploadErrorKind::MultipartError) + mp.map_err(ContextualErrorKind::MultipartError) .map(move |item| handle_multipart(item, file_path.clone(), overwrite_files)) .flatten(), ), @@ -134,7 +155,7 @@ pub fn upload_file(req: &HttpRequest) -> FutureResponse< let overwrite_files = req.state().overwrite_files; Box::new( req.multipart() - .map_err(FileUploadErrorKind::MultipartError) + .map_err(ContextualErrorKind::MultipartError) .map(move |item| handle_multipart(item, target_dir.clone(), overwrite_files)) .flatten() .collect() diff --git a/src/main.rs b/src/main.rs index 42a43b5..fec3913 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,8 @@ mod listing; mod renderer; mod themes; +use crate::errors::{ContextualError, ContextualErrorKind}; + #[derive(Clone)] /// Configuration of the Miniserve application pub struct MiniserveConfig { @@ -57,10 +59,18 @@ pub struct MiniserveConfig { } fn main() { + match run() { + Ok(()) => (), + Err(e) => eprintln!("\n{}\n", Paint::red(e)), + } +} + +fn run() -> Result<(), ContextualError> { if cfg!(windows) && !Paint::enable_windows_ascii() { Paint::disable(); } + let sys = actix::System::new("miniserve"); let miniserve_config = args::parse_args(); let _ = if miniserve_config.verbose { @@ -73,16 +83,20 @@ fn main() { && miniserve_config .path .symlink_metadata() - .expect("Can't get file metadata") + .map_err(|e| { + ContextualError::new(ContextualErrorKind::IOError( + "Failed to retrieve symlink's metadata".to_string(), + e, + )) + })? .file_type() .is_symlink() { - log::error!("The no-symlinks option cannot be used with a symlink path"); - return; + return Err(ContextualError::from( + "The no-symlinks option cannot be used with a symlink path".to_string(), + )); } - let sys = actix::System::new("miniserve"); - let inside_config = miniserve_config.clone(); let interfaces = miniserve_config @@ -102,7 +116,12 @@ fn main() { }) .collect::>(); - let canon_path = miniserve_config.path.canonicalize().unwrap(); + let canon_path = miniserve_config.path.canonicalize().map_err(|e| { + ContextualError::new(ContextualErrorKind::IOError( + "Failed to resolve path to be served".to_string(), + e, + )) + })?; let path_string = canon_path.to_string_lossy(); println!( @@ -116,10 +135,20 @@ fn main() { " Invoke with -h|--help to see options or invoke as `miniserve .` to hide this advice." ); print!("Starting server in "); - io::stdout().flush().unwrap(); + io::stdout().flush().map_err(|e| { + ContextualError::new(ContextualErrorKind::IOError( + "Failed to write data".to_string(), + e, + )) + })?; for c in "3… 2… 1… \n".chars() { print!("{}", c); - io::stdout().flush().unwrap(); + io::stdout().flush().map_err(|e| { + ContextualError::new(ContextualErrorKind::IOError( + "Failed to write data".to_string(), + e, + )) + })?; thread::sleep(Duration::from_millis(500)); } } @@ -148,12 +177,6 @@ fn main() { )); } } - println!( - "Serving path {path} at {addresses}", - path = Color::Yellow.paint(path_string).bold(), - addresses = addresses, - ); - println!("\nQuit by pressing CTRL-C"); let socket_addresses = interfaces .iter() @@ -167,10 +190,18 @@ fn main() { }) .collect::, _>>(); - // Note that this should never fail, since CLI parsing succeeded - // This means the format of each IP address is valid, and so is the port - // Valid IpAddr + valid port == valid SocketAddr - let socket_addresses = socket_addresses.expect("Failed to parse string as socket address"); + let socket_addresses = match socket_addresses { + Ok(addresses) => addresses, + Err(e) => { + // Note that this should never fail, since CLI parsing succeeded + // This means the format of each IP address is valid, and so is the port + // Valid IpAddr + valid port == valid SocketAddr + return Err(ContextualError::new(ContextualErrorKind::ParseError( + "string as socket address".to_string(), + e.to_string(), + ))); + } + }; server::new(move || { App::with_state(inside_config.clone()) @@ -179,10 +210,26 @@ fn main() { .configure(configure_app) }) .bind(socket_addresses.as_slice()) - .expect("Couldn't bind server") + .map_err(|e| { + ContextualError::new(ContextualErrorKind::IOError( + "Failed to bind server".to_string(), + e, + )) + })? .shutdown_timeout(0) .start(); + + println!( + "Serving path {path} at {addresses}", + path = Color::Yellow.paint(path_string).bold(), + addresses = addresses, + ); + + println!("\nQuit by pressing CTRL-C"); + let _ = sys.run(); + + Ok(()) } /// Configures the Actix application @@ -204,7 +251,7 @@ fn configure_app(app: App) -> App { let u_r = upload_route.clone(); Some( fs::StaticFiles::new(path) - .expect("Couldn't create path") + .expect("Failed to setup static file handler") .show_files_listing() .files_listing_renderer(move |dir, req| { listing::directory_listing( -- cgit v1.2.3 From 23567f3b73ef309ba9afda51e084751b64942c53 Mon Sep 17 00:00:00 2001 From: boastful-squirrel Date: Sun, 21 Apr 2019 19:21:59 +0200 Subject: Print upload/archive errors also in terminal --- src/file_upload.rs | 1 + src/listing.rs | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/file_upload.rs b/src/file_upload.rs index 960f831..54c56dd 100644 --- a/src/file_upload.rs +++ b/src/file_upload.rs @@ -175,6 +175,7 @@ fn create_error_response( description: &str, return_path: &str, ) -> FutureResult { + log::error!("{}", description); future::ok( HttpResponse::BadRequest() .content_type("text/html; charset=utf-8") diff --git a/src/listing.rs b/src/listing.rs index a9e4f5f..4473c6d 100644 --- a/src/listing.rs +++ b/src/listing.rs @@ -261,9 +261,12 @@ pub fn directory_listing( .chunked() .body(Body::Streaming(Box::new(once(Ok(content)))))) } - Err(err) => Ok(HttpResponse::Ok() - .status(http::StatusCode::INTERNAL_SERVER_ERROR) - .body(err.to_string())), + Err(err) => { + log::error!("{}", &err); + Ok(HttpResponse::Ok() + .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .body(err.to_string())) + } } } else { Ok(HttpResponse::Ok() -- cgit v1.2.3 From 6dad3eb1bf0cb3b36cdb3b312cca7caa91de2f57 Mon Sep 17 00:00:00 2001 From: boastful-squirrel Date: Mon, 22 Apr 2019 00:39:38 +0200 Subject: Properly log error + added render_error method --- src/errors.rs | 6 ++++++ src/file_upload.rs | 12 ++++++------ src/listing.rs | 5 +++-- src/renderer.rs | 2 +- 4 files changed, 16 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/errors.rs b/src/errors.rs index f2d185e..2bf4130 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -50,6 +50,12 @@ pub enum ContextualErrorKind { ArchiveCreationError(String, Box), } +pub fn log_error_chain(description: String) { + for cause in description.lines() { + log::error!("{}", cause); + } +} + /// Based on https://boats.gitlab.io/failure/error-errorkind.html pub struct ContextualError { inner: Context, diff --git a/src/file_upload.rs b/src/file_upload.rs index 54c56dd..3b84721 100644 --- a/src/file_upload.rs +++ b/src/file_upload.rs @@ -10,8 +10,8 @@ use std::{ path::{Component, PathBuf}, }; -use crate::errors::ContextualErrorKind; -use crate::renderer::file_upload_error; +use crate::errors::{self, ContextualErrorKind}; +use crate::renderer; /// Query parameters #[derive(Debug, Deserialize)] @@ -31,11 +31,11 @@ fn save_file( ))); } - let mut file = match std::fs::File::create(file_path) { + let mut file = match std::fs::File::create(&file_path) { Ok(file) => file, Err(e) => { return Box::new(future::err(ContextualErrorKind::IOError( - "Failed to create file".to_string(), + format!("Failed to create file in {}", file_path.display()), e, ))); } @@ -175,10 +175,10 @@ fn create_error_response( description: &str, return_path: &str, ) -> FutureResult { - log::error!("{}", description); + errors::log_error_chain(description.to_string()); future::ok( HttpResponse::BadRequest() .content_type("text/html; charset=utf-8") - .body(file_upload_error(description, return_path).into_string()), + .body(renderer::render_error(description, return_path).into_string()), ) } diff --git a/src/listing.rs b/src/listing.rs index 4473c6d..0ac02ff 100644 --- a/src/listing.rs +++ b/src/listing.rs @@ -12,6 +12,7 @@ use strum_macros::{Display, EnumString}; use crate::archive; use crate::renderer; use crate::themes; +use crate::errors; /// Query parameters #[derive(Deserialize)] @@ -262,10 +263,10 @@ pub fn directory_listing( .body(Body::Streaming(Box::new(once(Ok(content)))))) } Err(err) => { - log::error!("{}", &err); + errors::log_error_chain(err.to_string()); Ok(HttpResponse::Ok() .status(http::StatusCode::INTERNAL_SERVER_ERROR) - .body(err.to_string())) + .body(renderer::render_error(&err.to_string(), serve_path).into_string()) } } } else { diff --git a/src/renderer.rs b/src/renderer.rs index be7a51f..eccc72d 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -761,7 +761,7 @@ fn humanize_systemtime(src_time: Option) -> Option { } /// Renders error page when file uploading fails -pub fn file_upload_error(error_description: &str, return_address: &str) -> Markup { +pub fn render_error(error_description: &str, return_address: &str) -> Markup { html! { pre { (error_description) } a href=(return_address) { -- cgit v1.2.3 From 92953d1500d6687cac2a4494e38aecac264e4789 Mon Sep 17 00:00:00 2001 From: boastful-squirrel Date: Mon, 22 Apr 2019 01:05:34 +0200 Subject: Fixed syntax error --- src/listing.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/listing.rs b/src/listing.rs index 0ac02ff..a030feb 100644 --- a/src/listing.rs +++ b/src/listing.rs @@ -10,9 +10,9 @@ use std::time::SystemTime; use strum_macros::{Display, EnumString}; use crate::archive; +use crate::errors; use crate::renderer; use crate::themes; -use crate::errors; /// Query parameters #[derive(Deserialize)] @@ -266,7 +266,7 @@ pub fn directory_listing( 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).into_string())) } } } else { -- cgit v1.2.3 From 4ddade5bb351b454ebc7c9d6ad69ea40549cf9b1 Mon Sep 17 00:00:00 2001 From: boastful-squirrel Date: Mon, 22 Apr 2019 12:08:40 +0200 Subject: Improved errors --- src/auth.rs | 10 ++++++---- src/errors.rs | 7 +++++++ 2 files changed, 13 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/auth.rs b/src/auth.rs index 8cedaae..1bdf0be 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -50,10 +50,12 @@ impl Middleware for Auth { let auth_req = match parse_basic_auth(auth_headers) { Ok(auth_req) => auth_req, Err(err) => { - return Ok(Response::Done(HttpResponse::BadRequest().body(format!( - "An error occured during HTTP authentication\ncaused by: {}", - err - )))) + let auth_err = ContextualError::new( + ContextualErrorKind::HTTPAuthenticationError(Box::new(err)), + ); + return Ok(Response::Done( + HttpResponse::BadRequest().body(auth_err.to_string()), + )); } }; if auth_req.username != required_auth.username diff --git a/src/errors.rs b/src/errors.rs index 2bf4130..bd6dc66 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -48,6 +48,13 @@ pub enum ContextualErrorKind { _0, _1 )] ArchiveCreationError(String, Box), + + /// This error might occur when the HTTP authentication fails + #[fail( + display = "An error occured during HTTP authentication\ncaused by: {}", + _0 + )] + HTTPAuthenticationError(Box), } pub fn log_error_chain(description: String) { -- cgit v1.2.3 From 6efacace8c735fa9f2a957e148a9cfaedf59f455 Mon Sep 17 00:00:00 2001 From: boastful-squirrel Date: Mon, 22 Apr 2019 18:01:49 +0200 Subject: Display errors from main() through log strem --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/main.rs b/src/main.rs index fec3913..59e5f1d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,7 +61,7 @@ pub struct MiniserveConfig { fn main() { match run() { Ok(()) => (), - Err(e) => eprintln!("\n{}\n", Paint::red(e)), + Err(e) => errors::log_error_chain(e.to_string()), } } -- cgit v1.2.3 From c0d5e5c73be8ec5891c0e3d189a89b5c260a8313 Mon Sep 17 00:00:00 2001 From: boastful-squirrel Date: Tue, 23 Apr 2019 00:02:12 +0200 Subject: Fixed comment --- src/renderer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/renderer.rs b/src/renderer.rs index eccc72d..b292e70 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -760,7 +760,7 @@ fn humanize_systemtime(src_time: Option) -> Option { .map(|duration| HumanTime::from(duration).to_text_en(Accuracy::Rough, Tense::Past)) } -/// Renders error page when file uploading fails +/// Renders an error on the webpage pub fn render_error(error_description: &str, return_address: &str) -> Markup { html! { pre { (error_description) } -- cgit v1.2.3 From 63572cf0fc2f94646e2736e27967ba985e5f7405 Mon Sep 17 00:00:00 2001 From: boastful-squirrel Date: Tue, 23 Apr 2019 19:52:42 +0200 Subject: Made code more idiomatic --- src/archive.rs | 69 +++++++++++++++++++++++++++++----------------------------- 1 file changed, 34 insertions(+), 35 deletions(-) (limited to 'src') diff --git a/src/archive.rs b/src/archive.rs index 00d2901..b5788f5 100644 --- a/src/archive.rs +++ b/src/archive.rs @@ -55,42 +55,41 @@ pub fn create_archive( /// Compresses a given folder in .tar.gz format, and returns the result as a stream of bytes fn tgz_compress(dir: &PathBuf, skip_symlinks: bool) -> Result<(String, Bytes), ContextualError> { let src_dir = dir.display().to_string(); - let inner_folder = match dir.file_name() { - Some(directory_name) => match directory_name.to_str() { - Some(directory) => directory, - None => { - // https://doc.rust-lang.org/std/ffi/struct.OsStr.html#method.to_str - return Err(ContextualError::new(ContextualErrorKind::InvalidPathError( - "Directory name contains invalid UTF-8 characters".to_string(), - ))); - } - }, - None => { - // https://doc.rust-lang.org/std/path/struct.Path.html#method.file_name - return Err(ContextualError::new(ContextualErrorKind::InvalidPathError( - "Directory name terminates in \"..\"".to_string(), - ))); - } - }; - let dst_filename = format!("{}.tar", inner_folder); - let dst_tgz_filename = format!("{}.gz", dst_filename); - - let tar_content = tar(src_dir, inner_folder.to_string(), skip_symlinks).map_err(|e| { - ContextualError::new(ContextualErrorKind::ArchiveCreationError( - "tarball".to_string(), - Box::new(e), - )) - })?; - let gz_data = gzip(&tar_content).map_err(|e| { - ContextualError::new(ContextualErrorKind::ArchiveCreationError( - "GZIP archive".to_string(), - Box::new(e), - )) - })?; - let mut data = Bytes::new(); - data.extend_from_slice(&gz_data); + if let Some(inner_folder) = dir.file_name() { + if let Some(directory) = inner_folder.to_str() { + let dst_filename = format!("{}.tar", directory); + let dst_tgz_filename = format!("{}.gz", dst_filename); + let mut tgz_data = Bytes::new(); - Ok((dst_tgz_filename, data)) + let tar_data = tar(src_dir, directory.to_string(), skip_symlinks).map_err(|e| { + ContextualError::new(ContextualErrorKind::ArchiveCreationError( + "tarball".to_string(), + Box::new(e), + )) + })?; + + let gz_data = gzip(&tar_data).map_err(|e| { + ContextualError::new(ContextualErrorKind::ArchiveCreationError( + "GZIP archive".to_string(), + Box::new(e), + )) + })?; + + tgz_data.extend_from_slice(&gz_data); + + Ok((dst_tgz_filename, tgz_data)) + } else { + // https://doc.rust-lang.org/std/ffi/struct.OsStr.html#method.to_str + Err(ContextualError::new(ContextualErrorKind::InvalidPathError( + "Directory name contains invalid UTF-8 characters".to_string(), + ))) + } + } else { + // https://doc.rust-lang.org/std/path/struct.Path.html#method.file_name + Err(ContextualError::new(ContextualErrorKind::InvalidPathError( + "Directory name terminates in \"..\"".to_string(), + ))) + } } /// Creates a TAR archive of a folder, and returns it as a stream of bytes -- cgit v1.2.3