mirror of
https://github.com/timvisee/ffsend.git
synced 2025-10-03 01:29:16 +02:00
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:
commit
4a89cc3f82
10 changed files with 438 additions and 162 deletions
382
Cargo.lock
generated
382
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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
115
src/cmd/arg/expiry_time.rs
Normal 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
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
|
|
65
src/util.rs
65
src/util.rs
|
@ -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.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue