Merge branch '85-make-upload-expiry-time-configurable' into 'master'

Resolve "Make upload expiry time configurable"

Closes #85

See merge request timvisee/ffsend!30
This commit is contained in:
Tim Visée 2019-10-25 13:03:48 +00:00
commit 4a89cc3f82
10 changed files with 438 additions and 162 deletions

382
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -109,7 +109,7 @@ colored = "1.8"
derive_builder = "0.7"
directories = "2.0"
failure = "0.1"
ffsend-api = { version = "0.3.3", default-features = false }
ffsend-api = { version = "0.4", default-features = false }
fs2 = "0.4"
lazy_static = "1.0"
open = "1"
@ -118,6 +118,7 @@ pathdiff = "0.1"
pbr = "1"
prettytable-rs = "0.8"
qr2term = { version = "0.1", optional = true }
regex = "1.3.1"
rpassword = "4.0"
serde = "1.0"
serde_derive = "1.0"

View file

@ -74,12 +74,13 @@ $ ffsend upload my-file.txt
https://send.firefox.com/#sample-share-url
# Advanced upload
# - Specify a download limit of 20
# - Specify a download limit of 1
# - Specify upload expiry time of 5 minutes
# - Enter a password to encrypt the file
# - Archive the file before uploading
# - Copy the shareable link to your clipboard
# - Open the shareable link in your browser
$ ffsend upload --downloads 20 --password --archive --copy --open my-file.txt
$ ffsend upload --downloads 1 --expiry 5m --password --archive --copy --open my-file.txt
Password: ******
https://send.firefox.com/#sample-share-url

View file

@ -322,6 +322,7 @@ impl<'a> Upload<'a> {
.download_limit(&matcher_main, api_version, auth)
.map(|d| d as u8),
)
.expiry_time(matcher_upload.expiry_time(&matcher_main, api_version, auth))
.build()
.unwrap();

View file

@ -31,7 +31,7 @@ impl ArgDownloadLimit {
.map(|value| format!("{}", value))
.collect::<Vec<_>>()
.join(", ");
eprintln!("The downloads limit must be one of: {}", allowed_str,);
eprintln!("The downloads limit must be one of: {}", allowed_str);
if auth {
eprintln!("Use '{}' to force", highlight("--force"));
} else {

115
src/cmd/arg/expiry_time.rs Normal file
View file

@ -0,0 +1,115 @@
use chrono::Duration;
use clap::{Arg, ArgMatches};
use failure::Fail;
use ffsend_api::api::Version as ApiVersion;
use ffsend_api::config::expiry_max;
use super::{CmdArg, CmdArgFlag, CmdArgOption};
use crate::cmd::matcher::MainMatcher;
use crate::util::{
format_duration, highlight, parse_duration, prompt_yes, quit, quit_error, ErrorHints,
};
/// The download limit argument.
pub struct ArgExpiryTime {}
impl ArgExpiryTime {
pub fn value_checked<'a>(
matches: &ArgMatches<'a>,
main_matcher: &MainMatcher,
api_version: ApiVersion,
auth: bool,
) -> Option<usize> {
// Get the expiry time value
let mut expiry = Self::value(matches)?;
// Get expiry time, return if allowed or when forcing
let allowed = expiry_max(api_version, auth);
if allowed.contains(&expiry) || main_matcher.force() {
return Some(expiry);
}
// Define function to format seconds
let format_secs = |secs: usize| format_duration(Duration::seconds(secs as i64));
// Prompt the user the specified expiry time is invalid
let allowed_str = allowed
.iter()
.map(|secs| format_secs(*secs))
.collect::<Vec<_>>()
.join(", ");
eprintln!("The expiry time must be one of: {}", allowed_str);
if auth {
eprintln!("Use '{}' to force", highlight("--force"));
} else {
eprintln!(
"Use '{}' to force, authenticate for higher limits",
highlight("--force")
);
}
// Ask to use closest limit, quit if user cancelled
let closest = closest(allowed, expiry);
if !prompt_yes(
&format!(
"Would you like to set expiry time to {} instead?",
format_secs(closest)
),
None,
main_matcher,
) {
quit();
}
expiry = closest;
Some(expiry)
}
}
impl CmdArg for ArgExpiryTime {
fn name() -> &'static str {
"expiry-time"
}
fn build<'b, 'c>() -> Arg<'b, 'c> {
Arg::with_name("expiry-time")
.long("expiry-time")
.short("e")
.alias("expire")
.alias("expiry")
.value_name("TIME")
.help("The file expiry time")
}
}
impl CmdArgFlag for ArgExpiryTime {}
impl<'a> CmdArgOption<'a> for ArgExpiryTime {
type Value = Option<usize>;
fn value<'b: 'a>(matches: &'a ArgMatches<'b>) -> Self::Value {
Self::value_raw(matches).map(|t| match parse_duration(t) {
Ok(seconds) => seconds,
Err(err) => quit_error(
err.context("specified invalid file expiry time"),
ErrorHints::default(),
),
})
}
}
/// Find the closest value to `current` in the given `values` range.
fn closest(values: &[usize], current: usize) -> usize {
// Own the values, sort and reverse, start with biggest first
let mut values = values.to_vec();
values.sort_unstable();
// Find the closest value, return it
*values
.iter()
.rev()
.map(|value| (value, (current as i64 - *value as i64).abs()))
.min_by_key(|value| value.1)
.expect("failed to find closest value, none given")
.0
}

View file

@ -1,6 +1,7 @@
pub mod api;
pub mod basic_auth;
pub mod download_limit;
pub mod expiry_time;
pub mod gen_passphrase;
pub mod host;
pub mod owner;
@ -11,6 +12,7 @@ pub mod url;
pub use self::api::ArgApi;
pub use self::basic_auth::ArgBasicAuth;
pub use self::download_limit::ArgDownloadLimit;
pub use self::expiry_time::ArgExpiryTime;
pub use self::gen_passphrase::ArgGenPassphrase;
pub use self::host::ArgHost;
pub use self::owner::ArgOwner;

View file

@ -5,7 +5,10 @@ use ffsend_api::url::Url;
use super::Matcher;
use crate::cmd::{
arg::{ArgDownloadLimit, ArgGenPassphrase, ArgHost, ArgPassword, CmdArgFlag, CmdArgOption},
arg::{
ArgDownloadLimit, ArgExpiryTime, ArgGenPassphrase, ArgHost, ArgPassword, CmdArgFlag,
CmdArgOption,
},
matcher::MainMatcher,
};
use crate::util::{bin_name, env_var_present, quit_error_msg, ErrorHintsBuilder};
@ -93,6 +96,18 @@ impl<'a: 'b, 'b> UploadMatcher<'a> {
)
}
/// Get the expiry time in seconds.
///
/// If the expiry time was not set, `None` is returned.
pub fn expiry_time(
&'a self,
main_matcher: &MainMatcher,
api_version: ApiVersion,
auth: bool,
) -> Option<usize> {
ArgExpiryTime::value_checked(self.matches, main_matcher, api_version, auth)
}
/// Check whether to archive the file to upload.
#[cfg(feature = "archive")]
pub fn archive(&self) -> bool {

View file

@ -1,7 +1,8 @@
use clap::{App, Arg, SubCommand};
use ffsend_api::action::params::PARAMS_DEFAULT_DOWNLOAD_STR as DOWNLOAD_DEFAULT;
use crate::cmd::arg::{ArgDownloadLimit, ArgGenPassphrase, ArgHost, ArgPassword, CmdArg};
use crate::cmd::arg::{
ArgDownloadLimit, ArgExpiryTime, ArgGenPassphrase, ArgHost, ArgPassword, CmdArg,
};
/// The upload command definition.
pub struct CmdUpload;
@ -22,7 +23,8 @@ impl CmdUpload {
)
.arg(ArgPassword::build().help("Protect the file with a password"))
.arg(ArgGenPassphrase::build())
.arg(ArgDownloadLimit::build().default_value(DOWNLOAD_DEFAULT))
.arg(ArgDownloadLimit::build())
.arg(ArgExpiryTime::build())
.arg(ArgHost::build())
.arg(
Arg::with_name("name")

View file

@ -4,6 +4,7 @@ extern crate colored;
extern crate directories;
extern crate fs2;
extern crate open;
extern crate regex;
#[cfg(feature = "clipboard-bin")]
extern crate which;
@ -38,6 +39,7 @@ use ffsend_api::{
reqwest,
url::Url,
};
use regex::Regex;
use rpassword::prompt_password_stderr;
#[cfg(feature = "clipboard-bin")]
use which::which;
@ -824,6 +826,69 @@ pub fn format_bytes(bytes: u64) -> String {
}
}
/// Parse the given duration string from human readable format into seconds.
/// This method parses a string of time components to represent the given duration.
///
/// The following time units are used:
/// - `w`: weeks
/// - `d`: days
/// - `h`: hours
/// - `m`: minutes
/// - `s`: seconds
/// The following time strings can be parsed:
/// - `8w6d`
/// - `23h14m`
/// - `9m55s`
/// - `1s1s1s1s1s`
pub fn parse_duration(duration: &str) -> Result<usize, ParseDurationError> {
// Build a regex to grab time parts
let re = Regex::new(r"(?i)([0-9]+)(([a-z]|\s*$))")
.expect("failed to compile duration parsing regex");
// We must find any match
if re.find(duration).is_none() {
return Err(ParseDurationError::Empty);
}
// Parse each time part, sum it's seconds
let mut seconds = 0;
for capture in re.captures_iter(duration) {
// Parse time value and modifier
let number = capture[1]
.parse::<usize>()
.map_err(ParseDurationError::InvalidValue)?;
let modifier = capture[2].trim().to_lowercase();
// Multiply and sum seconds by modifier
seconds += match modifier.as_str() {
"" | "s" => number,
"m" => number * 60,
"h" => number * 60 * 60,
"d" => number * 60 * 60 * 24,
"w" => number * 60 * 60 * 24 * 7,
m => return Err(ParseDurationError::UnknownIdentifier(m.into())),
};
}
Ok(seconds)
}
/// Represents a duration parsing error.
#[derive(Debug, Fail)]
pub enum ParseDurationError {
/// The given duration string did not contain any duration part.
#[fail(display = "given string did not contain any duration part")]
Empty,
/// A numeric value was invalid.
#[fail(display = "duration part has invalid numeric value")]
InvalidValue(std::num::ParseIntError),
/// The given duration string contained an invalid duration modifier.
#[fail(display = "duration part has unknown time identifier '{}'", _0)]
UnknownIdentifier(String),
}
/// Format the given duration in a human readable format.
/// This method builds a string of time components to represent
/// the given duration.