1159 lines
34 KiB
Rust
1159 lines
34 KiB
Rust
#[cfg(feature = "clipboard-crate")]
|
|
extern crate clip;
|
|
#[cfg(feature = "clipboard-bin")]
|
|
extern crate which;
|
|
|
|
use std::borrow::Borrow;
|
|
use std::env::{self, current_exe, var_os};
|
|
use std::ffi::OsStr;
|
|
#[cfg(feature = "clipboard")]
|
|
use std::fmt;
|
|
use std::fmt::{Debug, Display};
|
|
#[cfg(feature = "clipboard-bin")]
|
|
use std::io::ErrorKind as IoErrorKind;
|
|
use std::io::{self, Read};
|
|
use std::io::{stderr, stdin, Error as IoError, Write};
|
|
use std::iter;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use std::process::exit;
|
|
#[cfg(feature = "clipboard-bin")]
|
|
use std::process::{Command, Stdio};
|
|
|
|
#[cfg(feature = "clipboard-crate")]
|
|
use self::clip::{ClipboardContext, ClipboardProvider};
|
|
use chrono::Duration;
|
|
use colored::*;
|
|
#[cfg(feature = "history")]
|
|
use directories::ProjectDirs;
|
|
use failure::{err_msg, Fail};
|
|
#[cfg(feature = "clipboard-crate")]
|
|
use failure::{Compat, Error};
|
|
use ffsend_api::{
|
|
api::request::{ensure_success, ResponseError},
|
|
client::Client,
|
|
reqwest,
|
|
url::Url,
|
|
};
|
|
use fs2::available_space;
|
|
use rand::distributions::Alphanumeric;
|
|
use rand::{thread_rng, Rng};
|
|
use regex::Regex;
|
|
use rpassword::prompt_password_stderr;
|
|
#[cfg(feature = "clipboard-bin")]
|
|
use which::which;
|
|
|
|
use crate::cmd::matcher::MainMatcher;
|
|
|
|
/// Print a success message.
|
|
pub fn print_success(msg: &str) {
|
|
eprintln!("{}", msg.green());
|
|
}
|
|
|
|
/// Print the given error in a proper format for the user,
|
|
/// with it's causes.
|
|
pub fn print_error<E: Fail>(err: impl Borrow<E>) {
|
|
// Report each printable error, count them
|
|
let count = err
|
|
.borrow()
|
|
.causes()
|
|
.map(|err| format!("{}", err))
|
|
.filter(|err| !err.is_empty())
|
|
.enumerate()
|
|
.map(|(i, err)| {
|
|
if i == 0 {
|
|
eprintln!("{} {}", highlight_error("error:"), err);
|
|
} else {
|
|
eprintln!("{} {}", highlight_error("caused by:"), err);
|
|
}
|
|
})
|
|
.count();
|
|
|
|
// Fall back to a basic message
|
|
if count == 0 {
|
|
eprintln!(
|
|
"{} {}",
|
|
highlight_error("error:"),
|
|
"an undefined error occurred"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Print the given error message in a proper format for the user,
|
|
/// with it's causes.
|
|
pub fn print_error_msg<S>(err: S)
|
|
where
|
|
S: AsRef<str> + Display + Debug + Sync + Send + 'static,
|
|
{
|
|
print_error(err_msg(err).compat());
|
|
}
|
|
|
|
/// Print a warning.
|
|
pub fn print_warning<S>(err: S)
|
|
where
|
|
S: AsRef<str> + Display + Debug + Sync + Send + 'static,
|
|
{
|
|
eprintln!("{} {}", highlight_warning("warning:"), err);
|
|
}
|
|
|
|
/// Quit the application regularly.
|
|
pub fn quit() -> ! {
|
|
exit(0);
|
|
}
|
|
|
|
/// Quit the application with an error code,
|
|
/// and print the given error.
|
|
pub fn quit_error<E: Fail>(err: E, hints: impl Borrow<ErrorHints>) -> ! {
|
|
// Print the error
|
|
print_error(err);
|
|
|
|
// Print error hints
|
|
hints.borrow().print();
|
|
|
|
// Quit
|
|
exit(1);
|
|
}
|
|
|
|
/// Quit the application with an error code,
|
|
/// and print the given error message.
|
|
pub fn quit_error_msg<S>(err: S, hints: impl Borrow<ErrorHints>) -> !
|
|
where
|
|
S: AsRef<str> + Display + Debug + Sync + Send + 'static,
|
|
{
|
|
quit_error(err_msg(err).compat(), hints);
|
|
}
|
|
|
|
/// The error hint configuration.
|
|
#[derive(Clone, Builder)]
|
|
#[builder(default)]
|
|
pub struct ErrorHints {
|
|
/// Show about specifying an API version.
|
|
api: bool,
|
|
|
|
/// A list of info messages to print along with the error.
|
|
info: Vec<String>,
|
|
|
|
/// Show about the name option.
|
|
name: bool,
|
|
|
|
/// Show about the password option.
|
|
password: bool,
|
|
|
|
/// Show about the owner option.
|
|
owner: bool,
|
|
|
|
/// Show about the history flag.
|
|
#[cfg(feature = "history")]
|
|
history: 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 {
|
|
// Determine the result
|
|
#[allow(unused_mut)]
|
|
let mut result =
|
|
self.name || self.password || self.owner || self.force || self.verbose || self.help;
|
|
|
|
// Factor in the history hint when enabled
|
|
#[cfg(feature = "history")]
|
|
{
|
|
result = result || self.history;
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
/// Print the error hints.
|
|
pub fn print(&self) {
|
|
// Print info messages
|
|
for msg in &self.info {
|
|
eprintln!("{} {}", highlight_info("info:"), msg);
|
|
}
|
|
|
|
// Stop if nothing should be printed
|
|
if !self.any() {
|
|
return;
|
|
}
|
|
|
|
eprint!("\n");
|
|
|
|
// Print hints
|
|
if self.api {
|
|
eprintln!(
|
|
"Use '{}' to select a server API version",
|
|
highlight("--api <VERSION>")
|
|
);
|
|
}
|
|
if self.name {
|
|
eprintln!(
|
|
"Use '{}' to specify a file name",
|
|
highlight("--name <NAME>")
|
|
);
|
|
}
|
|
if self.password {
|
|
eprintln!(
|
|
"Use '{}' to specify a password",
|
|
highlight("--password <PASSWORD>")
|
|
);
|
|
}
|
|
if self.owner {
|
|
eprintln!(
|
|
"Use '{}' to specify an owner token",
|
|
highlight("--owner <TOKEN>")
|
|
);
|
|
}
|
|
#[cfg(feature = "history")]
|
|
{
|
|
if self.history {
|
|
eprintln!(
|
|
"Use '{}' to specify a history file",
|
|
highlight("--history <FILE>")
|
|
);
|
|
}
|
|
}
|
|
if self.force {
|
|
eprintln!("Use '{}' to force", highlight("--force"));
|
|
}
|
|
if self.verbose {
|
|
eprintln!("For detailed errors try '{}'", highlight("--verbose"));
|
|
}
|
|
if self.help {
|
|
eprintln!("For more information try '{}'", highlight("--help"));
|
|
}
|
|
|
|
// Flush
|
|
let _ = stderr().flush();
|
|
}
|
|
}
|
|
|
|
impl Default for ErrorHints {
|
|
fn default() -> Self {
|
|
ErrorHints {
|
|
api: false,
|
|
info: Vec::new(),
|
|
name: false,
|
|
password: false,
|
|
owner: false,
|
|
#[cfg(feature = "history")]
|
|
history: false,
|
|
force: false,
|
|
verbose: true,
|
|
help: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ErrorHintsBuilder {
|
|
/// Add a single info entry.
|
|
pub fn add_info(mut self, info: String) -> Self {
|
|
// Initialize the info list
|
|
if self.info.is_none() {
|
|
self.info = Some(Vec::new());
|
|
}
|
|
|
|
// Add the item to the info list
|
|
if let Some(ref mut list) = self.info {
|
|
list.push(info);
|
|
}
|
|
|
|
self
|
|
}
|
|
}
|
|
|
|
/// Highlight the given text with a color.
|
|
pub fn highlight(msg: &str) -> ColoredString {
|
|
msg.yellow()
|
|
}
|
|
|
|
/// Highlight the given text with an error color.
|
|
pub fn highlight_error(msg: &str) -> ColoredString {
|
|
msg.red().bold()
|
|
}
|
|
|
|
/// Highlight the given text with an warning color.
|
|
pub fn highlight_warning(msg: &str) -> ColoredString {
|
|
highlight(msg).bold()
|
|
}
|
|
|
|
/// Highlight the given text with an info color
|
|
pub fn highlight_info(msg: &str) -> ColoredString {
|
|
msg.cyan()
|
|
}
|
|
|
|
/// Open the given URL in the users default browser.
|
|
/// The browsers exit status is returned.
|
|
pub fn open_url(url: impl Borrow<Url>) -> Result<(), IoError> {
|
|
open_path(url.borrow().as_str())
|
|
}
|
|
|
|
/// Open the given path or URL using the program configured on the system.
|
|
/// The program exit status is returned.
|
|
pub fn open_path(path: &str) -> Result<(), IoError> {
|
|
open::that(path)
|
|
}
|
|
|
|
/// Set the clipboard of the user to the given `content` string.
|
|
#[cfg(feature = "clipboard")]
|
|
pub fn set_clipboard(content: String) -> Result<(), ClipboardError> {
|
|
ClipboardType::select().set(content)
|
|
}
|
|
|
|
/// Clipboard management enum.
|
|
///
|
|
/// Defines which method of setting the clipboard is used.
|
|
/// Invoke `ClipboardType::select()` to select the best variant to use determined at runtime.
|
|
///
|
|
/// Usually, the `Native` variant is used. However, on Linux system a different variant will be
|
|
/// selected which will call a system binary to set the clipboard. This must be done because the
|
|
/// native clipboard interface only has a lifetime of the application. This means that the
|
|
/// clipboard is instantly cleared as soon as this application quits, which is always immediately.
|
|
/// This limitation is due to security reasons as defined by X11. The alternative binaries we set
|
|
/// the clipboard with spawn a daemon in the background to keep the clipboard alive until it's
|
|
/// flushed.
|
|
#[cfg(feature = "clipboard")]
|
|
#[derive(Clone, Eq, PartialEq)]
|
|
pub enum ClipboardType {
|
|
/// Native operating system clipboard.
|
|
#[cfg(feature = "clipboard-crate")]
|
|
Native,
|
|
|
|
/// Manage clipboard through `xclip` on Linux.
|
|
///
|
|
/// May contain a binary path if specified at compile time through the `XCLIP_PATH` variable.
|
|
#[cfg(feature = "clipboard-bin")]
|
|
Xclip(Option<String>),
|
|
|
|
/// Manage clipboard through `xsel` on Linux.
|
|
///
|
|
/// May contain a binary path if specified at compile time through the `XSEL_PATH` variable.
|
|
#[cfg(feature = "clipboard-bin")]
|
|
Xsel(Option<String>),
|
|
}
|
|
|
|
#[cfg(feature = "clipboard")]
|
|
impl ClipboardType {
|
|
/// Select the clipboard type to use, depending on the runtime system.
|
|
pub fn select() -> Self {
|
|
#[cfg(feature = "clipboard-crate")]
|
|
{
|
|
ClipboardType::Native
|
|
}
|
|
|
|
#[cfg(feature = "clipboard-bin")]
|
|
{
|
|
if let Some(path) = option_env!("XCLIP_PATH") {
|
|
ClipboardType::Xclip(Some(path.to_owned()))
|
|
} else if let Some(path) = option_env!("XSEL_PATH") {
|
|
ClipboardType::Xsel(Some(path.to_owned()))
|
|
} else if which("xclip").is_ok() {
|
|
ClipboardType::Xclip(None)
|
|
} else if which("xsel").is_ok() {
|
|
ClipboardType::Xsel(None)
|
|
} else {
|
|
// TODO: should we error here instead, as no clipboard binary was found?
|
|
ClipboardType::Xclip(None)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Set clipboard contents through the selected clipboard type.
|
|
pub fn set(&self, content: String) -> Result<(), ClipboardError> {
|
|
match self {
|
|
#[cfg(feature = "clipboard-crate")]
|
|
ClipboardType::Native => Self::native_set(content),
|
|
#[cfg(feature = "clipboard-bin")]
|
|
ClipboardType::Xclip(path) => Self::xclip_set(path.clone(), &content),
|
|
#[cfg(feature = "clipboard-bin")]
|
|
ClipboardType::Xsel(path) => Self::xsel_set(path.clone(), &content),
|
|
}
|
|
}
|
|
|
|
/// Set the clipboard through a native interface.
|
|
///
|
|
/// This is used on non-Linux systems.
|
|
#[cfg(feature = "clipboard-crate")]
|
|
fn native_set(content: String) -> Result<(), ClipboardError> {
|
|
ClipboardProvider::new()
|
|
.and_then(|mut context: ClipboardContext| context.set_contents(content))
|
|
.map_err(|err| format_err!("{}", err).compat())
|
|
.map_err(ClipboardError::Native)
|
|
}
|
|
|
|
#[cfg(feature = "clipboard-bin")]
|
|
fn xclip_set(path: Option<String>, content: &str) -> Result<(), ClipboardError> {
|
|
Self::sys_cmd_set(
|
|
"xclip",
|
|
Command::new(path.unwrap_or_else(|| "xclip".into()))
|
|
.arg("-sel")
|
|
.arg("clip"),
|
|
content,
|
|
)
|
|
}
|
|
|
|
#[cfg(feature = "clipboard-bin")]
|
|
fn xsel_set(path: Option<String>, content: &str) -> Result<(), ClipboardError> {
|
|
Self::sys_cmd_set(
|
|
"xsel",
|
|
Command::new(path.unwrap_or_else(|| "xsel".into())).arg("--clipboard"),
|
|
content,
|
|
)
|
|
}
|
|
|
|
#[cfg(feature = "clipboard-bin")]
|
|
fn sys_cmd_set(
|
|
bin: &'static str,
|
|
command: &mut Command,
|
|
content: &str,
|
|
) -> Result<(), ClipboardError> {
|
|
// Spawn the command process for setting the clipboard
|
|
let mut process = match command.stdin(Stdio::piped()).stdout(Stdio::null()).spawn() {
|
|
Ok(process) => process,
|
|
Err(err) => {
|
|
return Err(match err.kind() {
|
|
IoErrorKind::NotFound => ClipboardError::NoBinary,
|
|
_ => ClipboardError::BinaryIo(bin, err),
|
|
});
|
|
}
|
|
};
|
|
|
|
// Write the contents to the xclip process
|
|
process
|
|
.stdin
|
|
.as_mut()
|
|
.unwrap()
|
|
.write_all(content.as_bytes())
|
|
.map_err(|err| ClipboardError::BinaryIo(bin, err))?;
|
|
|
|
// Wait for xclip to exit
|
|
let status = process
|
|
.wait()
|
|
.map_err(|err| ClipboardError::BinaryIo(bin, err))?;
|
|
if !status.success() {
|
|
return Err(ClipboardError::BinaryStatus(
|
|
bin,
|
|
status.code().unwrap_or(0),
|
|
));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "clipboard")]
|
|
impl fmt::Display for ClipboardType {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
match self {
|
|
#[cfg(feature = "clipboard-crate")]
|
|
ClipboardType::Native => write!(f, "native"),
|
|
#[cfg(feature = "clipboard-bin")]
|
|
ClipboardType::Xclip(path) => match path {
|
|
None => write!(f, "xclip"),
|
|
Some(path) => write!(f, "xclip ({})", path),
|
|
},
|
|
#[cfg(feature = "clipboard-bin")]
|
|
ClipboardType::Xsel(path) => match path {
|
|
None => write!(f, "xsel"),
|
|
Some(path) => write!(f, "xsel ({})", path),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "clipboard")]
|
|
#[derive(Debug, Fail)]
|
|
pub enum ClipboardError {
|
|
/// A generic error occurred while setting the clipboard contents.
|
|
///
|
|
/// This is for non-Linux systems, using a native clipboard interface.
|
|
#[cfg(feature = "clipboard-crate")]
|
|
#[fail(display = "failed to access clipboard")]
|
|
Native(#[cause] Compat<Error>),
|
|
|
|
/// The `xclip` or `xsel` binary could not be found on the system, required for clipboard support.
|
|
#[cfg(feature = "clipboard-bin")]
|
|
#[fail(display = "failed to access clipboard, xclip or xsel is not installed")]
|
|
NoBinary,
|
|
|
|
/// An error occurred while using `xclip` or `xsel` to set the clipboard contents.
|
|
/// This problem probably occurred when starting, or while piping the clipboard contents to
|
|
/// the process.
|
|
#[cfg(feature = "clipboard-bin")]
|
|
#[fail(display = "failed to access clipboard using {}", _0)]
|
|
BinaryIo(&'static str, #[cause] IoError),
|
|
|
|
/// `xclip` or `xsel` unexpectedly exited with a non-successful status code.
|
|
#[cfg(feature = "clipboard-bin")]
|
|
#[fail(
|
|
display = "failed to use clipboard, {} exited with status code {}",
|
|
_0, _1
|
|
)]
|
|
BinaryStatus(&'static str, i32),
|
|
}
|
|
|
|
/// Check for an empty password in the given `password`.
|
|
/// If the password is empty the program will quit with an error unless
|
|
/// forced.
|
|
// TODO: move this to a better module
|
|
pub fn check_empty_password(password: &str, matcher_main: &MainMatcher) {
|
|
if !matcher_main.force() && password.is_empty() {
|
|
quit_error_msg(
|
|
"an empty password is not supported by the web interface",
|
|
ErrorHintsBuilder::default()
|
|
.force(true)
|
|
.verbose(false)
|
|
.build()
|
|
.unwrap(),
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Prompt the user to enter a password.
|
|
///
|
|
/// If `empty` is `false`, empty passwords aren't allowed unless forced.
|
|
pub fn prompt_password(main_matcher: &MainMatcher, optional: bool) -> Option<String> {
|
|
// Quit with an error if we may not interact
|
|
if !optional && main_matcher.no_interact() {
|
|
quit_error_msg(
|
|
"missing password, must be specified in no-interact mode",
|
|
ErrorHintsBuilder::default()
|
|
.password(true)
|
|
.verbose(false)
|
|
.build()
|
|
.unwrap(),
|
|
);
|
|
}
|
|
|
|
// Prompt for the password
|
|
let prompt = if optional {
|
|
"Password (optional): "
|
|
} else {
|
|
"Password: "
|
|
};
|
|
match prompt_password_stderr(prompt) {
|
|
// If optional and nothing is entered, regard it as not defined
|
|
Ok(password) => {
|
|
if password.is_empty() && optional {
|
|
None
|
|
} else {
|
|
Some(password)
|
|
}
|
|
}
|
|
|
|
// On input error, propagate the error or don't use a password if optional
|
|
Err(err) => {
|
|
if !optional {
|
|
quit_error(
|
|
err.context("failed to read password from password prompt"),
|
|
ErrorHints::default(),
|
|
)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Get a password if required.
|
|
/// This method will ensure a password is set (or not) in the given `password`
|
|
/// parameter, as defined by `needs`.
|
|
/// If a password is needed, it may optionally be entered if `option` is set to true.
|
|
///
|
|
/// This method will prompt the user for a password, if one is required but
|
|
/// wasn't set. An ignore message will be shown if it was not required while it
|
|
/// was set.
|
|
///
|
|
/// Returns true if a password is now set, false if not.
|
|
pub fn ensure_password(
|
|
password: &mut Option<String>,
|
|
needs: bool,
|
|
main_matcher: &MainMatcher,
|
|
optional: bool,
|
|
) -> bool {
|
|
// Return if we're fine, ignore if set but we don't need it
|
|
if password.is_some() == needs {
|
|
return needs;
|
|
}
|
|
if !needs {
|
|
// Notify the user a set password is ignored
|
|
if password.is_some() {
|
|
println!("Ignoring password, it is not required");
|
|
*password = None;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Check whether we allow interaction
|
|
let interact = !main_matcher.no_interact();
|
|
|
|
loop {
|
|
// Prompt for an owner token if not set yet
|
|
if password.is_none() {
|
|
// Do not ask for a token if optional when non-interactive or forced
|
|
if optional && (!interact || main_matcher.force()) {
|
|
return false;
|
|
}
|
|
|
|
// Ask for the password
|
|
*password = prompt_password(main_matcher, optional);
|
|
}
|
|
|
|
// The token must not be empty, unless it's optional
|
|
let empty = password.is_none();
|
|
if empty && !optional {
|
|
eprintln!(
|
|
"No password given, which is required. Use {} to cancel.",
|
|
highlight("[CTRL+C]"),
|
|
);
|
|
} else {
|
|
return !empty;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Prompt the user to enter some value.
|
|
/// The prompt that is shown should be passed to `msg`,
|
|
/// excluding the `:` suffix.
|
|
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_msg(
|
|
format!(
|
|
"could not prompt for '{}' in no-interact mode, maybe specify it",
|
|
msg,
|
|
),
|
|
ErrorHints::default(),
|
|
);
|
|
}
|
|
|
|
// Show the prompt
|
|
eprint!("{}: ", msg);
|
|
let _ = stderr().flush();
|
|
|
|
// Get the input
|
|
let mut input = String::new();
|
|
if let Err(err) = stdin().read_line(&mut input) {
|
|
quit_error(
|
|
err.context("failed to read input from prompt"),
|
|
ErrorHints::default(),
|
|
);
|
|
}
|
|
|
|
// Trim and return
|
|
input.trim().to_owned()
|
|
}
|
|
|
|
/// Prompt the user for a question, allowing a yes or now answer.
|
|
/// True is returned if yes was answered, false if no.
|
|
///
|
|
/// A default may be given, which is chosen if no-interact mode is
|
|
/// enabled, or if enter was pressed by the user without entering anything.
|
|
pub fn prompt_yes(msg: &str, def: Option<bool>, main_matcher: &MainMatcher) -> bool {
|
|
// Define the available options string
|
|
let options = format!(
|
|
"[{}/{}]",
|
|
match def {
|
|
Some(def) if def => "Y",
|
|
_ => "y",
|
|
},
|
|
match def {
|
|
Some(def) if !def => "N",
|
|
_ => "n",
|
|
}
|
|
);
|
|
|
|
// Assume yes
|
|
if main_matcher.assume_yes() {
|
|
eprintln!("{} {}: yes", msg, options);
|
|
return true;
|
|
}
|
|
|
|
// Autoselect if in no-interact mode
|
|
if main_matcher.no_interact() {
|
|
if let Some(def) = def {
|
|
eprintln!("{} {}: {}", msg, options, if def { "yes" } else { "no" });
|
|
return def;
|
|
} else {
|
|
quit_error_msg(
|
|
format!(
|
|
"could not prompt question '{}' in no-interact mode, maybe specify it",
|
|
msg,
|
|
),
|
|
ErrorHints::default(),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Get the user input
|
|
let answer = prompt(&format!("{} {}", msg, options), main_matcher);
|
|
|
|
// Assume the default if the answer is empty
|
|
if answer.is_empty() && def.is_some() {
|
|
return def.unwrap();
|
|
}
|
|
|
|
// Derive a boolean and return
|
|
match derive_bool(&answer) {
|
|
Some(answer) => answer,
|
|
None => prompt_yes(msg, def, main_matcher),
|
|
}
|
|
}
|
|
|
|
/// Try to derive true or false (yes or no) from the given input.
|
|
/// None is returned if no boolean could be derived accurately.
|
|
fn derive_bool(input: &str) -> Option<bool> {
|
|
// Process the input
|
|
let input = input.trim().to_lowercase();
|
|
|
|
// Handle short or incomplete answers
|
|
match input.as_str() {
|
|
"y" | "ye" | "t" | "1" => return Some(true),
|
|
"n" | "f" | "0" => return Some(false),
|
|
_ => {}
|
|
}
|
|
|
|
// Handle complete answers with any suffix
|
|
if input.starts_with("yes") || input.starts_with("true") {
|
|
return Some(true);
|
|
}
|
|
if input.starts_with("no") || input.starts_with("false") {
|
|
return Some(false);
|
|
}
|
|
|
|
// The answer could not be determined, return none
|
|
None
|
|
}
|
|
|
|
/// Prompt the user to enter an owner token.
|
|
pub fn prompt_owner_token(main_matcher: &MainMatcher, optional: bool) -> String {
|
|
prompt(
|
|
if optional {
|
|
"Owner token (optional)"
|
|
} else {
|
|
"Owner token"
|
|
},
|
|
main_matcher,
|
|
)
|
|
}
|
|
|
|
/// Get the owner token.
|
|
/// This method will ensure an owner token is set in the given `token`
|
|
/// parameter.
|
|
///
|
|
/// This method will prompt the user for the token, if it wasn't set.
|
|
///
|
|
/// Returns if an owner token was set.
|
|
/// If `optional` is false, this always returns true.
|
|
///
|
|
/// If in non-interactive or force mode, the user will not be prompted for a token if `optional` is
|
|
/// set to true.
|
|
pub fn ensure_owner_token(
|
|
token: &mut Option<String>,
|
|
main_matcher: &MainMatcher,
|
|
optional: bool,
|
|
) -> bool {
|
|
// Check whether we allow interaction
|
|
let interact = !main_matcher.no_interact();
|
|
|
|
// Notify that an owner token is required
|
|
if interact && token.is_none() {
|
|
if optional {
|
|
println!("The file owner token is recommended for authentication.");
|
|
} else {
|
|
println!("The file owner token is required for authentication.");
|
|
}
|
|
}
|
|
|
|
loop {
|
|
// Prompt for an owner token if not set yet
|
|
if token.is_none() {
|
|
// Do not ask for a token if optional when non-interactive or forced
|
|
if optional && (!interact || main_matcher.force()) {
|
|
return false;
|
|
}
|
|
|
|
// Ask for the token, or quit with an error if non-interactive
|
|
if interact {
|
|
*token = Some(prompt_owner_token(main_matcher, optional));
|
|
} else {
|
|
quit_error_msg(
|
|
"missing owner token, must be specified in no-interact mode",
|
|
ErrorHintsBuilder::default()
|
|
.owner(true)
|
|
.verbose(false)
|
|
.build()
|
|
.unwrap(),
|
|
);
|
|
}
|
|
}
|
|
|
|
// The token must not be empty, unless it's optional
|
|
let empty = token.as_ref().unwrap().is_empty();
|
|
if empty {
|
|
*token = None;
|
|
}
|
|
if empty && !optional {
|
|
eprintln!(
|
|
"Empty owner token given, which is invalid. Use {} to cancel.",
|
|
highlight("[CTRL+C]"),
|
|
);
|
|
} else {
|
|
return !empty;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Format the given number of bytes readable for humans.
|
|
pub fn format_bytes(bytes: u64) -> String {
|
|
let bytes = bytes as f64;
|
|
let kb = 1024f64;
|
|
match bytes {
|
|
bytes if bytes >= kb.powf(4_f64) => format!("{:.*} TiB", 2, bytes / kb.powf(4_f64)),
|
|
bytes if bytes >= kb.powf(3_f64) => format!("{:.*} GiB", 2, bytes / kb.powf(3_f64)),
|
|
bytes if bytes >= kb.powf(2_f64) => format!("{:.*} MiB", 2, bytes / kb.powf(2_f64)),
|
|
bytes if bytes >= kb => format!("{:.*} KiB", 2, bytes / kb),
|
|
_ => format!("{:.*} B", 0, bytes),
|
|
}
|
|
}
|
|
|
|
/// Parse the given duration string from human readable format into seconds.
|
|
/// This method parses a string of time components to represent the given duration.
|
|
///
|
|
/// The following time units are used:
|
|
/// - `w`: weeks
|
|
/// - `d`: days
|
|
/// - `h`: hours
|
|
/// - `m`: minutes
|
|
/// - `s`: seconds
|
|
/// The following time strings can be parsed:
|
|
/// - `8w6d`
|
|
/// - `23h14m`
|
|
/// - `9m55s`
|
|
/// - `1s1s1s1s1s`
|
|
pub fn parse_duration(duration: &str) -> Result<usize, ParseDurationError> {
|
|
// Build a regex to grab time parts
|
|
let re = Regex::new(r"(?i)([0-9]+)(([a-z]|\s*$))")
|
|
.expect("failed to compile duration parsing regex");
|
|
|
|
// We must find any match
|
|
if re.find(duration).is_none() {
|
|
return Err(ParseDurationError::Empty);
|
|
}
|
|
|
|
// Parse each time part, sum it's seconds
|
|
let mut seconds = 0;
|
|
for capture in re.captures_iter(duration) {
|
|
// Parse time value and modifier
|
|
let number = capture[1]
|
|
.parse::<usize>()
|
|
.map_err(ParseDurationError::InvalidValue)?;
|
|
let modifier = capture[2].trim().to_lowercase();
|
|
|
|
// Multiply and sum seconds by modifier
|
|
seconds += match modifier.as_str() {
|
|
"" | "s" => number,
|
|
"m" => number * 60,
|
|
"h" => number * 60 * 60,
|
|
"d" => number * 60 * 60 * 24,
|
|
"w" => number * 60 * 60 * 24 * 7,
|
|
m => return Err(ParseDurationError::UnknownIdentifier(m.into())),
|
|
};
|
|
}
|
|
|
|
Ok(seconds)
|
|
}
|
|
|
|
/// Represents a duration parsing error.
|
|
#[derive(Debug, Fail)]
|
|
pub enum ParseDurationError {
|
|
/// The given duration string did not contain any duration part.
|
|
#[fail(display = "given string did not contain any duration part")]
|
|
Empty,
|
|
|
|
/// A numeric value was invalid.
|
|
#[fail(display = "duration part has invalid numeric value")]
|
|
InvalidValue(std::num::ParseIntError),
|
|
|
|
/// The given duration string contained an invalid duration modifier.
|
|
#[fail(display = "duration part has unknown time identifier '{}'", _0)]
|
|
UnknownIdentifier(String),
|
|
}
|
|
|
|
/// Format the given duration in a human readable format.
|
|
/// This method builds a string of time components to represent
|
|
/// the given duration.
|
|
///
|
|
/// The following time units are used:
|
|
/// - `w`: weeks
|
|
/// - `d`: days
|
|
/// - `h`: hours
|
|
/// - `m`: minutes
|
|
/// - `s`: seconds
|
|
///
|
|
/// Only the two most significant units are returned.
|
|
/// If the duration is zero seconds or less `now` is returned.
|
|
///
|
|
/// The following time strings may be produced:
|
|
/// - `8w6d`
|
|
/// - `23h14m`
|
|
/// - `9m55s`
|
|
/// - `1s`
|
|
/// - `now`
|
|
pub fn format_duration(duration: impl Borrow<Duration>) -> String {
|
|
// Get the total number of seconds, return immediately if zero or less
|
|
let mut secs = duration.borrow().num_seconds();
|
|
if secs <= 0 {
|
|
return "now".into();
|
|
}
|
|
|
|
// Build a list of time units, define a list for time components
|
|
let mut components = Vec::new();
|
|
let units = [
|
|
(60 * 60 * 24 * 7, "w"),
|
|
(60 * 60 * 24, "d"),
|
|
(60 * 60, "h"),
|
|
(60, "m"),
|
|
(1, "s"),
|
|
];
|
|
|
|
// Fill the list of time components based on the units which fit
|
|
for unit in &units {
|
|
if secs >= unit.0 {
|
|
components.push(format!("{}{}", secs / unit.0, unit.1));
|
|
secs %= unit.0;
|
|
}
|
|
}
|
|
|
|
// Show only the two most significant components and join them in a string
|
|
components.truncate(2);
|
|
components.join("")
|
|
}
|
|
|
|
/// Format the given boolean, as `yes` or `no`.
|
|
pub fn format_bool(b: bool) -> &'static str {
|
|
if b {
|
|
"yes"
|
|
} else {
|
|
"no"
|
|
}
|
|
}
|
|
|
|
/// Get the name of the executable that was invoked.
|
|
///
|
|
/// When a symbolic or hard link is used, the name of the link is returned.
|
|
///
|
|
/// This attempts to obtain the binary name in the following order:
|
|
/// - name in first item of program arguments via `std::env::args`
|
|
/// - current executable name via `std::env::current_exe`
|
|
/// - crate name
|
|
pub fn bin_name() -> String {
|
|
env::args_os()
|
|
.next()
|
|
.filter(|path| !path.is_empty())
|
|
.map(PathBuf::from)
|
|
.or_else(|| current_exe().ok())
|
|
.and_then(|p| p.file_name().map(|n| n.to_owned()))
|
|
.and_then(|n| n.into_string().ok())
|
|
.unwrap_or_else(|| crate_name!().into())
|
|
}
|
|
|
|
/// Ensure that there is enough free disk space available at the given `path`,
|
|
/// to store a file with the given `size`.
|
|
///
|
|
/// If an error occurred while querying the file system,
|
|
/// the error is reported to the user and the method returns.
|
|
///
|
|
/// If there is not enough disk space available,
|
|
/// an error is reported and the program will quit.
|
|
pub fn ensure_enough_space<P: AsRef<Path>>(path: P, size: u64) {
|
|
// Get the available space at this path
|
|
let space = match available_space(path) {
|
|
Ok(space) => space,
|
|
Err(err) => {
|
|
print_error(err.context("failed to check available space on disk, ignoring"));
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Return if enough disk space is available
|
|
if space >= size {
|
|
return;
|
|
}
|
|
|
|
// Create an info message giving details about the required space
|
|
let info = format!(
|
|
"{} of space required, but only {} is available",
|
|
format_bytes(size),
|
|
format_bytes(space),
|
|
);
|
|
|
|
// Print an descriptive error and quit
|
|
quit_error(
|
|
err_msg("not enough disk space available in the target directory")
|
|
.context("failed to download file"),
|
|
ErrorHintsBuilder::default()
|
|
.add_info(info)
|
|
.force(true)
|
|
.verbose(false)
|
|
.build()
|
|
.unwrap(),
|
|
);
|
|
}
|
|
|
|
/// Get the project directories instance for this application.
|
|
/// This may be used to determine the project, cache, configuration, data and
|
|
/// some other directory paths.
|
|
#[cfg(feature = "history")]
|
|
pub fn app_project_dirs() -> ProjectDirs {
|
|
ProjectDirs::from("", "", crate_name!())
|
|
.expect("failed to determine location of project directories")
|
|
}
|
|
|
|
/// Get the default path to use for the history file.
|
|
#[cfg(feature = "history")]
|
|
pub fn app_history_file_path() -> PathBuf {
|
|
app_project_dirs().cache_dir().join("history.toml")
|
|
}
|
|
|
|
/// Get the default path to use for the history file, as a string.
|
|
#[cfg(feature = "history")]
|
|
pub fn app_history_file_path_string() -> String {
|
|
app_history_file_path().to_str().unwrap().to_owned()
|
|
}
|
|
|
|
/// Check whether an environment variable with the given key is present in the context of the
|
|
/// current process. The environment variable doesn't have to hold any specific value.
|
|
/// Returns `true` if present, `false` if not.
|
|
pub fn env_var_present(key: impl AsRef<OsStr>) -> bool {
|
|
var_os(key).is_some()
|
|
}
|
|
|
|
/// Get a list of all features that were enabled during compilation.
|
|
pub fn features_list() -> Vec<&'static str> {
|
|
// Build the list
|
|
#[allow(unused_mut)]
|
|
let mut features = Vec::new();
|
|
|
|
// Add each feature
|
|
#[cfg(feature = "archive")]
|
|
features.push("archive");
|
|
#[cfg(feature = "clipboard")]
|
|
features.push("clipboard");
|
|
#[cfg(feature = "clipboard-bin")]
|
|
features.push("clipboard-bin");
|
|
#[cfg(feature = "clipboard-crate")]
|
|
features.push("clipboard-crate");
|
|
#[cfg(feature = "history")]
|
|
features.push("history");
|
|
#[cfg(feature = "qrcode")]
|
|
features.push("qrcode");
|
|
#[cfg(feature = "urlshorten")]
|
|
features.push("urlshorten");
|
|
#[cfg(feature = "infer-command")]
|
|
features.push("infer-command");
|
|
#[cfg(feature = "no-qcolor")]
|
|
features.push("no-color");
|
|
#[cfg(feature = "send2")]
|
|
features.push("send2");
|
|
#[cfg(feature = "send3")]
|
|
features.push("send3");
|
|
#[cfg(feature = "crypto-ring")]
|
|
features.push("crypto-ring");
|
|
#[cfg(feature = "crypto-openssl")]
|
|
features.push("crypto-openssl");
|
|
|
|
features
|
|
}
|
|
|
|
/// Get a list of supported API versions.
|
|
pub fn api_version_list() -> Vec<&'static str> {
|
|
// Build the list
|
|
#[allow(unused_mut)]
|
|
let mut versions = Vec::new();
|
|
|
|
// Add each feature
|
|
#[cfg(feature = "send2")]
|
|
versions.push("v2");
|
|
#[cfg(feature = "send3")]
|
|
versions.push("v3");
|
|
|
|
versions
|
|
}
|
|
|
|
/// Follow redirects on the given URL, and return the final full URL.
|
|
///
|
|
/// This is used to obtain share URLs from shortened links.
|
|
///
|
|
// TODO: extract this into module
|
|
pub fn follow_url(client: &Client, url: &Url) -> Result<Url, FollowError> {
|
|
// Send the request, follow the URL, ensure success
|
|
let response = client
|
|
.get(url.as_str())
|
|
.send()
|
|
.map_err(FollowError::Request)?;
|
|
ensure_success(&response)?;
|
|
|
|
// Obtain the final URL
|
|
Ok(response.url().clone())
|
|
}
|
|
|
|
/// URL following error.
|
|
#[derive(Debug, Fail)]
|
|
pub enum FollowError {
|
|
/// Failed to send the shortening request.
|
|
#[fail(display = "failed to send URL follow request")]
|
|
Request(#[cause] reqwest::Error),
|
|
|
|
/// The server responded with a bad response.
|
|
#[fail(display = "failed to shorten URL, got bad response")]
|
|
Response(#[cause] ResponseError),
|
|
}
|
|
|
|
impl From<ResponseError> for FollowError {
|
|
fn from(err: ResponseError) -> Self {
|
|
FollowError::Response(err)
|
|
}
|
|
}
|
|
|
|
/// Generate a random alphanumeric string with the given length.
|
|
pub fn rand_alphanum_string(len: usize) -> String {
|
|
let mut rng = thread_rng();
|
|
iter::repeat(())
|
|
.map(|()| rng.sample(Alphanumeric) as char)
|
|
.take(len)
|
|
.collect()
|
|
}
|
|
|
|
/// Read file from stdin.
|
|
pub fn stdin_read_file(prompt: bool) -> Result<Vec<u8>, StdinErr> {
|
|
if prompt {
|
|
#[cfg(not(windows))]
|
|
eprintln!("Enter input. Use [CTRL+D] to stop:");
|
|
#[cfg(windows)]
|
|
eprintln!("Enter input. Use [CTRL+Z] to stop:");
|
|
}
|
|
|
|
let mut data = vec![];
|
|
io::stdin()
|
|
.lock()
|
|
.read_to_end(&mut data)
|
|
.map_err(StdinErr::Stdin)?;
|
|
Ok(data)
|
|
}
|
|
|
|
/// URL following error.
|
|
#[derive(Debug, Fail)]
|
|
pub enum StdinErr {
|
|
#[fail(display = "failed to read from stdin")]
|
|
Stdin(#[cause] io::Error),
|
|
}
|