Implement configurable error hints

This commit is contained in:
timvisee 2018-04-12 21:27:52 +02:00
parent b8a03b89dd
commit c377e2be51
No known key found for this signature in database
GPG key ID: 109CBA0BF74036C2
7 changed files with 177 additions and 46 deletions

1
Cargo.lock generated
View file

@ -361,6 +361,7 @@ dependencies = [
"clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)",
"clipboard 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "clipboard 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"colored 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "colored 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"derive_builder 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"ffsend-api 0.1.0", "ffsend-api 0.1.0",

View file

@ -18,6 +18,7 @@ no-color = ["colored/no-color"]
clap = "2.31" clap = "2.31"
clipboard = { version = "0.4", optional = true } clipboard = { version = "0.4", optional = true }
colored = "1.6" colored = "1.6"
derive_builder = "0.5"
failure = "0.1" failure = "0.1"
failure_derive = "0.1" failure_derive = "0.1"
ffsend-api = { version = "*", path = "../api" } ffsend-api = { version = "*", path = "../api" }

View file

@ -4,7 +4,7 @@ use std::path::{self, PathBuf};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use clap::ArgMatches; use clap::ArgMatches;
use failure::{err_msg, Fail}; use failure::Fail;
use ffsend_api::action::download::{ use ffsend_api::action::download::{
Download as ApiDownload, Download as ApiDownload,
Error as DownloadError, Error as DownloadError,
@ -26,7 +26,14 @@ use cmd::matcher::{
main::MainMatcher, main::MainMatcher,
}; };
use progress::ProgressBar; use progress::ProgressBar;
use util::{ensure_password, prompt_yes, quit, quit_error}; use util::{
ensure_password,
ErrorHints,
prompt_yes,
quit,
quit_error,
quit_error_msg,
};
/// A file download action. /// A file download action.
pub struct Download<'a> { pub struct Download<'a> {
@ -153,10 +160,13 @@ impl<'a> Download<'a> {
if let Err(err) = create_dir_all(parent) { if let Err(err) = create_dir_all(parent) {
quit_error(err.context( quit_error(err.context(
"Failed to create parent directories for output file", "Failed to create parent directories for output file",
)); ), ErrorHints::default());
} }
}, },
None => quit_error(err_msg("Invalid output file path").compat()), None => quit_error_msg(
"Invalid output file path",
ErrorHints::default(),
),
} }
return target; return target;
@ -175,7 +185,8 @@ impl<'a> Download<'a> {
match target.canonicalize() { match target.canonicalize() {
Ok(target) => return target, Ok(target) => return target,
Err(err) => quit_error( Err(err) => quit_error(
err.context("Failed to canonicalize target path") err.context("Failed to canonicalize target path"),
ErrorHints::default(),
), ),
} }
} }
@ -185,7 +196,8 @@ impl<'a> Download<'a> {
match target.canonicalize() { match target.canonicalize() {
Ok(target) => return target.join(name_hint), Ok(target) => return target.join(name_hint),
Err(err) => quit_error( Err(err) => quit_error(
err.context("Failed to canonicalize target path") err.context("Failed to canonicalize target path"),
ErrorHints::default(),
), ),
} }
} }
@ -204,7 +216,7 @@ impl<'a> Download<'a> {
Ok(target) => return target.join(name_hint), Ok(target) => return target.join(name_hint),
Err(err) => quit_error(err.context( Err(err) => quit_error(err.context(
"Failed to determine working directory to use for the output file" "Failed to determine working directory to use for the output file"
)), ), ErrorHints::default()),
} }
} }
let path = path.unwrap(); let path = path.unwrap();
@ -222,8 +234,8 @@ impl<'a> Download<'a> {
match current_dir() { match current_dir() {
Ok(workdir) => target = workdir.join(target), Ok(workdir) => target = workdir.join(target),
Err(err) => quit_error(err.context( Err(err) => quit_error(err.context(
"Failed to determine working directory to use for the output file" "Failed to determine working directory to use for the output file"
)), ), ErrorHints::default()),
} }
} }

View file

@ -3,7 +3,7 @@ use ffsend_api::url::{ParseError, Url};
use app::SEND_DEF_HOST; use app::SEND_DEF_HOST;
use super::{CmdArg, CmdArgOption}; use super::{CmdArg, CmdArgOption};
use util::quit_error_msg; use util::{ErrorHints, quit_error_msg};
/// The host argument. /// The host argument.
pub struct ArgHost { } pub struct ArgHost { }
@ -36,18 +36,33 @@ impl<'a> CmdArgOption<'a> for ArgHost {
match Url::parse(url) { match Url::parse(url) {
Ok(url) => url, Ok(url) => url,
Err(ParseError::EmptyHost) => Err(ParseError::EmptyHost) =>
quit_error_msg("Emtpy host given"), quit_error_msg("Emtpy host given", ErrorHints::default()),
Err(ParseError::InvalidPort) => Err(ParseError::InvalidPort) =>
quit_error_msg("Invalid host port"), quit_error_msg("Invalid host port", ErrorHints::default()),
Err(ParseError::InvalidIpv4Address) => Err(ParseError::InvalidIpv4Address) =>
quit_error_msg("Invalid IPv4 address in host"), quit_error_msg(
"Invalid IPv4 address in host",
ErrorHints::default(),
),
Err(ParseError::InvalidIpv6Address) => Err(ParseError::InvalidIpv6Address) =>
quit_error_msg("Invalid IPv6 address in host"), quit_error_msg(
"Invalid IPv6 address in host",
ErrorHints::default(),
),
Err(ParseError::InvalidDomainCharacter) => Err(ParseError::InvalidDomainCharacter) =>
quit_error_msg("Host domains contains an invalid character"), quit_error_msg(
"Host domains contains an invalid character",
ErrorHints::default(),
),
Err(ParseError::RelativeUrlWithoutBase) => Err(ParseError::RelativeUrlWithoutBase) =>
quit_error_msg("Host domain doesn't contain a host"), quit_error_msg(
_ => quit_error_msg("The given host is invalid"), "Host domain doesn't contain a host",
ErrorHints::default(),
),
_ => quit_error_msg(
"The given host is invalid",
ErrorHints::default(),
),
} }
} }
} }

View file

@ -2,7 +2,7 @@ use clap::{Arg, ArgMatches};
use ffsend_api::url::{ParseError, Url}; use ffsend_api::url::{ParseError, Url};
use super::{CmdArg, CmdArgOption}; use super::{CmdArg, CmdArgOption};
use util::quit_error_msg; use util::{ErrorHints, quit_error_msg};
/// The URL argument. /// The URL argument.
pub struct ArgUrl { } pub struct ArgUrl { }
@ -32,18 +32,33 @@ impl<'a> CmdArgOption<'a> for ArgUrl {
match Url::parse(url) { match Url::parse(url) {
Ok(url) => url, Ok(url) => url,
Err(ParseError::EmptyHost) => Err(ParseError::EmptyHost) =>
quit_error_msg("Emtpy host given"), quit_error_msg("Emtpy host given", ErrorHints::default()),
Err(ParseError::InvalidPort) => Err(ParseError::InvalidPort) =>
quit_error_msg("Invalid host port"), quit_error_msg("Invalid host port", ErrorHints::default()),
Err(ParseError::InvalidIpv4Address) => Err(ParseError::InvalidIpv4Address) =>
quit_error_msg("Invalid IPv4 address in host"), quit_error_msg(
"Invalid IPv4 address in host",
ErrorHints::default(),
),
Err(ParseError::InvalidIpv6Address) => Err(ParseError::InvalidIpv6Address) =>
quit_error_msg("Invalid IPv6 address in host"), quit_error_msg(
"Invalid IPv6 address in host",
ErrorHints::default(),
),
Err(ParseError::InvalidDomainCharacter) => Err(ParseError::InvalidDomainCharacter) =>
quit_error_msg("Host domains contains an invalid character"), quit_error_msg(
"Host domains contains an invalid character",
ErrorHints::default(),
),
Err(ParseError::RelativeUrlWithoutBase) => Err(ParseError::RelativeUrlWithoutBase) =>
quit_error_msg("Host domain doesn't contain a host"), quit_error_msg(
_ => quit_error_msg("The given host is invalid"), "Host domain doesn't contain a host",
ErrorHints::default(),
),
_ => quit_error_msg(
"The given host is invalid",
ErrorHints::default(),
),
} }
} }
} }

View file

@ -1,5 +1,6 @@
extern crate clap; extern crate clap;
#[macro_use] #[macro_use]
extern crate derive_builder;
extern crate failure; extern crate failure;
#[macro_use] #[macro_use]
extern crate failure_derive; extern crate failure_derive;
@ -22,7 +23,7 @@ use action::password::Password;
use action::upload::Upload; use action::upload::Upload;
use cmd::Handler; use cmd::Handler;
use error::Error; use error::Error;
use util::quit_error; use util::{ErrorHints, quit_error};
/// Application entrypoint. /// Application entrypoint.
fn main() { fn main() {
@ -31,7 +32,7 @@ fn main() {
// Invoke the proper action // Invoke the proper action
if let Err(err) = invoke_action(&cmd_handler) { if let Err(err) = invoke_action(&cmd_handler) {
quit_error(err); quit_error(err, ErrorHints::default());
}; };
} }

View file

@ -17,7 +17,7 @@ use std::process::{exit, ExitStatus};
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
use self::clipboard::{ClipboardContext, ClipboardProvider}; use self::clipboard::{ClipboardContext, ClipboardProvider};
use self::colored::*; use self::colored::*;
use failure::{self, err_msg, Fail}; use failure::{err_msg, Fail};
use ffsend_api::url::Url; use ffsend_api::url::Url;
use rpassword::prompt_password_stderr; use rpassword::prompt_password_stderr;
@ -55,13 +55,12 @@ pub fn quit() -> ! {
/// Quit the application with an error code, /// Quit the application with an error code,
/// and print the given error. /// and print the given error.
pub fn quit_error<E: Fail>(err: E) -> ! { pub fn quit_error<E: Fail>(err: E, hints: ErrorHints) -> ! {
// Print the error // Print the error
print_error(err); print_error(err);
// Print some additional information // Print error hints
eprintln!("\nFor detailed errors try '{}'", "--verbose".yellow()); hints.print();
eprintln!("For more information try '{}'", "--help".yellow());
// Quit // Quit
exit(1); exit(1);
@ -69,11 +68,80 @@ pub fn quit_error<E: Fail>(err: E) -> ! {
/// Quit the application with an error code, /// Quit the application with an error code,
/// and print the given error message. /// and print the given error message.
pub fn quit_error_msg<S>(err: S) -> ! pub fn quit_error_msg<S>(err: S, hints: ErrorHints) -> !
where where
S: AsRef<str> + Display + Debug + Sync + Send + 'static S: AsRef<str> + Display + Debug + Sync + Send + 'static
{ {
quit_error(failure::err_msg(err).compat()); quit_error(err_msg(err).compat(), hints);
}
/// The error hint configuration.
#[derive(Copy, Clone, Builder)]
#[builder(default)]
pub struct ErrorHints {
/// Show about the password option.
password: bool,
/// Show about the owner option.
owner: bool,
/// Show about the force flag.
force: bool,
/// Show about the verbose flag.
verbose: bool,
/// Show about the help flag.
help: bool,
}
impl ErrorHints {
/// Check whether any hint should be printed.
pub fn any(&self) -> bool {
self.password || self.owner || self.force || self.verbose || self.help
}
/// Print the error hints.
pub fn print(&self) {
// Stop if nothing should be printed
if !self.any() {
return;
}
eprint!("\n");
// Print hints
if self.password {
eprintln!("Use '{}' to specify a password", "--password <PASSWORD>".yellow());
}
if self.owner {
eprintln!("Use '{}' to specify an owner token", "--owner <TOKEN>".yellow());
}
if self.force {
eprintln!("Use '{}' to force", "--force".yellow());
}
if self.verbose {
eprintln!("For detailed errors try '{}'", "--verbose".yellow());
}
if self.help {
eprintln!("For more information try '{}'", "--help".yellow());
}
// Flush
let _ = stderr().flush();
}
}
impl Default for ErrorHints {
fn default() -> Self {
ErrorHints {
password: false,
owner: false,
force: false,
verbose: true,
help: true,
}
}
} }
/// Open the given URL in the users default browser. /// Open the given URL in the users default browser.
@ -100,7 +168,14 @@ pub fn set_clipboard(content: String) -> Result<(), Box<StdError>> {
pub fn prompt_password(main_matcher: &MainMatcher) -> String { pub fn prompt_password(main_matcher: &MainMatcher) -> String {
// Quit with an error if we may not interact // Quit with an error if we may not interact
if main_matcher.no_interact() { if main_matcher.no_interact() {
quit_error(err_msg("Missing password, must be specified in no-interact mode").compat()); quit_error_msg(
"Missing password, must be specified in no-interact mode",
ErrorHintsBuilder::default()
.password(true)
.verbose(false)
.build()
.unwrap(),
);
} }
// Prompt for the password // Prompt for the password
@ -108,15 +183,19 @@ pub fn prompt_password(main_matcher: &MainMatcher) -> String {
Ok(password) => password, Ok(password) => password,
Err(err) => quit_error(err.context( Err(err) => quit_error(err.context(
"Failed to read password from password prompt" "Failed to read password from password prompt"
)), ), ErrorHints::default()),
}; };
// Do not allow empty passwords unless forced // Do not allow empty passwords unless forced
if !main_matcher.force() && password.is_empty() { if !main_matcher.force() && password.is_empty() {
quit_error(err_msg("\ quit_error_msg(
An empty password is not supported by the web interface, \ "An empty password is not supported by the web interface",
use '-f' to force\ ErrorHintsBuilder::default()
").compat()) .force(true)
.verbose(false)
.build()
.unwrap(),
)
} }
password password
@ -155,10 +234,10 @@ pub fn ensure_password(
pub fn prompt(msg: &str, main_matcher: &MainMatcher) -> String { pub fn prompt(msg: &str, main_matcher: &MainMatcher) -> String {
// Quit with an error if we may not interact // Quit with an error if we may not interact
if main_matcher.no_interact() { if main_matcher.no_interact() {
quit_error(format_err!( quit_error_msg(format!(
"Could not prompt for '{}' in no-interact mode, maybe specify it", "Could not prompt for '{}' in no-interact mode, maybe specify it",
msg, msg,
).compat()); ), ErrorHints::default());
} }
// Show the prompt // Show the prompt
@ -170,7 +249,7 @@ pub fn prompt(msg: &str, main_matcher: &MainMatcher) -> String {
if let Err(err) = stdin().read_line(&mut input) { if let Err(err) = stdin().read_line(&mut input) {
quit_error(err.context( quit_error(err.context(
"Failed to read input from prompt" "Failed to read input from prompt"
)); ), ErrorHints::default());
} }
// Trim and return // Trim and return
@ -212,10 +291,10 @@ pub fn prompt_yes(
}); });
return def; return def;
} else { } else {
quit_error(format_err!( quit_error_msg(format!(
"Could not prompt question '{}' in no-interact mode, maybe specify it", "Could not prompt question '{}' in no-interact mode, maybe specify it",
msg, msg,
).compat()); ), ErrorHints::default());
} }
} }
@ -287,7 +366,14 @@ pub fn ensure_owner_token(
if interact { if interact {
*token = Some(prompt_owner_token(main_matcher)); *token = Some(prompt_owner_token(main_matcher));
} else { } else {
quit_error(err_msg("Missing owner token, must be specified in no-interact mode").compat()); quit_error_msg(
"Missing owner token, must be specified in no-interact mode",
ErrorHintsBuilder::default()
.owner(true)
.verbose(false)
.build()
.unwrap(),
);
} }
} }