Redo CLI command handling, make it modular with detached matchers

This commit is contained in:
timvisee 2018-04-04 01:23:50 +02:00
parent e0e7d7d009
commit 7ced1f4278
No known key found for this signature in database
GPG key ID: 109CBA0BF74036C2
36 changed files with 938 additions and 707 deletions

View file

@ -15,6 +15,7 @@ const HEADER_AUTH_NONCE: &'static str = "WWW-Authenticate";
/// The default download count. /// The default download count.
pub const PARAMS_DEFAULT_DOWNLOAD: u8 = 1; 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. /// The minimum allowed number of downloads, enforced by the server.
pub const PARAMS_DOWNLOAD_MIN: u8 = 1; pub const PARAMS_DOWNLOAD_MIN: u8 = 1;

View file

@ -1,3 +1,4 @@
use clap::ArgMatches;
use ffsend_api::action::delete::{ use ffsend_api::action::delete::{
Error as DeleteError, Error as DeleteError,
Delete as ApiDelete, Delete as ApiDelete,
@ -8,34 +9,40 @@ use ffsend_api::file::remote_file::{
}; };
use ffsend_api::reqwest::Client; use ffsend_api::reqwest::Client;
use cmd::cmd_delete::CmdDelete; use cmd::matcher::{
Matcher,
delete::DeleteMatcher,
};
use error::ActionError; use error::ActionError;
use util::print_success; use util::print_success;
/// A file delete action. /// A file delete action.
pub struct Delete<'a> { pub struct Delete<'a> {
cmd: &'a CmdDelete<'a>, cmd_matches: &'a ArgMatches<'a>,
} }
impl<'a> Delete<'a> { impl<'a> Delete<'a> {
/// Construct a new delete action. /// Construct a new delete action.
pub fn new(cmd: &'a CmdDelete<'a>) -> Self { pub fn new(cmd_matches: &'a ArgMatches<'a>) -> Self {
Self { Self {
cmd, cmd_matches,
} }
} }
/// Invoke the delete action. /// Invoke the delete action.
// TODO: create a trait for this method // TODO: create a trait for this method
pub fn invoke(&self) -> Result<(), ActionError> { pub fn invoke(&self) -> Result<(), ActionError> {
// Create the command matchers
let matcher_delete = DeleteMatcher::with(self.cmd_matches).unwrap();
// Get the share URL // Get the share URL
let url = self.cmd.url(); let url = matcher_delete.url();
// Create a reqwest client // Create a reqwest client
let client = Client::new(); let client = Client::new();
// Parse the remote file based on the share URL, get the password // 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 // TODO: show an informative error if the owner token isn't set

View file

@ -1,31 +1,38 @@
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use clap::ArgMatches;
use ffsend_api::action::download::Download as ApiDownload; use ffsend_api::action::download::Download as ApiDownload;
use ffsend_api::file::remote_file::RemoteFile; use ffsend_api::file::remote_file::RemoteFile;
use ffsend_api::reqwest::Client; use ffsend_api::reqwest::Client;
use cmd::cmd_download::CmdDownload; use cmd::matcher::{
Matcher,
download::DownloadMatcher,
};
use error::ActionError; use error::ActionError;
use progress::ProgressBar; use progress::ProgressBar;
/// A file download action. /// A file download action.
pub struct Download<'a> { pub struct Download<'a> {
cmd: &'a CmdDownload<'a>, cmd_matches: &'a ArgMatches<'a>,
} }
impl<'a> Download<'a> { impl<'a> Download<'a> {
/// Construct a new download action. /// Construct a new download action.
pub fn new(cmd: &'a CmdDownload<'a>) -> Self { pub fn new(cmd_matches: &'a ArgMatches<'a>) -> Self {
Self { Self {
cmd, cmd_matches,
} }
} }
/// Invoke the download action. /// Invoke the download action.
// TODO: create a trait for this method // TODO: create a trait for this method
pub fn invoke(&self) -> Result<(), ActionError> { pub fn invoke(&self) -> Result<(), ActionError> {
// Create the command matchers
let matcher_download = DownloadMatcher::with(self.cmd_matches).unwrap();
// Get the share URL // Get the share URL
let url = self.cmd.url(); let url = matcher_download.url();
// Create a reqwest client // Create a reqwest client
let client = Client::new(); let client = Client::new();
@ -35,7 +42,7 @@ impl<'a> Download<'a> {
let file = RemoteFile::parse_url(url, None)?; let file = RemoteFile::parse_url(url, None)?;
// Get the target file or directory // Get the target file or directory
let target = self.cmd.output(); let target = matcher_download.output();
// Create a progress bar reporter // Create a progress bar reporter
let bar = Arc::new(Mutex::new(ProgressBar::new_download())); let bar = Arc::new(Mutex::new(ProgressBar::new_download()));
@ -44,7 +51,7 @@ impl<'a> Download<'a> {
ApiDownload::new( ApiDownload::new(
&file, &file,
target, target,
self.cmd.password(), matcher_download.password(),
).invoke(&client, bar)?; ).invoke(&client, bar)?;
// TODO: open the file, or it's location // TODO: open the file, or it's location

View file

@ -1,3 +1,4 @@
use clap::ArgMatches;
use failure::Fail; use failure::Fail;
use ffsend_api::action::exists::{ use ffsend_api::action::exists::{
Error as ExistsError, Error as ExistsError,
@ -14,34 +15,40 @@ use ffsend_api::file::remote_file::{
}; };
use ffsend_api::reqwest::Client; use ffsend_api::reqwest::Client;
use cmd::cmd_info::CmdInfo; use cmd::matcher::{
Matcher,
info::InfoMatcher,
};
use util::print_error; use util::print_error;
/// A file info action. /// A file info action.
pub struct Info<'a> { pub struct Info<'a> {
cmd: &'a CmdInfo<'a>, cmd_matches: &'a ArgMatches<'a>,
} }
impl<'a> Info<'a> { impl<'a> Info<'a> {
/// Construct a new info action. /// Construct a new info action.
pub fn new(cmd: &'a CmdInfo<'a>) -> Self { pub fn new(cmd_matches: &'a ArgMatches<'a>) -> Self {
Self { Self {
cmd, cmd_matches,
} }
} }
/// Invoke the info action. /// Invoke the info action.
// TODO: create a trait for this method // TODO: create a trait for this method
pub fn invoke(&self) -> Result<(), Error> { pub fn invoke(&self) -> Result<(), Error> {
// Create the command matchers
let matcher_info = InfoMatcher::with(self.cmd_matches).unwrap();
// Get the share URL // Get the share URL
let url = self.cmd.url(); let url = matcher_info.url();
// Create a reqwest client // Create a reqwest client
let client = Client::new(); let client = Client::new();
// Parse the remote file based on the share URL, get the password // 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_info.owner())?;
let password = self.cmd.password(); let password = matcher_info.password();
// TODO: show an informative error if the owner token isn't set // TODO: show an informative error if the owner token isn't set

View file

@ -1,3 +1,4 @@
use clap::ArgMatches;
use ffsend_api::action::params::{ use ffsend_api::action::params::{
Params as ApiParams, Params as ApiParams,
ParamsDataBuilder, ParamsDataBuilder,
@ -5,41 +6,47 @@ use ffsend_api::action::params::{
use ffsend_api::file::remote_file::RemoteFile; use ffsend_api::file::remote_file::RemoteFile;
use ffsend_api::reqwest::Client; use ffsend_api::reqwest::Client;
use cmd::cmd_params::CmdParams; use cmd::matcher::{
Matcher,
params::ParamsMatcher,
};
use error::ActionError; use error::ActionError;
use util::print_success; use util::print_success;
/// A file parameters action. /// A file parameters action.
pub struct Params<'a> { pub struct Params<'a> {
cmd: &'a CmdParams<'a>, cmd_matches: &'a ArgMatches<'a>,
} }
impl<'a> Params<'a> { impl<'a> Params<'a> {
/// Construct a new parameters action. /// Construct a new parameters action.
pub fn new(cmd: &'a CmdParams<'a>) -> Self { pub fn new(cmd_matches: &'a ArgMatches<'a>) -> Self {
Self { Self {
cmd, cmd_matches,
} }
} }
/// Invoke the parameters action. /// Invoke the parameters action.
// TODO: create a trait for this method // TODO: create a trait for this method
pub fn invoke(&self) -> Result<(), ActionError> { pub fn invoke(&self) -> Result<(), ActionError> {
// Create the command matchers
let matcher_params = ParamsMatcher::with(self.cmd_matches).unwrap();
// Get the share URL // Get the share URL
let url = self.cmd.url(); let url = matcher_params.url();
// Create a reqwest client // Create a reqwest client
let client = Client::new(); let client = Client::new();
// Parse the remote file based on the share URL // Parse the remote file based on the share URL
// TODO: handle error here // 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 // TODO: show an informative error if the owner token isn't set
// Build the parameters data object // Build the parameters data object
let data = ParamsDataBuilder::default() let data = ParamsDataBuilder::default()
.download_limit(self.cmd.download_limit()) .download_limit(matcher_params.download_limit())
.build() .build()
.unwrap(); .unwrap();

View file

@ -1,41 +1,48 @@
use clap::ArgMatches;
use ffsend_api::action::password::Password as ApiPassword; use ffsend_api::action::password::Password as ApiPassword;
use ffsend_api::file::remote_file::RemoteFile; use ffsend_api::file::remote_file::RemoteFile;
use ffsend_api::reqwest::Client; use ffsend_api::reqwest::Client;
use cmd::cmd_password::CmdPassword; use cmd::matcher::{
Matcher,
password::PasswordMatcher,
};
use error::ActionError; use error::ActionError;
use util::print_success; use util::print_success;
/// A file password action. /// A file password action.
pub struct Password<'a> { pub struct Password<'a> {
cmd: &'a CmdPassword<'a>, cmd_matches: &'a ArgMatches<'a>,
} }
impl<'a> Password<'a> { impl<'a> Password<'a> {
/// Construct a new password action. /// Construct a new password action.
pub fn new(cmd: &'a CmdPassword<'a>) -> Self { pub fn new(cmd_matches: &'a ArgMatches<'a>) -> Self {
Self { Self {
cmd, cmd_matches,
} }
} }
/// Invoke the password action. /// Invoke the password action.
// TODO: create a trait for this method // TODO: create a trait for this method
pub fn invoke(&self) -> Result<(), ActionError> { pub fn invoke(&self) -> Result<(), ActionError> {
// Create the command matchers
let matcher_password = PasswordMatcher::with(self.cmd_matches).unwrap();
// Get the share URL // Get the share URL
let url = self.cmd.url(); let url = matcher_password.url();
// Create a reqwest client // Create a reqwest client
let client = Client::new(); let client = Client::new();
// Parse the remote file based on the share URL // Parse the remote file based on the share URL
// TODO: handle error here // 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 // TODO: show an informative error if the owner token isn't set
// Execute an password action // 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 a success message
print_success("Password set"); print_success("Password set");

View file

@ -1,12 +1,16 @@
use std::path::Path; use std::path::Path;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use clap::ArgMatches;
use failure::{err_msg, Fail}; use failure::{err_msg, Fail};
use ffsend_api::action::params::ParamsDataBuilder; use ffsend_api::action::params::ParamsDataBuilder;
use ffsend_api::action::upload::Upload as ApiUpload; use ffsend_api::action::upload::Upload as ApiUpload;
use ffsend_api::reqwest::Client; use ffsend_api::reqwest::Client;
use cmd::cmd_upload::CmdUpload; use cmd::matcher::{
Matcher,
upload::UploadMatcher,
};
use error::ActionError; use error::ActionError;
use progress::ProgressBar; use progress::ProgressBar;
use util::open_url; use util::open_url;
@ -15,23 +19,26 @@ use util::{print_error, set_clipboard};
/// A file upload action. /// A file upload action.
pub struct Upload<'a> { pub struct Upload<'a> {
cmd: &'a CmdUpload<'a>, cmd_matches: &'a ArgMatches<'a>,
} }
impl<'a> Upload<'a> { impl<'a> Upload<'a> {
/// Construct a new upload action. /// Construct a new upload action.
pub fn new(cmd: &'a CmdUpload<'a>) -> Self { pub fn new(cmd_matches: &'a ArgMatches<'a>) -> Self {
Self { Self {
cmd, cmd_matches,
} }
} }
/// Invoke the upload action. /// Invoke the upload action.
// TODO: create a trait for this method // TODO: create a trait for this method
pub fn invoke(&self) -> Result<(), ActionError> { pub fn invoke(&self) -> Result<(), ActionError> {
// Create the command matchers
let matcher_upload = UploadMatcher::with(self.cmd_matches).unwrap();
// Get API parameters // Get API parameters
let path = Path::new(self.cmd.file()).to_path_buf(); let path = Path::new(matcher_upload.file()).to_path_buf();
let host = self.cmd.host(); let host = matcher_upload.host();
// Create a reqwest client // Create a reqwest client
let client = Client::new(); let client = Client::new();
@ -43,7 +50,7 @@ impl<'a> Upload<'a> {
let params = { let params = {
// Build the parameters data object // Build the parameters data object
let mut params = ParamsDataBuilder::default() let mut params = ParamsDataBuilder::default()
.download_limit(self.cmd.download_limit()) .download_limit(matcher_upload.download_limit())
.build() .build()
.unwrap(); .unwrap();
@ -59,8 +66,8 @@ impl<'a> Upload<'a> {
let file = ApiUpload::new( let file = ApiUpload::new(
host, host,
path, path,
self.cmd.name().map(|name| name.to_owned()), matcher_upload.name().map(|name| name.to_owned()),
self.cmd.password(), matcher_upload.password(),
params, params,
).invoke(&client, bar)?; ).invoke(&client, bar)?;
@ -70,7 +77,7 @@ impl<'a> Upload<'a> {
println!("Owner token: {}", file.owner_token().unwrap()); println!("Owner token: {}", file.owner_token().unwrap());
// Open the URL in the browser // Open the URL in the browser
if self.cmd.open() { if matcher_upload.open() {
if let Err(err) = open_url(url.clone()) { if let Err(err) = open_url(url.clone()) {
print_error( print_error(
err.context("Failed to open the URL in the browser") 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 // Copy the URL in the user's clipboard
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
{ {
if self.cmd.copy() { if matcher_upload.copy() {
if set_clipboard(url.as_str().to_owned()).is_err() { if set_clipboard(url.as_str().to_owned()).is_err() {
print_error( print_error(
err_msg("Failed to copy the URL to the clipboard") err_msg("Failed to copy the URL to the clipboard")

View file

@ -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<u8>;
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::<u8>().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)
})
}
}

53
cli/src/cmd/arg/host.rs Normal file
View file

@ -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"),
}
}
}

49
cli/src/cmd/arg/mod.rs Normal file
View file

@ -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()) }
}

31
cli/src/cmd/arg/owner.rs Normal file
View file

@ -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)
}
}

View file

@ -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<String>;
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")
)
}
}

49
cli/src/cmd/arg/url.rs Normal file
View file

@ -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"),
}
}
}

View file

@ -1,6 +1,6 @@
use ffsend_api::url::{ParseError, Url}; 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 rpassword::prompt_password_stderr;
use util::quit_error_msg; use util::quit_error_msg;

19
cli/src/cmd/cmd/delete.rs Normal file
View file

@ -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())
}
}

View file

@ -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"))
}
}

18
cli/src/cmd/cmd/info.rs Normal file
View file

@ -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())
}
}

14
cli/src/cmd/cmd/mod.rs Normal file
View file

@ -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;

25
cli/src/cmd/cmd/params.rs Normal file
View file

@ -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(&param_args))
}
}

View file

@ -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())
}
}

47
cli/src/cmd/cmd/upload.rs Normal file
View file

@ -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
}
}

View file

@ -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<CmdDelete<'a>> {
parent.subcommand_matches("delete")
.map(|matches| CmdDelete { matches })
}
/// Get the owner token.
pub fn owner(&'a self) -> Option<String> {
// 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"),
}
}
}

View file

@ -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<CmdDownload<'a>> {
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<String> {
// 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")
)
}
}

View file

@ -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<CmdInfo<'a>> {
parent.subcommand_matches("info")
.map(|matches| CmdInfo { matches })
}
/// Get the owner token.
pub fn owner(&'a self) -> Option<String> {
// 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<String> {
// 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")
)
}
}

View file

@ -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(&param_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<CmdParams<'a>> {
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<String> {
// 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<u8> {
// TODO: do not unwrap, report an error
self.matches.value_of("download-limit")
.map(|d| d.parse::<u8>().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)
})
}
}

View file

@ -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<CmdUpload<'a>> {
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<String> {
// 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<u8> {
// 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::<u8>().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)
})
}
}

View file

@ -1,13 +1,24 @@
use super::clap::{App, AppSettings, Arg, ArgMatches}; use clap::{App, AppSettings, Arg, ArgMatches};
use app::*; use app::*;
use super::cmd_delete::CmdDelete; use super::matcher::{
use super::cmd_download::CmdDownload; DeleteMatcher,
use super::cmd_info::CmdInfo; DownloadMatcher,
use super::cmd_params::CmdParams; InfoMatcher,
use super::cmd_password::CmdPassword; Matcher,
use super::cmd_upload::CmdUpload; ParamsMatcher,
PasswordMatcher,
UploadMatcher,
};
use super::cmd::{
CmdDelete,
CmdDownload,
CmdInfo,
CmdParams,
CmdPassword,
CmdUpload,
};
/// CLI argument handler. /// CLI argument handler.
pub struct Handler<'a> { pub struct Handler<'a> {
@ -22,8 +33,7 @@ impl<'a: 'b, 'b> Handler<'a> {
.version(APP_VERSION) .version(APP_VERSION)
.author(APP_AUTHOR) .author(APP_AUTHOR)
.about(APP_ABOUT) .about(APP_ABOUT)
.global_setting(AppSettings::GlobalVersion) .global_setting(AppSettings::GlobalVersion) .global_setting(AppSettings::VersionlessSubcommands)
.global_setting(AppSettings::VersionlessSubcommands)
// TODO: enable below command when it doesn't break `p` anymore. // TODO: enable below command when it doesn't break `p` anymore.
// .global_setting(AppSettings::InferSubcommands) // .global_setting(AppSettings::InferSubcommands)
.arg(Arg::with_name("force") .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. /// Get the delete sub command, if matched.
pub fn delete(&'a self) -> Option<CmdDelete<'a>> { pub fn delete(&'a self) -> Option<DeleteMatcher> {
CmdDelete::parse(&self.matches) DeleteMatcher::with(&self.matches)
} }
/// Get the download sub command, if matched. /// Get the download sub command, if matched.
pub fn download(&'a self) -> Option<CmdDownload<'a>> { pub fn download(&'a self) -> Option<DownloadMatcher> {
CmdDownload::parse(&self.matches) DownloadMatcher::with(&self.matches)
} }
/// Get the info sub command, if matched. /// Get the info matcher, if that subcommand is entered.
pub fn info(&'a self) -> Option<CmdInfo<'a>> { pub fn info(&'a self) -> Option<InfoMatcher> {
CmdInfo::parse(&self.matches) InfoMatcher::with(&self.matches)
} }
/// Get the parameters sub command, if matched. /// Get the parameters sub command, if matched.
pub fn params(&'a self) -> Option<CmdParams<'a>> { pub fn params(&'a self) -> Option<ParamsMatcher> {
CmdParams::parse(&self.matches) ParamsMatcher::with(&self.matches)
} }
/// Get the password sub command, if matched. /// Get the password sub command, if matched.
pub fn password(&'a self) -> Option<CmdPassword<'a>> { pub fn password(&'a self) -> Option<PasswordMatcher> {
CmdPassword::parse(&self.matches) PasswordMatcher::with(&self.matches)
} }
/// Get the upload sub command, if matched. /// Get the upload sub command, if matched.
pub fn upload(&'a self) -> Option<CmdUpload<'a>> { pub fn upload(&'a self) -> Option<UploadMatcher> {
CmdUpload::parse(&self.matches) UploadMatcher::with(&self.matches)
} }
} }

View file

@ -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<String> {
// 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<Self> {
matches.subcommand_matches("delete")
.map(|matches|
DeleteMatcher {
matches,
}
)
}
}

View file

@ -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<String> {
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<Self> {
matches.subcommand_matches("download")
.map(|matches|
DownloadMatcher {
matches,
}
)
}
}

View file

@ -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<String> {
// 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<String> {
ArgPassword::value(self.matches)
}
}
impl<'a> Matcher<'a> for InfoMatcher<'a> {
fn with(matches: &'a ArgMatches) -> Option<Self> {
matches.subcommand_matches("info")
.map(|matches|
InfoMatcher {
matches,
}
)
}
}

View file

@ -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<Self>;
}

View file

@ -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<String> {
// 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<u8> {
ArgDownloadLimit::value(self.matches)
}
}
impl<'a> Matcher<'a> for ParamsMatcher<'a> {
fn with(matches: &'a ArgMatches) -> Option<Self> {
matches.subcommand_matches("parameters")
.map(|matches|
ParamsMatcher {
matches,
}
)
}
}

View file

@ -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<String> {
// 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<Self> {
matches.subcommand_matches("password")
.map(|matches|
PasswordMatcher {
matches,
}
)
}
}

View file

@ -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<String> {
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<u8> {
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<Self> {
matches.subcommand_matches("upload")
.map(|matches|
UploadMatcher {
matches,
}
)
}
}

View file

@ -1,12 +1,7 @@
extern crate clap; pub mod arg;
pub mod cmd;
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 handler; pub mod handler;
pub mod matcher;
// Reexport modules // Reexport modules
pub use self::handler::Handler; pub use self::handler::Handler;

View file

@ -1,3 +1,4 @@
extern crate clap;
extern crate failure; extern crate failure;
#[macro_use] #[macro_use]
extern crate failure_derive; extern crate failure_derive;
@ -38,38 +39,38 @@ fn main() {
/// message. /// message.
fn invoke_action(handler: &Handler) -> Result<(), Error> { fn invoke_action(handler: &Handler) -> Result<(), Error> {
// Match the delete command // Match the delete command
if let Some(cmd) = handler.delete() { if handler.delete().is_some() {
return Delete::new(&cmd).invoke() return Delete::new(handler.matches()).invoke()
.map_err(|err| err.into()); .map_err(|err| err.into());
} }
// Match the download command // Match the download command
if let Some(cmd) = handler.download() { if handler.download().is_some() {
return Download::new(&cmd).invoke() return Download::new(handler.matches()).invoke()
.map_err(|err| err.into()); .map_err(|err| err.into());
} }
// Match the info command // Match the info command
if let Some(cmd) = handler.info() { if handler.info().is_some() {
return Info::new(&cmd).invoke() return Info::new(handler.matches()).invoke()
.map_err(|err| err.into()); .map_err(|err| err.into());
} }
// Match the parameters command // Match the parameters command
if let Some(cmd) = handler.params() { if handler.params().is_some() {
return Params::new(&cmd).invoke() return Params::new(handler.matches()).invoke()
.map_err(|err| err.into()); .map_err(|err| err.into());
} }
// Match the password command // Match the password command
if let Some(cmd) = handler.password() { if handler.password().is_some() {
return Password::new(&cmd).invoke() return Password::new(handler.matches()).invoke()
.map_err(|err| err.into()); .map_err(|err| err.into());
} }
// Match the upload command // Match the upload command
if let Some(cmd) = handler.upload() { if handler.upload().is_some() {
return Upload::new(&cmd).invoke() return Upload::new(handler.matches()).invoke()
.map_err(|err| err.into()); .map_err(|err| err.into());
} }