From 7ced1f427804acc9ba3e97136a2da6affdbb27d6 Mon Sep 17 00:00:00 2001 From: timvisee Date: Wed, 4 Apr 2018 01:23:50 +0200 Subject: [PATCH] Redo CLI command handling, make it modular with detached matchers --- api/src/action/params.rs | 1 + cli/src/action/delete.rs | 19 ++- cli/src/action/download.rs | 21 ++- cli/src/action/info.rs | 21 ++- cli/src/action/params.rs | 21 ++- cli/src/action/password.rs | 21 ++- cli/src/action/upload.rs | 29 ++-- cli/src/cmd/arg/download_limit.rs | 55 +++++++ cli/src/cmd/arg/host.rs | 53 ++++++ cli/src/cmd/arg/mod.rs | 49 ++++++ cli/src/cmd/arg/owner.rs | 31 ++++ cli/src/cmd/arg/password.rs | 51 ++++++ cli/src/cmd/arg/url.rs | 49 ++++++ cli/src/cmd/{cmd_password.rs => cmd/.rs} | 2 +- cli/src/cmd/cmd/delete.rs | 19 +++ cli/src/cmd/cmd/download.rs | 25 +++ cli/src/cmd/cmd/info.rs | 18 +++ cli/src/cmd/cmd/mod.rs | 14 ++ cli/src/cmd/cmd/params.rs | 25 +++ cli/src/cmd/cmd/password.rs | 19 +++ cli/src/cmd/cmd/upload.rs | 47 ++++++ cli/src/cmd/cmd_delete.rs | 81 ---------- cli/src/cmd/cmd_download.rs | 113 ------------- cli/src/cmd/cmd_info.rs | 109 ------------- cli/src/cmd/cmd_params.rs | 120 -------------- cli/src/cmd/cmd_upload.rs | 196 ----------------------- cli/src/cmd/handler.rs | 59 ++++--- cli/src/cmd/matcher/delete.rs | 39 +++++ cli/src/cmd/matcher/download.rs | 49 ++++++ cli/src/cmd/matcher/info.rs | 46 ++++++ cli/src/cmd/matcher/mod.rs | 21 +++ cli/src/cmd/matcher/params.rs | 44 +++++ cli/src/cmd/matcher/password.rs | 55 +++++++ cli/src/cmd/matcher/upload.rs | 87 ++++++++++ cli/src/cmd/mod.rs | 11 +- cli/src/main.rs | 25 +-- 36 files changed, 938 insertions(+), 707 deletions(-) create mode 100644 cli/src/cmd/arg/download_limit.rs create mode 100644 cli/src/cmd/arg/host.rs create mode 100644 cli/src/cmd/arg/mod.rs create mode 100644 cli/src/cmd/arg/owner.rs create mode 100644 cli/src/cmd/arg/password.rs create mode 100644 cli/src/cmd/arg/url.rs rename cli/src/cmd/{cmd_password.rs => cmd/.rs} (98%) create mode 100644 cli/src/cmd/cmd/delete.rs create mode 100644 cli/src/cmd/cmd/download.rs create mode 100644 cli/src/cmd/cmd/info.rs create mode 100644 cli/src/cmd/cmd/mod.rs create mode 100644 cli/src/cmd/cmd/params.rs create mode 100644 cli/src/cmd/cmd/password.rs create mode 100644 cli/src/cmd/cmd/upload.rs delete mode 100644 cli/src/cmd/cmd_delete.rs delete mode 100644 cli/src/cmd/cmd_download.rs delete mode 100644 cli/src/cmd/cmd_info.rs delete mode 100644 cli/src/cmd/cmd_params.rs delete mode 100644 cli/src/cmd/cmd_upload.rs create mode 100644 cli/src/cmd/matcher/delete.rs create mode 100644 cli/src/cmd/matcher/download.rs create mode 100644 cli/src/cmd/matcher/info.rs create mode 100644 cli/src/cmd/matcher/mod.rs create mode 100644 cli/src/cmd/matcher/params.rs create mode 100644 cli/src/cmd/matcher/password.rs create mode 100644 cli/src/cmd/matcher/upload.rs diff --git a/api/src/action/params.rs b/api/src/action/params.rs index 3549800..8f9dcfd 100644 --- a/api/src/action/params.rs +++ b/api/src/action/params.rs @@ -15,6 +15,7 @@ const HEADER_AUTH_NONCE: &'static str = "WWW-Authenticate"; /// The default download count. pub const PARAMS_DEFAULT_DOWNLOAD: u8 = 1; +pub const PARAMS_DEFAULT_DOWNLOAD_STR: &'static str = "1"; /// The minimum allowed number of downloads, enforced by the server. pub const PARAMS_DOWNLOAD_MIN: u8 = 1; diff --git a/cli/src/action/delete.rs b/cli/src/action/delete.rs index 3b21b72..e98d59f 100644 --- a/cli/src/action/delete.rs +++ b/cli/src/action/delete.rs @@ -1,3 +1,4 @@ +use clap::ArgMatches; use ffsend_api::action::delete::{ Error as DeleteError, Delete as ApiDelete, @@ -8,34 +9,40 @@ use ffsend_api::file::remote_file::{ }; use ffsend_api::reqwest::Client; -use cmd::cmd_delete::CmdDelete; +use cmd::matcher::{ + Matcher, + delete::DeleteMatcher, +}; use error::ActionError; use util::print_success; /// A file delete action. pub struct Delete<'a> { - cmd: &'a CmdDelete<'a>, + cmd_matches: &'a ArgMatches<'a>, } impl<'a> Delete<'a> { /// Construct a new delete action. - pub fn new(cmd: &'a CmdDelete<'a>) -> Self { + pub fn new(cmd_matches: &'a ArgMatches<'a>) -> Self { Self { - cmd, + cmd_matches, } } /// Invoke the delete action. // TODO: create a trait for this method pub fn invoke(&self) -> Result<(), ActionError> { + // Create the command matchers + let matcher_delete = DeleteMatcher::with(self.cmd_matches).unwrap(); + // Get the share URL - let url = self.cmd.url(); + let url = matcher_delete.url(); // Create a reqwest client let client = Client::new(); // Parse the remote file based on the share URL, get the password - let file = RemoteFile::parse_url(url, self.cmd.owner())?; + let file = RemoteFile::parse_url(url, matcher_delete.owner())?; // TODO: show an informative error if the owner token isn't set diff --git a/cli/src/action/download.rs b/cli/src/action/download.rs index 6b16514..4403995 100644 --- a/cli/src/action/download.rs +++ b/cli/src/action/download.rs @@ -1,31 +1,38 @@ use std::sync::{Arc, Mutex}; +use clap::ArgMatches; use ffsend_api::action::download::Download as ApiDownload; use ffsend_api::file::remote_file::RemoteFile; use ffsend_api::reqwest::Client; -use cmd::cmd_download::CmdDownload; +use cmd::matcher::{ + Matcher, + download::DownloadMatcher, +}; use error::ActionError; use progress::ProgressBar; /// A file download action. pub struct Download<'a> { - cmd: &'a CmdDownload<'a>, + cmd_matches: &'a ArgMatches<'a>, } impl<'a> Download<'a> { /// Construct a new download action. - pub fn new(cmd: &'a CmdDownload<'a>) -> Self { + pub fn new(cmd_matches: &'a ArgMatches<'a>) -> Self { Self { - cmd, + cmd_matches, } } /// Invoke the download action. // TODO: create a trait for this method pub fn invoke(&self) -> Result<(), ActionError> { + // Create the command matchers + let matcher_download = DownloadMatcher::with(self.cmd_matches).unwrap(); + // Get the share URL - let url = self.cmd.url(); + let url = matcher_download.url(); // Create a reqwest client let client = Client::new(); @@ -35,7 +42,7 @@ impl<'a> Download<'a> { let file = RemoteFile::parse_url(url, None)?; // Get the target file or directory - let target = self.cmd.output(); + let target = matcher_download.output(); // Create a progress bar reporter let bar = Arc::new(Mutex::new(ProgressBar::new_download())); @@ -44,7 +51,7 @@ impl<'a> Download<'a> { ApiDownload::new( &file, target, - self.cmd.password(), + matcher_download.password(), ).invoke(&client, bar)?; // TODO: open the file, or it's location diff --git a/cli/src/action/info.rs b/cli/src/action/info.rs index 11e814b..f4f6d11 100644 --- a/cli/src/action/info.rs +++ b/cli/src/action/info.rs @@ -1,3 +1,4 @@ +use clap::ArgMatches; use failure::Fail; use ffsend_api::action::exists::{ Error as ExistsError, @@ -14,34 +15,40 @@ use ffsend_api::file::remote_file::{ }; use ffsend_api::reqwest::Client; -use cmd::cmd_info::CmdInfo; +use cmd::matcher::{ + Matcher, + info::InfoMatcher, +}; use util::print_error; /// A file info action. pub struct Info<'a> { - cmd: &'a CmdInfo<'a>, + cmd_matches: &'a ArgMatches<'a>, } impl<'a> Info<'a> { /// Construct a new info action. - pub fn new(cmd: &'a CmdInfo<'a>) -> Self { + pub fn new(cmd_matches: &'a ArgMatches<'a>) -> Self { Self { - cmd, + cmd_matches, } } /// Invoke the info action. // TODO: create a trait for this method pub fn invoke(&self) -> Result<(), Error> { + // Create the command matchers + let matcher_info = InfoMatcher::with(self.cmd_matches).unwrap(); + // Get the share URL - let url = self.cmd.url(); + let url = matcher_info.url(); // Create a reqwest client let client = Client::new(); // Parse the remote file based on the share URL, get the password - let file = RemoteFile::parse_url(url, self.cmd.owner())?; - let password = self.cmd.password(); + let file = RemoteFile::parse_url(url, matcher_info.owner())?; + let password = matcher_info.password(); // TODO: show an informative error if the owner token isn't set diff --git a/cli/src/action/params.rs b/cli/src/action/params.rs index f5825f0..496e8d8 100644 --- a/cli/src/action/params.rs +++ b/cli/src/action/params.rs @@ -1,3 +1,4 @@ +use clap::ArgMatches; use ffsend_api::action::params::{ Params as ApiParams, ParamsDataBuilder, @@ -5,41 +6,47 @@ use ffsend_api::action::params::{ use ffsend_api::file::remote_file::RemoteFile; use ffsend_api::reqwest::Client; -use cmd::cmd_params::CmdParams; +use cmd::matcher::{ + Matcher, + params::ParamsMatcher, +}; use error::ActionError; use util::print_success; /// A file parameters action. pub struct Params<'a> { - cmd: &'a CmdParams<'a>, + cmd_matches: &'a ArgMatches<'a>, } impl<'a> Params<'a> { /// Construct a new parameters action. - pub fn new(cmd: &'a CmdParams<'a>) -> Self { + pub fn new(cmd_matches: &'a ArgMatches<'a>) -> Self { Self { - cmd, + cmd_matches, } } /// Invoke the parameters action. // TODO: create a trait for this method pub fn invoke(&self) -> Result<(), ActionError> { + // Create the command matchers + let matcher_params = ParamsMatcher::with(self.cmd_matches).unwrap(); + // Get the share URL - let url = self.cmd.url(); + let url = matcher_params.url(); // Create a reqwest client let client = Client::new(); // Parse the remote file based on the share URL // TODO: handle error here - let file = RemoteFile::parse_url(url, self.cmd.owner())?; + let file = RemoteFile::parse_url(url, matcher_params.owner())?; // TODO: show an informative error if the owner token isn't set // Build the parameters data object let data = ParamsDataBuilder::default() - .download_limit(self.cmd.download_limit()) + .download_limit(matcher_params.download_limit()) .build() .unwrap(); diff --git a/cli/src/action/password.rs b/cli/src/action/password.rs index 7ef7574..7b01ebf 100644 --- a/cli/src/action/password.rs +++ b/cli/src/action/password.rs @@ -1,41 +1,48 @@ +use clap::ArgMatches; use ffsend_api::action::password::Password as ApiPassword; use ffsend_api::file::remote_file::RemoteFile; use ffsend_api::reqwest::Client; -use cmd::cmd_password::CmdPassword; +use cmd::matcher::{ + Matcher, + password::PasswordMatcher, +}; use error::ActionError; use util::print_success; /// A file password action. pub struct Password<'a> { - cmd: &'a CmdPassword<'a>, + cmd_matches: &'a ArgMatches<'a>, } impl<'a> Password<'a> { /// Construct a new password action. - pub fn new(cmd: &'a CmdPassword<'a>) -> Self { + pub fn new(cmd_matches: &'a ArgMatches<'a>) -> Self { Self { - cmd, + cmd_matches, } } /// Invoke the password action. // TODO: create a trait for this method pub fn invoke(&self) -> Result<(), ActionError> { + // Create the command matchers + let matcher_password = PasswordMatcher::with(self.cmd_matches).unwrap(); + // Get the share URL - let url = self.cmd.url(); + let url = matcher_password.url(); // Create a reqwest client let client = Client::new(); // Parse the remote file based on the share URL // TODO: handle error here - let file = RemoteFile::parse_url(url, self.cmd.owner())?; + let file = RemoteFile::parse_url(url, matcher_password.owner())?; // TODO: show an informative error if the owner token isn't set // Execute an password action - ApiPassword::new(&file, &self.cmd.password(), None).invoke(&client)?; + ApiPassword::new(&file, &matcher_password.password(), None).invoke(&client)?; // Print a success message print_success("Password set"); diff --git a/cli/src/action/upload.rs b/cli/src/action/upload.rs index 83f4ad4..82c38f5 100644 --- a/cli/src/action/upload.rs +++ b/cli/src/action/upload.rs @@ -1,12 +1,16 @@ use std::path::Path; use std::sync::{Arc, Mutex}; +use clap::ArgMatches; use failure::{err_msg, Fail}; use ffsend_api::action::params::ParamsDataBuilder; use ffsend_api::action::upload::Upload as ApiUpload; use ffsend_api::reqwest::Client; -use cmd::cmd_upload::CmdUpload; +use cmd::matcher::{ + Matcher, + upload::UploadMatcher, +}; use error::ActionError; use progress::ProgressBar; use util::open_url; @@ -15,23 +19,26 @@ use util::{print_error, set_clipboard}; /// A file upload action. pub struct Upload<'a> { - cmd: &'a CmdUpload<'a>, + cmd_matches: &'a ArgMatches<'a>, } impl<'a> Upload<'a> { /// Construct a new upload action. - pub fn new(cmd: &'a CmdUpload<'a>) -> Self { + pub fn new(cmd_matches: &'a ArgMatches<'a>) -> Self { Self { - cmd, + cmd_matches, } } /// Invoke the upload action. // TODO: create a trait for this method pub fn invoke(&self) -> Result<(), ActionError> { + // Create the command matchers + let matcher_upload = UploadMatcher::with(self.cmd_matches).unwrap(); + // Get API parameters - let path = Path::new(self.cmd.file()).to_path_buf(); - let host = self.cmd.host(); + let path = Path::new(matcher_upload.file()).to_path_buf(); + let host = matcher_upload.host(); // Create a reqwest client let client = Client::new(); @@ -43,7 +50,7 @@ impl<'a> Upload<'a> { let params = { // Build the parameters data object let mut params = ParamsDataBuilder::default() - .download_limit(self.cmd.download_limit()) + .download_limit(matcher_upload.download_limit()) .build() .unwrap(); @@ -59,8 +66,8 @@ impl<'a> Upload<'a> { let file = ApiUpload::new( host, path, - self.cmd.name().map(|name| name.to_owned()), - self.cmd.password(), + matcher_upload.name().map(|name| name.to_owned()), + matcher_upload.password(), params, ).invoke(&client, bar)?; @@ -70,7 +77,7 @@ impl<'a> Upload<'a> { println!("Owner token: {}", file.owner_token().unwrap()); // Open the URL in the browser - if self.cmd.open() { + if matcher_upload.open() { if let Err(err) = open_url(url.clone()) { print_error( err.context("Failed to open the URL in the browser") @@ -81,7 +88,7 @@ impl<'a> Upload<'a> { // Copy the URL in the user's clipboard #[cfg(feature = "clipboard")] { - if self.cmd.copy() { + if matcher_upload.copy() { if set_clipboard(url.as_str().to_owned()).is_err() { print_error( err_msg("Failed to copy the URL to the clipboard") diff --git a/cli/src/cmd/arg/download_limit.rs b/cli/src/cmd/arg/download_limit.rs new file mode 100644 index 0000000..19cc6bd --- /dev/null +++ b/cli/src/cmd/arg/download_limit.rs @@ -0,0 +1,55 @@ +use clap::{Arg, ArgMatches}; +use ffsend_api::action::params::{ + PARAMS_DOWNLOAD_MIN as DOWNLOAD_MIN, + PARAMS_DOWNLOAD_MAX as DOWNLOAD_MAX, +}; + +use super::{CmdArg, CmdArgFlag, CmdArgOption}; + +/// The download limit argument. +pub struct ArgDownloadLimit { } + +impl CmdArg for ArgDownloadLimit { + fn name() -> &'static str { + "download-limit" + } + + fn build<'b, 'c>() -> Arg<'b, 'c> { + Arg::with_name("download-limit") + .long("download-limit") + .short("d") + .alias("downloads") + .alias("download") + .alias("down") + .alias("dlimit") + .alias("limit") + .alias("lim") + .alias("l") + .value_name("COUNT") + .help("The file download limit") + } +} + +impl CmdArgFlag for ArgDownloadLimit { } + +impl<'a> CmdArgOption<'a> for ArgDownloadLimit { + type Value = Option; + + fn value<'b: 'a>(matches: &'a ArgMatches<'b>) -> Self::Value { + // TODO: do not unwrap, report an error + Self::value_raw(matches) + .map(|d| d.parse::().expect("invalid download limit")) + .and_then(|d| { + // Check the download limit bounds + if d < DOWNLOAD_MIN || d > DOWNLOAD_MAX { + panic!( + "invalid download limit, must be between {} and {}", + DOWNLOAD_MIN, + DOWNLOAD_MAX, + ); + } + + Some(d) + }) + } +} diff --git a/cli/src/cmd/arg/host.rs b/cli/src/cmd/arg/host.rs new file mode 100644 index 0000000..bfd5468 --- /dev/null +++ b/cli/src/cmd/arg/host.rs @@ -0,0 +1,53 @@ +use clap::{Arg, ArgMatches}; +use ffsend_api::url::{ParseError, Url}; + +use app::SEND_DEF_HOST; +use super::{CmdArg, CmdArgOption}; +use util::quit_error_msg; + +/// The host argument. +pub struct ArgHost { } + +impl CmdArg for ArgHost { + fn name() -> &'static str { + "host" + } + + fn build<'b, 'c>() -> Arg<'b, 'c> { + Arg::with_name("host") + .long("host") + .short("h") + .alias("server") + .value_name("URL") + .default_value(SEND_DEF_HOST) + .help("The remote host to upload to") + } +} + +impl<'a> CmdArgOption<'a> for ArgHost { + type Value = Url; + + fn value<'b: 'a>(matches: &'a ArgMatches<'b>) -> Self::Value { + // Get the URL + let url = Self::value_raw(matches).expect("missing host"); + + // Parse the URL + // TODO: improve these error messages + match Url::parse(url) { + Ok(url) => url, + Err(ParseError::EmptyHost) => + quit_error_msg("Emtpy host given"), + Err(ParseError::InvalidPort) => + quit_error_msg("Invalid host port"), + Err(ParseError::InvalidIpv4Address) => + quit_error_msg("Invalid IPv4 address in host"), + Err(ParseError::InvalidIpv6Address) => + quit_error_msg("Invalid IPv6 address in host"), + Err(ParseError::InvalidDomainCharacter) => + quit_error_msg("Host domains contains an invalid character"), + Err(ParseError::RelativeUrlWithoutBase) => + quit_error_msg("Host domain doesn't contain a host"), + _ => quit_error_msg("The given host is invalid"), + } + } +} diff --git a/cli/src/cmd/arg/mod.rs b/cli/src/cmd/arg/mod.rs new file mode 100644 index 0000000..dac4f72 --- /dev/null +++ b/cli/src/cmd/arg/mod.rs @@ -0,0 +1,49 @@ +pub mod download_limit; +pub mod host; +pub mod owner; +pub mod password; +pub mod url; + +// Reexport to arg module +pub use self::download_limit::ArgDownloadLimit; +pub use self::host::ArgHost; +pub use self::owner::ArgOwner; +pub use self::password::ArgPassword; +pub use self::url::ArgUrl; + +use clap::{Arg, ArgMatches}; + +/// A generic trait, for a reusable command argument struct. +/// The `CmdArgFlag` and `CmdArgOption` traits further specify what kind of +/// argument this is. +pub trait CmdArg { + /// Get the argument name that is used as main identifier. + fn name() -> &'static str; + + /// Build the argument. + fn build<'a, 'b>() -> Arg<'a, 'b>; +} + +/// This `CmdArg` specification defines that this argument may be tested as +/// flag. This will allow to test whether the flag is present in the given +/// matches. +pub trait CmdArgFlag: CmdArg { + /// Check whether the argument is present in the given matches. + fn is_present<'a>(matches: &ArgMatches<'a>) -> bool { + matches.is_present(Self::name()) + } +} + +/// This `CmdArg` specification defines that this argument may be tested as +/// option. This will allow to fetch the value of the argument. +pub trait CmdArgOption<'a>: CmdArg { + /// The type of the argument value. + type Value; + + /// Get the argument value. + fn value<'b: 'a>(matches: &'a ArgMatches<'b>) -> Self::Value; + + /// Get the raw argument value, as a string reference. + fn value_raw<'b: 'a>(matches: &'a ArgMatches<'b>) -> Option<&'a str> { + matches.value_of(Self::name()) } +} diff --git a/cli/src/cmd/arg/owner.rs b/cli/src/cmd/arg/owner.rs new file mode 100644 index 0000000..4d7d2f6 --- /dev/null +++ b/cli/src/cmd/arg/owner.rs @@ -0,0 +1,31 @@ +use clap::{Arg, ArgMatches}; + +use super::{CmdArg, CmdArgOption}; + +/// The owner argument. +pub struct ArgOwner { } + +impl CmdArg for ArgOwner { + fn name() -> &'static str { + "owner" + } + + fn build<'b, 'c>() -> Arg<'b, 'c> { + Arg::with_name("owner") + .long("owner") + .short("o") + .alias("own") + .alias("owner-token") + .alias("token") + .value_name("TOKEN") + .help("Specify the file owner token") + } +} + +impl<'a> CmdArgOption<'a> for ArgOwner { + type Value = Option<&'a str>; + + fn value<'b: 'a>(matches: &'a ArgMatches<'b>) -> Self::Value { + Self::value_raw(matches) + } +} diff --git a/cli/src/cmd/arg/password.rs b/cli/src/cmd/arg/password.rs new file mode 100644 index 0000000..ea58bfd --- /dev/null +++ b/cli/src/cmd/arg/password.rs @@ -0,0 +1,51 @@ +use clap::{Arg, ArgMatches}; +use rpassword::prompt_password_stderr; + +use super::{CmdArg, CmdArgFlag, CmdArgOption}; + +/// The password argument. +pub struct ArgPassword { } + +impl CmdArg for ArgPassword { + fn name() -> &'static str { + "password" + } + + fn build<'b, 'c>() -> Arg<'b, 'c> { + Arg::with_name("password") + .long("password") + .short("p") + .alias("pass") + .value_name("PASSWORD") + .min_values(0) + .max_values(1) + .help("Unlock a password protected file") + } +} + +impl CmdArgFlag for ArgPassword { } + +impl<'a> CmdArgOption<'a> for ArgPassword { + type Value = Option; + + fn value<'b: 'a>(matches: &'a ArgMatches<'b>) -> Self::Value { + // The password flag must be present + if !Self::is_present(matches) { + return None; + } + + // Get the password from the argument if set + match Self::value_raw(matches) { + None => {}, + p => return p.map(|p| p.into()), + } + + // Prompt for the password + // TODO: don't unwrap/expect + // TODO: create utility function for this + Some( + prompt_password_stderr("Password: ") + .expect("failed to read password from stdin") + ) + } +} diff --git a/cli/src/cmd/arg/url.rs b/cli/src/cmd/arg/url.rs new file mode 100644 index 0000000..dffb111 --- /dev/null +++ b/cli/src/cmd/arg/url.rs @@ -0,0 +1,49 @@ +use clap::{Arg, ArgMatches}; +use ffsend_api::url::{ParseError, Url}; + +use super::{CmdArg, CmdArgOption}; +use util::quit_error_msg; + +/// The URL argument. +pub struct ArgUrl { } + +impl CmdArg for ArgUrl { + fn name() -> &'static str { + "URL" + } + + fn build<'b, 'c>() -> Arg<'b, 'c> { + Arg::with_name("URL") + .required(true) + .multiple(false) + .help("The share URL") + } +} + +impl<'a> CmdArgOption<'a> for ArgUrl { + type Value = Url; + + fn value<'b: 'a>(matches: &'a ArgMatches<'b>) -> Self::Value { + // Get the URL + let url = Self::value_raw(matches).expect("missing URL"); + + // Parse the URL + // TODO: improve these error messages + match Url::parse(url) { + Ok(url) => url, + Err(ParseError::EmptyHost) => + quit_error_msg("Emtpy host given"), + Err(ParseError::InvalidPort) => + quit_error_msg("Invalid host port"), + Err(ParseError::InvalidIpv4Address) => + quit_error_msg("Invalid IPv4 address in host"), + Err(ParseError::InvalidIpv6Address) => + quit_error_msg("Invalid IPv6 address in host"), + Err(ParseError::InvalidDomainCharacter) => + quit_error_msg("Host domains contains an invalid character"), + Err(ParseError::RelativeUrlWithoutBase) => + quit_error_msg("Host domain doesn't contain a host"), + _ => quit_error_msg("The given host is invalid"), + } + } +} diff --git a/cli/src/cmd/cmd_password.rs b/cli/src/cmd/cmd/.rs similarity index 98% rename from cli/src/cmd/cmd_password.rs rename to cli/src/cmd/cmd/.rs index 55509b9..d157704 100644 --- a/cli/src/cmd/cmd_password.rs +++ b/cli/src/cmd/cmd/.rs @@ -1,6 +1,6 @@ use ffsend_api::url::{ParseError, Url}; -use super::clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{App, Arg, ArgMatches, SubCommand}; use rpassword::prompt_password_stderr; use util::quit_error_msg; diff --git a/cli/src/cmd/cmd/delete.rs b/cli/src/cmd/cmd/delete.rs new file mode 100644 index 0000000..f55c8d4 --- /dev/null +++ b/cli/src/cmd/cmd/delete.rs @@ -0,0 +1,19 @@ +use clap::{App, SubCommand}; + +use cmd::arg::{ArgOwner, ArgUrl, CmdArg}; + +/// The delete command definition. +pub struct CmdDelete; + +impl CmdDelete { + pub fn build<'a, 'b>() -> App<'a, 'b> { + SubCommand::with_name("delete") + .about("Delete a shared file.") + .visible_alias("del") + .alias("r") + .alias("rem") + .alias("remove") + .arg(ArgUrl::build()) + .arg(ArgOwner::build()) + } +} diff --git a/cli/src/cmd/cmd/download.rs b/cli/src/cmd/cmd/download.rs new file mode 100644 index 0000000..15a5732 --- /dev/null +++ b/cli/src/cmd/cmd/download.rs @@ -0,0 +1,25 @@ +use clap::{App, Arg, SubCommand}; + +use cmd::arg::{ArgPassword, ArgUrl, CmdArg}; + +/// The download command definition. +pub struct CmdDownload; + +impl CmdDownload { + pub fn build<'a, 'b>() -> App<'a, 'b> { + SubCommand::with_name("download") + .about("Download files.") + .visible_alias("d") + .visible_alias("down") + .arg(ArgUrl::build()) + .arg(ArgPassword::build()) + .arg(Arg::with_name("output") + .long("output") + .short("o") + .alias("output-file") + .alias("out") + .alias("file") + .value_name("PATH") + .help("The output file or directory")) + } +} diff --git a/cli/src/cmd/cmd/info.rs b/cli/src/cmd/cmd/info.rs new file mode 100644 index 0000000..45ae6bd --- /dev/null +++ b/cli/src/cmd/cmd/info.rs @@ -0,0 +1,18 @@ +use clap::{App, SubCommand}; + +use cmd::arg::{ArgOwner, ArgPassword, ArgUrl, CmdArg}; + +/// The info command definition. +pub struct CmdInfo; + +impl CmdInfo { + pub fn build<'a, 'b>() -> App<'a, 'b> { + SubCommand::with_name("info") + .about("Fetch info about a shared file.") + .visible_alias("i") + .alias("information") + .arg(ArgUrl::build()) + .arg(ArgOwner::build()) + .arg(ArgPassword::build()) + } +} diff --git a/cli/src/cmd/cmd/mod.rs b/cli/src/cmd/cmd/mod.rs new file mode 100644 index 0000000..8ca1272 --- /dev/null +++ b/cli/src/cmd/cmd/mod.rs @@ -0,0 +1,14 @@ +pub mod delete; +pub mod download; +pub mod info; +pub mod params; +pub mod password; +pub mod upload; + +// Reexport to cmd module +pub use self::delete::CmdDelete; +pub use self::download::CmdDownload; +pub use self::info::CmdInfo; +pub use self::params::CmdParams; +pub use self::password::CmdPassword; +pub use self::upload::CmdUpload; diff --git a/cli/src/cmd/cmd/params.rs b/cli/src/cmd/cmd/params.rs new file mode 100644 index 0000000..dfad96d --- /dev/null +++ b/cli/src/cmd/cmd/params.rs @@ -0,0 +1,25 @@ +use clap::{App, SubCommand}; + +use cmd::arg::{ArgDownloadLimit, ArgOwner, ArgUrl, CmdArg}; + +/// The params command definition. +pub struct CmdParams; + +impl CmdParams { + pub fn build<'a, 'b>() -> App<'a, 'b> { + // Create a list of parameter arguments, of which one is required + let param_args = [ + ArgDownloadLimit::name(), + ]; + + SubCommand::with_name("parameters") + .about("Change parameters of a shared file.") + .visible_alias("params") + .alias("par") + .alias("param") + .alias("parameter") + .arg(ArgUrl::build()) + .arg(ArgOwner::build()) + .arg(ArgDownloadLimit::build().required_unless_one(¶m_args)) + } +} diff --git a/cli/src/cmd/cmd/password.rs b/cli/src/cmd/cmd/password.rs new file mode 100644 index 0000000..43b0aef --- /dev/null +++ b/cli/src/cmd/cmd/password.rs @@ -0,0 +1,19 @@ +use clap::{App, SubCommand}; + +use cmd::arg::{ArgOwner, ArgPassword, ArgUrl, CmdArg}; + +/// The password command definition. +pub struct CmdPassword; + +impl CmdPassword { + pub fn build<'a, 'b>() -> App<'a, 'b> { + SubCommand::with_name("password") + .about("Change the password of a shared file.") + .visible_alias("pass") + .visible_alias("p") + .arg(ArgUrl::build()) + .arg(ArgPassword::build() + .help("Specify a password, do not prompt")) + .arg(ArgOwner::build()) + } +} diff --git a/cli/src/cmd/cmd/upload.rs b/cli/src/cmd/cmd/upload.rs new file mode 100644 index 0000000..210dbfa --- /dev/null +++ b/cli/src/cmd/cmd/upload.rs @@ -0,0 +1,47 @@ +use clap::{App, Arg, SubCommand}; +use ffsend_api::action::params::{ + PARAMS_DEFAULT_DOWNLOAD_STR as DOWNLOAD_DEFAULT, +}; + +use cmd::arg::{ArgDownloadLimit, ArgHost, ArgPassword, CmdArg}; + +/// The uplaod command definition. +pub struct CmdUpload; + +impl CmdUpload { + pub fn build<'a, 'b>() -> App<'a, 'b> { + // Build the subcommand + #[allow(unused_mut)] + let mut cmd = SubCommand::with_name("upload") + .about("Upload files.") + .visible_alias("u") + .visible_alias("up") + .arg(Arg::with_name("FILE") + .help("The file to upload") + .required(true) + .multiple(false)) + .arg(ArgPassword::build()) + .arg(ArgDownloadLimit::build().default_value(DOWNLOAD_DEFAULT)) + .arg(ArgHost::build()) + .arg(Arg::with_name("name") + .long("name") + .short("n") + .alias("file") + .alias("f") + .value_name("NAME") + .help("Rename the file being uploaded")) + .arg(Arg::with_name("open") + .long("open") + .short("o") + .help("Open the share link in your browser")); + + // Optional clipboard support + #[cfg(feature = "clipboard")] { + cmd = cmd.arg(Arg::with_name("copy") + .long("copy") + .short("c") + .help("Copy the share link to your clipboard")); + } + cmd + } +} diff --git a/cli/src/cmd/cmd_delete.rs b/cli/src/cmd/cmd_delete.rs deleted file mode 100644 index ac2231a..0000000 --- a/cli/src/cmd/cmd_delete.rs +++ /dev/null @@ -1,81 +0,0 @@ -use ffsend_api::url::{ParseError, Url}; - -use super::clap::{App, Arg, ArgMatches, SubCommand}; - -use util::quit_error_msg; - -/// The delete command. -pub struct CmdDelete<'a> { - matches: &'a ArgMatches<'a>, -} - -impl<'a: 'b, 'b> CmdDelete<'a> { - /// Build the sub command definition. - pub fn build<'y, 'z>() -> App<'y, 'z> { - // Build the subcommand - let cmd = SubCommand::with_name("delete") - .about("Delete a shared file.") - .visible_alias("d") - .visible_alias("del") - .alias("r") - .alias("rem") - .alias("remove") - .arg(Arg::with_name("URL") - .help("The share URL") - .required(true) - .multiple(false)) - .arg(Arg::with_name("owner") - .long("owner") - .short("o") - .alias("own") - .alias("owner-token") - .alias("token") - .value_name("TOKEN") - .help("File owner token")); - - cmd - } - - /// Parse CLI arguments, from the given parent command matches. - pub fn parse(parent: &'a ArgMatches<'a>) -> Option> { - parent.subcommand_matches("delete") - .map(|matches| CmdDelete { matches }) - } - - /// Get the owner token. - pub fn owner(&'a self) -> Option { - // TODO: validate the owner token if set - self.matches.value_of("owner") - .map(|token| token.to_owned()) - } - - /// Get the file share URL. - /// - /// This method parses the URL into an `Url`. - /// If the given URL is invalid, - /// the program will quit with an error message. - pub fn url(&'a self) -> Url { - // Get the host - let url = self.matches.value_of("URL") - .expect("missing URL"); - - // Parse the URL - // TODO: improve these error messages - match Url::parse(url) { - Ok(url) => url, - Err(ParseError::EmptyHost) => - quit_error_msg("Emtpy host given"), - Err(ParseError::InvalidPort) => - quit_error_msg("Invalid host port"), - Err(ParseError::InvalidIpv4Address) => - quit_error_msg("Invalid IPv4 address in host"), - Err(ParseError::InvalidIpv6Address) => - quit_error_msg("Invalid IPv6 address in host"), - Err(ParseError::InvalidDomainCharacter) => - quit_error_msg("Host domains contains an invalid character"), - Err(ParseError::RelativeUrlWithoutBase) => - quit_error_msg("Host domain doesn't contain a host"), - _ => quit_error_msg("The given host is invalid"), - } - } -} diff --git a/cli/src/cmd/cmd_download.rs b/cli/src/cmd/cmd_download.rs deleted file mode 100644 index 6025c37..0000000 --- a/cli/src/cmd/cmd_download.rs +++ /dev/null @@ -1,113 +0,0 @@ -use std::path::PathBuf; - -use ffsend_api::url::{ParseError, Url}; - -use rpassword::prompt_password_stderr; -use super::clap::{App, Arg, ArgMatches, SubCommand}; - -use util::quit_error_msg; - -/// The download command. -pub struct CmdDownload<'a> { - matches: &'a ArgMatches<'a>, -} - -impl<'a: 'b, 'b> CmdDownload<'a> { - /// Build the sub command definition. - pub fn build<'y, 'z>() -> App<'y, 'z> { - // Build the subcommand - let cmd = SubCommand::with_name("download") - .about("Download files.") - .visible_alias("d") - .visible_alias("down") - .arg(Arg::with_name("URL") - .help("The share URL") - .required(true) - .multiple(false)) - .arg(Arg::with_name("output") - .long("output") - .short("o") - .alias("output-file") - .alias("out") - .alias("file") - .value_name("PATH") - .help("The output file or directory")) - .arg(Arg::with_name("password") - .long("password") - .short("p") - .alias("pass") - .value_name("PASSWORD") - .min_values(0) - .max_values(1) - .help("Unlock a password protected file")); - - cmd - } - - /// Parse CLI arguments, from the given parent command matches. - pub fn parse(parent: &'a ArgMatches<'a>) -> Option> { - parent.subcommand_matches("download") - .map(|matches| CmdDownload { matches }) - } - - /// Get the file share URL, to download the file from. - /// - /// This method parses the URL into an `Url`. - /// If the given URL is invalid, - /// the program will quit with an error message. - pub fn url(&'a self) -> Url { - // Get the host - let url = self.matches.value_of("URL") - .expect("missing URL"); - - // Parse the URL - // TODO: improve these error messages - match Url::parse(url) { - Ok(url) => url, - Err(ParseError::EmptyHost) => - quit_error_msg("Emtpy host given"), - Err(ParseError::InvalidPort) => - quit_error_msg("Invalid host port"), - Err(ParseError::InvalidIpv4Address) => - quit_error_msg("Invalid IPv4 address in host"), - Err(ParseError::InvalidIpv6Address) => - quit_error_msg("Invalid IPv6 address in host"), - Err(ParseError::InvalidDomainCharacter) => - quit_error_msg("Host domains contains an invalid character"), - Err(ParseError::RelativeUrlWithoutBase) => - quit_error_msg("Host domain doesn't contain a host"), - _ => quit_error_msg("The given host is invalid"), - } - } - - /// The target file or directory to download the file to. - /// If a directory is given, the file name of the original uploaded file - /// will be used. - pub fn output(&'a self) -> PathBuf { - self.matches.value_of("output") - .map(|path| PathBuf::from(path)) - .unwrap_or(PathBuf::from("./")) - } - - /// Get the password. - /// `None` is returned if no password was specified. - pub fn password(&'a self) -> Option { - // Return none if the property was not set - if !self.matches.is_present("password") { - return None; - } - - // Get the password from the arguments - if let Some(password) = self.matches.value_of("password") { - return Some(password.into()); - } - - // Prompt for the password - // TODO: don't unwrap/expect - // TODO: create utility function for this - Some( - prompt_password_stderr("Password: ") - .expect("failed to read password from stdin") - ) - } -} diff --git a/cli/src/cmd/cmd_info.rs b/cli/src/cmd/cmd_info.rs deleted file mode 100644 index 0e260ff..0000000 --- a/cli/src/cmd/cmd_info.rs +++ /dev/null @@ -1,109 +0,0 @@ -use ffsend_api::url::{ParseError, Url}; - -use rpassword::prompt_password_stderr; -use super::clap::{App, Arg, ArgMatches, SubCommand}; - -use util::quit_error_msg; - -/// The info command. -pub struct CmdInfo<'a> { - matches: &'a ArgMatches<'a>, -} - -impl<'a: 'b, 'b> CmdInfo<'a> { - /// Build the sub command definition. - pub fn build<'y, 'z>() -> App<'y, 'z> { - // Build the subcommand - let cmd = SubCommand::with_name("info") - .about("Fetch info about a shared file.") - .visible_alias("i") - .alias("information") - .arg(Arg::with_name("URL") - .help("The share URL") - .required(true) - .multiple(false)) - .arg(Arg::with_name("owner") - .long("owner") - .short("o") - .alias("own") - .alias("owner-token") - .alias("token") - .value_name("TOKEN") - .help("File owner token")) - .arg(Arg::with_name("password") - .long("password") - .short("p") - .alias("pass") - .value_name("PASSWORD") - .min_values(0) - .max_values(1) - .help("Unlock a password protected file")); - - cmd - } - - /// Parse CLI arguments, from the given parent command matches. - pub fn parse(parent: &'a ArgMatches<'a>) -> Option> { - parent.subcommand_matches("info") - .map(|matches| CmdInfo { matches }) - } - - /// Get the owner token. - pub fn owner(&'a self) -> Option { - // TODO: validate the owner token if set - self.matches.value_of("owner") - .map(|token| token.to_owned()) - } - - /// Get the file share URL. - /// - /// This method parses the URL into an `Url`. - /// If the given URL is invalid, - /// the program will quit with an error message. - pub fn url(&'a self) -> Url { - // Get the host - let url = self.matches.value_of("URL") - .expect("missing URL"); - - // Parse the URL - // TODO: improve these error messages - match Url::parse(url) { - Ok(url) => url, - Err(ParseError::EmptyHost) => - quit_error_msg("Emtpy host given"), - Err(ParseError::InvalidPort) => - quit_error_msg("Invalid host port"), - Err(ParseError::InvalidIpv4Address) => - quit_error_msg("Invalid IPv4 address in host"), - Err(ParseError::InvalidIpv6Address) => - quit_error_msg("Invalid IPv6 address in host"), - Err(ParseError::InvalidDomainCharacter) => - quit_error_msg("Host domains contains an invalid character"), - Err(ParseError::RelativeUrlWithoutBase) => - quit_error_msg("Host domain doesn't contain a host"), - _ => quit_error_msg("The given host is invalid"), - } - } - - /// Get the password. - /// `None` is returned if no password was specified. - pub fn password(&'a self) -> Option { - // Return none if the property was not set - if !self.matches.is_present("password") { - return None; - } - - // Get the password from the arguments - if let Some(password) = self.matches.value_of("password") { - return Some(password.into()); - } - - // Prompt for the password - // TODO: don't unwrap/expect - // TODO: create utility function for this - Some( - prompt_password_stderr("Password: ") - .expect("failed to read password from stdin") - ) - } -} diff --git a/cli/src/cmd/cmd_params.rs b/cli/src/cmd/cmd_params.rs deleted file mode 100644 index 95252af..0000000 --- a/cli/src/cmd/cmd_params.rs +++ /dev/null @@ -1,120 +0,0 @@ -use ffsend_api::action::params::{ - PARAMS_DOWNLOAD_MIN as DOWNLOAD_MIN, - PARAMS_DOWNLOAD_MAX as DOWNLOAD_MAX, -}; -use ffsend_api::url::{ParseError, Url}; - -use super::clap::{App, Arg, ArgMatches, SubCommand}; - -use util::quit_error_msg; - -/// The parameters command. -pub struct CmdParams<'a> { - matches: &'a ArgMatches<'a>, -} - -impl<'a: 'b, 'b> CmdParams<'a> { - /// Build the sub command definition. - pub fn build<'y, 'z>() -> App<'y, 'z> { - // Build a list of data parameter arguments of which one is required - let param_args = ["download-limit"]; - - // Build the subcommand - let cmd = SubCommand::with_name("parameters") - .about("Change parameters of a shared file.") - .visible_alias("params") - .alias("par") - .alias("param") - .alias("parameter") - .arg(Arg::with_name("URL") - .help("The share URL") - .required(true) - .multiple(false)) - .arg(Arg::with_name("owner") - .long("owner") - .short("o") - .alias("own") - .alias("owner-token") - .alias("token") - .value_name("TOKEN") - .help("File owner token")) - .arg(Arg::with_name("download-limit") - .long("download-limit") - .short("d") - .alias("downloads") - .alias("download") - .alias("down") - .alias("dlimit") - .alias("limit") - .alias("lim") - .alias("l") - .required_unless_one(¶m_args) - .value_name("COUNT") - .help("Set the download limit parameter")); - - cmd - } - - /// Parse CLI arguments, from the given parent command matches. - pub fn parse(parent: &'a ArgMatches<'a>) -> Option> { - parent.subcommand_matches("parameters") - .map(|matches| CmdParams { matches }) - } - - /// Get the file share URL. - /// - /// This method parses the URL into an `Url`. - /// If the given URL is invalid, - /// the program will quit with an error message. - pub fn url(&'a self) -> Url { - // Get the host - let url = self.matches.value_of("URL") - .expect("missing URL"); - - // Parse the URL - // TODO: improve these error messages - match Url::parse(url) { - Ok(url) => url, - Err(ParseError::EmptyHost) => - quit_error_msg("Emtpy host given"), - Err(ParseError::InvalidPort) => - quit_error_msg("Invalid host port"), - Err(ParseError::InvalidIpv4Address) => - quit_error_msg("Invalid IPv4 address in host"), - Err(ParseError::InvalidIpv6Address) => - quit_error_msg("Invalid IPv6 address in host"), - Err(ParseError::InvalidDomainCharacter) => - quit_error_msg("Host domains contains an invalid character"), - Err(ParseError::RelativeUrlWithoutBase) => - quit_error_msg("Host domain doesn't contain a host"), - _ => quit_error_msg("The given host is invalid"), - } - } - - /// Get the owner token. - pub fn owner(&'a self) -> Option { - // TODO: validate the owner token if set - self.matches.value_of("owner") - .map(|token| token.to_owned()) - } - - /// Get the download limit. - pub fn download_limit(&'a self) -> Option { - // TODO: do not unwrap, report an error - self.matches.value_of("download-limit") - .map(|d| d.parse::().expect("invalid download limit")) - .and_then(|d| { - // Check the download limit bounds - if d < DOWNLOAD_MIN || d > DOWNLOAD_MAX { - panic!( - "invalid download limit, must be between {} and {}", - DOWNLOAD_MIN, - DOWNLOAD_MAX, - ); - } - - // Return the value - Some(d) - }) - } -} diff --git a/cli/src/cmd/cmd_upload.rs b/cli/src/cmd/cmd_upload.rs deleted file mode 100644 index b6c1148..0000000 --- a/cli/src/cmd/cmd_upload.rs +++ /dev/null @@ -1,196 +0,0 @@ -use ffsend_api::action::params::{ - PARAMS_DEFAULT_DOWNLOAD as DOWNLOAD_DEFAULT, - PARAMS_DOWNLOAD_MIN as DOWNLOAD_MIN, - PARAMS_DOWNLOAD_MAX as DOWNLOAD_MAX, -}; -use ffsend_api::url::{ParseError, Url}; - -use rpassword::prompt_password_stderr; -use super::clap::{App, Arg, ArgMatches, SubCommand}; - -use app::SEND_DEF_HOST; -use util::quit_error_msg; - -/// The upload command. -pub struct CmdUpload<'a> { - matches: &'a ArgMatches<'a>, -} - -impl<'a: 'b, 'b> CmdUpload<'a> { - /// Build the sub command definition. - pub fn build<'y, 'z>() -> App<'y, 'z> { - // Build the subcommand - #[allow(unused_mut)] - let mut cmd = SubCommand::with_name("upload") - .about("Upload files.") - .visible_alias("u") - .visible_alias("up") - .arg(Arg::with_name("FILE") - .help("The file to upload") - .required(true) - .multiple(false)) - .arg(Arg::with_name("name") - .long("name") - .short("n") - .alias("file") - .alias("f") - .value_name("NAME") - .help("Rename the file being uploaded")) - .arg(Arg::with_name("password") - .long("password") - .short("p") - .alias("pass") - .value_name("PASSWORD") - .min_values(0) - .max_values(1) - .help("Protect the file with a password")) - .arg(Arg::with_name("downloads-limit") - .long("download-limit") - .short("d") - .alias("downloads") - .alias("download") - .alias("down") - .alias("dlimit") - .alias("limit") - .alias("lim") - .alias("l") - .value_name("COUNT") - .default_value("1") - .help("Set the download limit")) - .arg(Arg::with_name("host") - .long("host") - .short("h") - .alias("server") - .value_name("URL") - .default_value(SEND_DEF_HOST) - .help("The Send host to upload to")) - .arg(Arg::with_name("open") - .long("open") - .short("o") - .help("Open the share link in your browser")); - - // Optional clipboard support - #[cfg(feature = "clipboard")] { - cmd = cmd.arg(Arg::with_name("copy") - .long("copy") - .short("c") - .help("Copy the share link to your clipboard")); - } - - cmd - } - - /// Parse CLI arguments, from the given parent command matches. - pub fn parse(parent: &'a ArgMatches<'a>) -> Option> { - parent.subcommand_matches("upload") - .map(|matches| CmdUpload { matches }) - } - - /// The the name to use for the uploaded file. - /// If no custom name is given, none is returned. - // TODO: validate custom names, no path separators - // TODO: only allow extension renaming with force flag - pub fn name(&'a self) -> Option<&'a str> { - // Get the chosen file name - let name = self.matches.value_of("name")?; - - // The file name must not be empty - if name.trim().is_empty() { - // TODO: return an error here - panic!("the new name must not be empty"); - } - - Some(name) - } - - /// Get the selected file to upload. - // TODO: maybe return a file or path instance here - pub fn file(&'a self) -> &'a str { - self.matches.value_of("FILE") - .expect("no file specified to upload") - } - - /// Get the host to upload to. - /// - /// This method parses the host into an `Url`. - /// If the given host is invalid, - /// the program will quit with an error message. - pub fn host(&'a self) -> Url { - // Get the host - let host = self.matches.value_of("host") - .expect("missing host"); - - // Parse the URL - match Url::parse(host) { - Ok(url) => url, - Err(ParseError::EmptyHost) => - quit_error_msg("Emtpy host given"), - Err(ParseError::InvalidPort) => - quit_error_msg("Invalid host port"), - Err(ParseError::InvalidIpv4Address) => - quit_error_msg("Invalid IPv4 address in host"), - Err(ParseError::InvalidIpv6Address) => - quit_error_msg("Invalid IPv6 address in host"), - Err(ParseError::InvalidDomainCharacter) => - quit_error_msg("Host domains contains an invalid character"), - Err(ParseError::RelativeUrlWithoutBase) => - quit_error_msg("Host domain doesn't contain a host"), - _ => quit_error_msg("The given host is invalid"), - } - } - - /// Check whether to open the file URL in the user's browser. - pub fn open(&self) -> bool { - self.matches.is_present("open") - } - - /// Check whether to copy the file URL in the user's clipboard. - #[cfg(feature = "clipboard")] - pub fn copy(&self) -> bool { - self.matches.is_present("copy") - } - - /// Get the password. - /// `None` is returned if no password was specified. - pub fn password(&'a self) -> Option { - // Return none if the property was not set - if !self.matches.is_present("password") { - return None; - } - - // Get the password from the arguments - if let Some(password) = self.matches.value_of("password") { - return Some(password.into()); - } - - // Prompt for the password - // TODO: don't unwrap/expect - // TODO: create utility function for this - Some( - prompt_password_stderr("Password: ") - .expect("failed to read password from stdin") - ) - } - - /// Get the download limit if set. - pub fn download_limit(&'a self) -> Option { - // Get the download limit, or None if not set or default - // TODO: do not unwrap, report an error - self.matches.value_of("download-limit") - .map(|d| d.parse::().expect("invalid download limit")) - .and_then(|d| if d == DOWNLOAD_DEFAULT { None } else { Some(d) }) - .and_then(|d| { - // Check the download limit bounds - if d < DOWNLOAD_MIN || d > DOWNLOAD_MAX { - panic!( - "invalid download limit, must be between {} and {}", - DOWNLOAD_MIN, - DOWNLOAD_MAX, - ); - } - - // Return the value - Some(d) - }) - } -} diff --git a/cli/src/cmd/handler.rs b/cli/src/cmd/handler.rs index 16e08e1..de99f8f 100644 --- a/cli/src/cmd/handler.rs +++ b/cli/src/cmd/handler.rs @@ -1,13 +1,24 @@ -use super::clap::{App, AppSettings, Arg, ArgMatches}; +use clap::{App, AppSettings, Arg, ArgMatches}; use app::*; -use super::cmd_delete::CmdDelete; -use super::cmd_download::CmdDownload; -use super::cmd_info::CmdInfo; -use super::cmd_params::CmdParams; -use super::cmd_password::CmdPassword; -use super::cmd_upload::CmdUpload; +use super::matcher::{ + DeleteMatcher, + DownloadMatcher, + InfoMatcher, + Matcher, + ParamsMatcher, + PasswordMatcher, + UploadMatcher, +}; +use super::cmd::{ + CmdDelete, + CmdDownload, + CmdInfo, + CmdParams, + CmdPassword, + CmdUpload, +}; /// CLI argument handler. pub struct Handler<'a> { @@ -22,8 +33,7 @@ impl<'a: 'b, 'b> Handler<'a> { .version(APP_VERSION) .author(APP_AUTHOR) .about(APP_ABOUT) - .global_setting(AppSettings::GlobalVersion) - .global_setting(AppSettings::VersionlessSubcommands) + .global_setting(AppSettings::GlobalVersion) .global_setting(AppSettings::VersionlessSubcommands) // TODO: enable below command when it doesn't break `p` anymore. // .global_setting(AppSettings::InferSubcommands) .arg(Arg::with_name("force") @@ -59,33 +69,38 @@ impl<'a: 'b, 'b> Handler<'a> { } } + /// Get the raw matches. + pub fn matches(&'a self) -> &'a ArgMatches { + &self.matches + } + /// Get the delete sub command, if matched. - pub fn delete(&'a self) -> Option> { - CmdDelete::parse(&self.matches) + pub fn delete(&'a self) -> Option { + DeleteMatcher::with(&self.matches) } /// Get the download sub command, if matched. - pub fn download(&'a self) -> Option> { - CmdDownload::parse(&self.matches) + pub fn download(&'a self) -> Option { + DownloadMatcher::with(&self.matches) } - /// Get the info sub command, if matched. - pub fn info(&'a self) -> Option> { - CmdInfo::parse(&self.matches) + /// Get the info matcher, if that subcommand is entered. + pub fn info(&'a self) -> Option { + InfoMatcher::with(&self.matches) } /// Get the parameters sub command, if matched. - pub fn params(&'a self) -> Option> { - CmdParams::parse(&self.matches) + pub fn params(&'a self) -> Option { + ParamsMatcher::with(&self.matches) } /// Get the password sub command, if matched. - pub fn password(&'a self) -> Option> { - CmdPassword::parse(&self.matches) + pub fn password(&'a self) -> Option { + PasswordMatcher::with(&self.matches) } /// Get the upload sub command, if matched. - pub fn upload(&'a self) -> Option> { - CmdUpload::parse(&self.matches) + pub fn upload(&'a self) -> Option { + UploadMatcher::with(&self.matches) } } diff --git a/cli/src/cmd/matcher/delete.rs b/cli/src/cmd/matcher/delete.rs new file mode 100644 index 0000000..8da7a26 --- /dev/null +++ b/cli/src/cmd/matcher/delete.rs @@ -0,0 +1,39 @@ +use clap::ArgMatches; +use ffsend_api::url::Url; + +use cmd::arg::{ArgOwner, ArgUrl, CmdArgOption}; +use super::Matcher; + +/// The delete command matcher. +pub struct DeleteMatcher<'a> { + matches: &'a ArgMatches<'a>, +} + +impl<'a: 'b, 'b> DeleteMatcher<'a> { + /// Get the file share URL. + /// + /// This method parses the URL into an `Url`. + /// If the given URL is invalid, + /// the program will quit with an error message. + pub fn url(&'a self) -> Url { + ArgUrl::value(self.matches) + } + + /// Get the owner token. + pub fn owner(&'a self) -> Option { + // TODO: just return a string reference here? + ArgOwner::value(self.matches) + .map(|token| token.to_owned()) + } +} + +impl<'a> Matcher<'a> for DeleteMatcher<'a> { + fn with(matches: &'a ArgMatches) -> Option { + matches.subcommand_matches("delete") + .map(|matches| + DeleteMatcher { + matches, + } + ) + } +} diff --git a/cli/src/cmd/matcher/download.rs b/cli/src/cmd/matcher/download.rs new file mode 100644 index 0000000..ad21038 --- /dev/null +++ b/cli/src/cmd/matcher/download.rs @@ -0,0 +1,49 @@ +use std::path::PathBuf; + +use clap::ArgMatches; +use ffsend_api::url::Url; + +use cmd::arg::{ArgPassword, ArgUrl, CmdArgOption}; +use super::Matcher; + +/// The download command matcher. +pub struct DownloadMatcher<'a> { + matches: &'a ArgMatches<'a>, +} + +impl<'a: 'b, 'b> DownloadMatcher<'a> { + /// Get the file share URL. + /// + /// This method parses the URL into an `Url`. + /// If the given URL is invalid, + /// the program will quit with an error message. + pub fn url(&'a self) -> Url { + ArgUrl::value(self.matches) + } + + /// Get the password. + /// `None` is returned if no password was specified. + pub fn password(&'a self) -> Option { + ArgPassword::value(self.matches) + } + + /// The target file or directory to download the file to. + /// If a directory is given, the file name of the original uploaded file + /// will be used. + pub fn output(&'a self) -> PathBuf { + self.matches.value_of("output") + .map(|path| PathBuf::from(path)) + .unwrap_or(PathBuf::from("./")) + } +} + +impl<'a> Matcher<'a> for DownloadMatcher<'a> { + fn with(matches: &'a ArgMatches) -> Option { + matches.subcommand_matches("download") + .map(|matches| + DownloadMatcher { + matches, + } + ) + } +} diff --git a/cli/src/cmd/matcher/info.rs b/cli/src/cmd/matcher/info.rs new file mode 100644 index 0000000..1bcd2e1 --- /dev/null +++ b/cli/src/cmd/matcher/info.rs @@ -0,0 +1,46 @@ +use ffsend_api::url::Url; + +use clap::ArgMatches; + +use cmd::arg::{ArgOwner, ArgPassword, ArgUrl, CmdArgOption}; +use super::Matcher; + +/// The info command matcher. +pub struct InfoMatcher<'a> { + matches: &'a ArgMatches<'a>, +} + +impl<'a: 'b, 'b> InfoMatcher<'a> { + /// Get the file share URL. + /// + /// This method parses the URL into an `Url`. + /// If the given URL is invalid, + /// the program will quit with an error message. + pub fn url(&'a self) -> Url { + ArgUrl::value(self.matches) + } + + /// Get the owner token. + pub fn owner(&'a self) -> Option { + // TODO: just return a string reference here? + ArgOwner::value(self.matches) + .map(|token| token.to_owned()) + } + + /// Get the password. + /// `None` is returned if no password was specified. + pub fn password(&'a self) -> Option { + ArgPassword::value(self.matches) + } +} + +impl<'a> Matcher<'a> for InfoMatcher<'a> { + fn with(matches: &'a ArgMatches) -> Option { + matches.subcommand_matches("info") + .map(|matches| + InfoMatcher { + matches, + } + ) + } +} diff --git a/cli/src/cmd/matcher/mod.rs b/cli/src/cmd/matcher/mod.rs new file mode 100644 index 0000000..3ecc589 --- /dev/null +++ b/cli/src/cmd/matcher/mod.rs @@ -0,0 +1,21 @@ +pub mod delete; +pub mod download; +pub mod info; +pub mod params; +pub mod password; +pub mod upload; + +// Reexport to matcher module +pub use self::delete::DeleteMatcher; +pub use self::download::DownloadMatcher; +pub use self::info::InfoMatcher; +pub use self::params::ParamsMatcher; +pub use self::password::PasswordMatcher; +pub use self::upload::UploadMatcher; + +use clap::ArgMatches; + +pub trait Matcher<'a>: Sized { + // Construct a new matcher instance from these argument matches. + fn with(matches: &'a ArgMatches) -> Option; +} diff --git a/cli/src/cmd/matcher/params.rs b/cli/src/cmd/matcher/params.rs new file mode 100644 index 0000000..4bc5974 --- /dev/null +++ b/cli/src/cmd/matcher/params.rs @@ -0,0 +1,44 @@ +use clap::ArgMatches; +use ffsend_api::url::Url; + +use cmd::arg::{ArgDownloadLimit, ArgOwner, ArgUrl, CmdArgOption}; +use super::Matcher; + +/// The params command matcher. +pub struct ParamsMatcher<'a> { + matches: &'a ArgMatches<'a>, +} + +impl<'a: 'b, 'b> ParamsMatcher<'a> { + /// Get the file share URL. + /// + /// This method parses the URL into an `Url`. + /// If the given URL is invalid, + /// the program will quit with an error message. + pub fn url(&'a self) -> Url { + ArgUrl::value(self.matches) + } + + /// Get the owner token. + pub fn owner(&'a self) -> Option { + // TODO: just return a string reference here? + ArgOwner::value(self.matches) + .map(|token| token.to_owned()) + } + + /// Get the download limit. + pub fn download_limit(&'a self) -> Option { + ArgDownloadLimit::value(self.matches) + } +} + +impl<'a> Matcher<'a> for ParamsMatcher<'a> { + fn with(matches: &'a ArgMatches) -> Option { + matches.subcommand_matches("parameters") + .map(|matches| + ParamsMatcher { + matches, + } + ) + } +} diff --git a/cli/src/cmd/matcher/password.rs b/cli/src/cmd/matcher/password.rs new file mode 100644 index 0000000..550ad24 --- /dev/null +++ b/cli/src/cmd/matcher/password.rs @@ -0,0 +1,55 @@ +use clap::ArgMatches; +use ffsend_api::url::Url; +use rpassword::prompt_password_stderr; + +use cmd::arg::{ArgOwner, ArgPassword, ArgUrl, CmdArgOption}; +use super::Matcher; + +/// The password command matcher. +pub struct PasswordMatcher<'a> { + matches: &'a ArgMatches<'a>, +} + +impl<'a: 'b, 'b> PasswordMatcher<'a> { + /// Get the file share URL. + /// + /// This method parses the URL into an `Url`. + /// If the given URL is invalid, + /// the program will quit with an error message. + pub fn url(&'a self) -> Url { + ArgUrl::value(self.matches) + } + + /// Get the owner token. + pub fn owner(&'a self) -> Option { + // TODO: just return a string reference here? + ArgOwner::value(self.matches) + .map(|token| token.to_owned()) + } + + /// Get the password. + pub fn password(&'a self) -> String { + // Get the password, or prompt for it + match ArgPassword::value(self.matches) { + Some(password) => password, + None => { + // Prompt for the password + // TODO: don't unwrap/expect + // TODO: create utility function for this + prompt_password_stderr("New password: ") + .expect("failed to read password from stdin") + }, + } + } +} + +impl<'a> Matcher<'a> for PasswordMatcher<'a> { + fn with(matches: &'a ArgMatches) -> Option { + matches.subcommand_matches("password") + .map(|matches| + PasswordMatcher { + matches, + } + ) + } +} diff --git a/cli/src/cmd/matcher/upload.rs b/cli/src/cmd/matcher/upload.rs new file mode 100644 index 0000000..c98cfb2 --- /dev/null +++ b/cli/src/cmd/matcher/upload.rs @@ -0,0 +1,87 @@ +use clap::ArgMatches; +use ffsend_api::action::params::{ + PARAMS_DEFAULT_DOWNLOAD as DOWNLOAD_DEFAULT, +}; +use ffsend_api::url::Url; + +use cmd::arg::{ArgDownloadLimit, ArgHost, ArgPassword, CmdArgOption}; +use super::Matcher; + +/// The upload command matcher. +pub struct UploadMatcher<'a> { + matches: &'a ArgMatches<'a>, +} + +impl<'a: 'b, 'b> UploadMatcher<'a> { + /// Get the selected file to upload. + // TODO: maybe return a file or path instance here + pub fn file(&'a self) -> &'a str { + self.matches.value_of("FILE") + .expect("no file specified to upload") + } + + /// The the name to use for the uploaded file. + /// If no custom name is given, none is returned. + // TODO: validate custom names, no path separators + // TODO: only allow extension renaming with force flag + pub fn name(&'a self) -> Option<&'a str> { + // Get the chosen file name + let name = self.matches.value_of("name")?; + + // The file name must not be empty + if name.trim().is_empty() { + // TODO: return an error here + panic!("the new name must not be empty"); + } + + Some(name) + } + + /// Get the host to upload to. + /// + /// This method parses the host into an `Url`. + /// If the given host is invalid, + /// the program will quit with an error message. + pub fn host(&'a self) -> Url { + ArgHost::value(self.matches) + } + + /// Get the password. + /// `None` is returned if no password was specified. + pub fn password(&'a self) -> Option { + ArgPassword::value(self.matches) + } + + /// Get the download limit. + /// If the download limit was the default, `None` is returned to not + /// explicitly set it. + pub fn download_limit(&'a self) -> Option { + ArgDownloadLimit::value(self.matches) + .and_then(|d| match d { + DOWNLOAD_DEFAULT => None, + d => Some(d), + }) + } + + /// Check whether to open the file URL in the user's browser. + pub fn open(&self) -> bool { + self.matches.is_present("open") + } + + /// Check whether to copy the file URL in the user's clipboard. + #[cfg(feature = "clipboard")] + pub fn copy(&self) -> bool { + self.matches.is_present("copy") + } +} + +impl<'a> Matcher<'a> for UploadMatcher<'a> { + fn with(matches: &'a ArgMatches) -> Option { + matches.subcommand_matches("upload") + .map(|matches| + UploadMatcher { + matches, + } + ) + } +} diff --git a/cli/src/cmd/mod.rs b/cli/src/cmd/mod.rs index 57be043..d4a63df 100644 --- a/cli/src/cmd/mod.rs +++ b/cli/src/cmd/mod.rs @@ -1,12 +1,7 @@ -extern crate clap; - -pub mod cmd_delete; -pub mod cmd_download; -pub mod cmd_info; -pub mod cmd_params; -pub mod cmd_password; -pub mod cmd_upload; +pub mod arg; +pub mod cmd; pub mod handler; +pub mod matcher; // Reexport modules pub use self::handler::Handler; diff --git a/cli/src/main.rs b/cli/src/main.rs index 8059052..5894d1e 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,3 +1,4 @@ +extern crate clap; extern crate failure; #[macro_use] extern crate failure_derive; @@ -38,38 +39,38 @@ fn main() { /// message. fn invoke_action(handler: &Handler) -> Result<(), Error> { // Match the delete command - if let Some(cmd) = handler.delete() { - return Delete::new(&cmd).invoke() + if handler.delete().is_some() { + return Delete::new(handler.matches()).invoke() .map_err(|err| err.into()); } // Match the download command - if let Some(cmd) = handler.download() { - return Download::new(&cmd).invoke() + if handler.download().is_some() { + return Download::new(handler.matches()).invoke() .map_err(|err| err.into()); } // Match the info command - if let Some(cmd) = handler.info() { - return Info::new(&cmd).invoke() + if handler.info().is_some() { + return Info::new(handler.matches()).invoke() .map_err(|err| err.into()); } // Match the parameters command - if let Some(cmd) = handler.params() { - return Params::new(&cmd).invoke() + if handler.params().is_some() { + return Params::new(handler.matches()).invoke() .map_err(|err| err.into()); } // Match the password command - if let Some(cmd) = handler.password() { - return Password::new(&cmd).invoke() + if handler.password().is_some() { + return Password::new(handler.matches()).invoke() .map_err(|err| err.into()); } // Match the upload command - if let Some(cmd) = handler.upload() { - return Upload::new(&cmd).invoke() + if handler.upload().is_some() { + return Upload::new(handler.matches()).invoke() .map_err(|err| err.into()); }