diff options
author | boastful-squirrel <boastful.squirrel@gmail.com> | 2019-04-21 14:27:34 +0000 |
---|---|---|
committer | boastful-squirrel <boastful.squirrel@gmail.com> | 2019-04-21 14:27:34 +0000 |
commit | 8af3ff10e2347da70c35eb45046f8a04843f7256 (patch) | |
tree | 88b48970bfd3223c30eb820c21bf5b7df8d3511f /src | |
parent | cargo fmt (diff) | |
download | miniserve-8af3ff10e2347da70c35eb45046f8a04843f7256.tar.gz miniserve-8af3ff10e2347da70c35eb45046f8a04843f7256.zip |
Rework error system + avoid panics in main()
Diffstat (limited to 'src')
-rw-r--r-- | src/archive.rs | 106 | ||||
-rw-r--r-- | src/args.rs | 15 | ||||
-rw-r--r-- | src/auth.rs | 29 | ||||
-rw-r--r-- | src/errors.rs | 135 | ||||
-rw-r--r-- | src/file_upload.rs | 57 | ||||
-rw-r--r-- | src/main.rs | 87 |
6 files changed, 251 insertions, 178 deletions
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<Vec<u8>, errors::CompressionError> { +) -> Result<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, 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<IpAddr, std::net::AddrParseError> { } /// 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<BasicAuthParams, BasicAuthError> { - let basic_removed = authorization_header.to_str().unwrap().replace("Basic ", ""); - let decoded = base64::decode(&basic_removed).map_err(|_| BasicAuthError::Base64DecodeError)?; +) -> Result<BasicAuthParams, ContextualError> { + 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<crate::MiniserveConfig> 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<ContextualError>), } /// Based on https://boats.gitlab.io/failure/error-errorkind.html -pub struct CompressionError { - inner: Context<CompressionErrorKind>, +pub struct ContextualError { + inner: Context<ContextualErrorKind>, } -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<Context<CompressionErrorKind>> for CompressionError { - fn from(inner: Context<CompressionErrorKind>) -> CompressionError { - CompressionError { inner } +impl From<Context<ContextualErrorKind>> for ContextualError { + fn from(inner: Context<ContextualErrorKind>) -> ContextualError { + ContextualError { inner } } } -impl From<CompressionErrorKind> for CompressionError { - fn from(kind: CompressionErrorKind) -> CompressionError { - CompressionError { +impl From<ContextualErrorKind> for ContextualError { + fn from(kind: ContextualErrorKind) -> ContextualError { + ContextualError { inner: Context::new(kind), } } } + +/// This allows to create CustomErrors more simply +impl From<String> 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<dev::Payload>, file_path: PathBuf, overwrite_files: bool, -) -> Box<Future<Item = i64, Error = FileUploadErrorKind>> { +) -> Box<Future<Item = i64, Error = ContextualErrorKind>> { 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<dev::Payload>, mut file_path: PathBuf, overwrite_files: bool, -) -> Box<Stream<Item = i64, Error = FileUploadErrorKind>> { +) -> Box<Stream<Item = i64, Error = ContextualErrorKind>> { 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<crate::MiniserveConfig>) -> 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::<Vec<String>>(); - 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::<Result<Vec<SocketAddr>, _>>(); - // 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<MiniserveConfig>) -> App<MiniserveConfig> { 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( |