mirror of
https://github.com/timvisee/ffsend.git
synced 2025-10-06 02:29:57 +02:00
Implement configurable error hints
This commit is contained in:
parent
b8a03b89dd
commit
c377e2be51
7 changed files with 177 additions and 46 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
|
@ -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()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
124
cli/src/util.rs
124
cli/src/util.rs
|
@ -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(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue