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)",
"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)",
"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_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"ffsend-api 0.1.0",

View file

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

View file

@ -4,7 +4,7 @@ use std::path::{self, PathBuf};
use std::sync::{Arc, Mutex};
use clap::ArgMatches;
use failure::{err_msg, Fail};
use failure::Fail;
use ffsend_api::action::download::{
Download as ApiDownload,
Error as DownloadError,
@ -26,7 +26,14 @@ use cmd::matcher::{
main::MainMatcher,
};
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.
pub struct Download<'a> {
@ -153,10 +160,13 @@ impl<'a> Download<'a> {
if let Err(err) = create_dir_all(parent) {
quit_error(err.context(
"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;
@ -175,7 +185,8 @@ impl<'a> Download<'a> {
match target.canonicalize() {
Ok(target) => return target,
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() {
Ok(target) => return target.join(name_hint),
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),
Err(err) => quit_error(err.context(
"Failed to determine working directory to use for the output file"
)),
), ErrorHints::default()),
}
}
let path = path.unwrap();
@ -222,8 +234,8 @@ impl<'a> Download<'a> {
match current_dir() {
Ok(workdir) => target = workdir.join(target),
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 super::{CmdArg, CmdArgOption};
use util::quit_error_msg;
use util::{ErrorHints, quit_error_msg};
/// The host argument.
pub struct ArgHost { }
@ -36,18 +36,33 @@ impl<'a> CmdArgOption<'a> for ArgHost {
match Url::parse(url) {
Ok(url) => url,
Err(ParseError::EmptyHost) =>
quit_error_msg("Emtpy host given"),
quit_error_msg("Emtpy host given", ErrorHints::default()),
Err(ParseError::InvalidPort) =>
quit_error_msg("Invalid host port"),
quit_error_msg("Invalid host port", ErrorHints::default()),
Err(ParseError::InvalidIpv4Address) =>
quit_error_msg("Invalid IPv4 address in host"),
quit_error_msg(
"Invalid IPv4 address in host",
ErrorHints::default(),
),
Err(ParseError::InvalidIpv6Address) =>
quit_error_msg("Invalid IPv6 address in host"),
quit_error_msg(
"Invalid IPv6 address in host",
ErrorHints::default(),
),
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) =>
quit_error_msg("Host domain doesn't contain a host"),
_ => quit_error_msg("The given host is invalid"),
quit_error_msg(
"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 super::{CmdArg, CmdArgOption};
use util::quit_error_msg;
use util::{ErrorHints, quit_error_msg};
/// The URL argument.
pub struct ArgUrl { }
@ -32,18 +32,33 @@ impl<'a> CmdArgOption<'a> for ArgUrl {
match Url::parse(url) {
Ok(url) => url,
Err(ParseError::EmptyHost) =>
quit_error_msg("Emtpy host given"),
quit_error_msg("Emtpy host given", ErrorHints::default()),
Err(ParseError::InvalidPort) =>
quit_error_msg("Invalid host port"),
quit_error_msg("Invalid host port", ErrorHints::default()),
Err(ParseError::InvalidIpv4Address) =>
quit_error_msg("Invalid IPv4 address in host"),
quit_error_msg(
"Invalid IPv4 address in host",
ErrorHints::default(),
),
Err(ParseError::InvalidIpv6Address) =>
quit_error_msg("Invalid IPv6 address in host"),
quit_error_msg(
"Invalid IPv6 address in host",
ErrorHints::default(),
),
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) =>
quit_error_msg("Host domain doesn't contain a host"),
_ => quit_error_msg("The given host is invalid"),
quit_error_msg(
"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;
#[macro_use]
extern crate derive_builder;
extern crate failure;
#[macro_use]
extern crate failure_derive;
@ -22,7 +23,7 @@ use action::password::Password;
use action::upload::Upload;
use cmd::Handler;
use error::Error;
use util::quit_error;
use util::{ErrorHints, quit_error};
/// Application entrypoint.
fn main() {
@ -31,7 +32,7 @@ fn main() {
// Invoke the proper action
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")]
use self::clipboard::{ClipboardContext, ClipboardProvider};
use self::colored::*;
use failure::{self, err_msg, Fail};
use failure::{err_msg, Fail};
use ffsend_api::url::Url;
use rpassword::prompt_password_stderr;
@ -55,13 +55,12 @@ pub fn quit() -> ! {
/// Quit the application with an error code,
/// 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_error(err);
// Print some additional information
eprintln!("\nFor detailed errors try '{}'", "--verbose".yellow());
eprintln!("For more information try '{}'", "--help".yellow());
// Print error hints
hints.print();
// Quit
exit(1);
@ -69,11 +68,80 @@ pub fn quit_error<E: Fail>(err: E) -> ! {
/// Quit the application with an error code,
/// 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
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.
@ -100,7 +168,14 @@ pub fn set_clipboard(content: String) -> Result<(), Box<StdError>> {
pub fn prompt_password(main_matcher: &MainMatcher) -> String {
// Quit with an error if we may not 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
@ -108,15 +183,19 @@ pub fn prompt_password(main_matcher: &MainMatcher) -> String {
Ok(password) => password,
Err(err) => quit_error(err.context(
"Failed to read password from password prompt"
)),
), ErrorHints::default()),
};
// Do not allow empty passwords unless forced
if !main_matcher.force() && password.is_empty() {
quit_error(err_msg("\
An empty password is not supported by the web interface, \
use '-f' to force\
").compat())
quit_error_msg(
"An empty password is not supported by the web interface",
ErrorHintsBuilder::default()
.force(true)
.verbose(false)
.build()
.unwrap(),
)
}
password
@ -155,10 +234,10 @@ pub fn ensure_password(
pub fn prompt(msg: &str, main_matcher: &MainMatcher) -> String {
// Quit with an error if we may not 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",
msg,
).compat());
), ErrorHints::default());
}
// 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) {
quit_error(err.context(
"Failed to read input from prompt"
));
), ErrorHints::default());
}
// Trim and return
@ -212,10 +291,10 @@ pub fn prompt_yes(
});
return def;
} else {
quit_error(format_err!(
quit_error_msg(format!(
"Could not prompt question '{}' in no-interact mode, maybe specify it",
msg,
).compat());
), ErrorHints::default());
}
}
@ -287,7 +366,14 @@ pub fn ensure_owner_token(
if interact {
*token = Some(prompt_owner_token(main_matcher));
} 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(),
);
}
}