mirror of
https://github.com/timvisee/ffsend.git
synced 2025-10-03 17:49:15 +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"
|
derive_builder = "0.7"
|
||||||
directories = "2.0"
|
directories = "2.0"
|
||||||
failure = "0.1"
|
failure = "0.1"
|
||||||
ffsend-api = { version = "0.3.3", default-features = false }
|
ffsend-api = { version = "0.4", default-features = false }
|
||||||
fs2 = "0.4"
|
fs2 = "0.4"
|
||||||
lazy_static = "1.0"
|
lazy_static = "1.0"
|
||||||
open = "1"
|
open = "1"
|
||||||
|
@ -118,6 +118,7 @@ pathdiff = "0.1"
|
||||||
pbr = "1"
|
pbr = "1"
|
||||||
prettytable-rs = "0.8"
|
prettytable-rs = "0.8"
|
||||||
qr2term = { version = "0.1", optional = true }
|
qr2term = { version = "0.1", optional = true }
|
||||||
|
regex = "1.3.1"
|
||||||
rpassword = "4.0"
|
rpassword = "4.0"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
|
|
|
@ -74,12 +74,13 @@ $ ffsend upload my-file.txt
|
||||||
https://send.firefox.com/#sample-share-url
|
https://send.firefox.com/#sample-share-url
|
||||||
|
|
||||||
# Advanced upload
|
# 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
|
# - Enter a password to encrypt the file
|
||||||
# - Archive the file before uploading
|
# - Archive the file before uploading
|
||||||
# - Copy the shareable link to your clipboard
|
# - Copy the shareable link to your clipboard
|
||||||
# - Open the shareable link in your browser
|
# - 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: ******
|
Password: ******
|
||||||
https://send.firefox.com/#sample-share-url
|
https://send.firefox.com/#sample-share-url
|
||||||
|
|
||||||
|
|
|
@ -322,6 +322,7 @@ impl<'a> Upload<'a> {
|
||||||
.download_limit(&matcher_main, api_version, auth)
|
.download_limit(&matcher_main, api_version, auth)
|
||||||
.map(|d| d as u8),
|
.map(|d| d as u8),
|
||||||
)
|
)
|
||||||
|
.expiry_time(matcher_upload.expiry_time(&matcher_main, api_version, auth))
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ impl ArgDownloadLimit {
|
||||||
.map(|value| format!("{}", value))
|
.map(|value| format!("{}", value))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", ");
|
.join(", ");
|
||||||
eprintln!("The downloads limit must be one of: {}", allowed_str,);
|
eprintln!("The downloads limit must be one of: {}", allowed_str);
|
||||||
if auth {
|
if auth {
|
||||||
eprintln!("Use '{}' to force", highlight("--force"));
|
eprintln!("Use '{}' to force", highlight("--force"));
|
||||||
} else {
|
} 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 api;
|
||||||
pub mod basic_auth;
|
pub mod basic_auth;
|
||||||
pub mod download_limit;
|
pub mod download_limit;
|
||||||
|
pub mod expiry_time;
|
||||||
pub mod gen_passphrase;
|
pub mod gen_passphrase;
|
||||||
pub mod host;
|
pub mod host;
|
||||||
pub mod owner;
|
pub mod owner;
|
||||||
|
@ -11,6 +12,7 @@ pub mod url;
|
||||||
pub use self::api::ArgApi;
|
pub use self::api::ArgApi;
|
||||||
pub use self::basic_auth::ArgBasicAuth;
|
pub use self::basic_auth::ArgBasicAuth;
|
||||||
pub use self::download_limit::ArgDownloadLimit;
|
pub use self::download_limit::ArgDownloadLimit;
|
||||||
|
pub use self::expiry_time::ArgExpiryTime;
|
||||||
pub use self::gen_passphrase::ArgGenPassphrase;
|
pub use self::gen_passphrase::ArgGenPassphrase;
|
||||||
pub use self::host::ArgHost;
|
pub use self::host::ArgHost;
|
||||||
pub use self::owner::ArgOwner;
|
pub use self::owner::ArgOwner;
|
||||||
|
|
|
@ -5,7 +5,10 @@ use ffsend_api::url::Url;
|
||||||
|
|
||||||
use super::Matcher;
|
use super::Matcher;
|
||||||
use crate::cmd::{
|
use crate::cmd::{
|
||||||
arg::{ArgDownloadLimit, ArgGenPassphrase, ArgHost, ArgPassword, CmdArgFlag, CmdArgOption},
|
arg::{
|
||||||
|
ArgDownloadLimit, ArgExpiryTime, ArgGenPassphrase, ArgHost, ArgPassword, CmdArgFlag,
|
||||||
|
CmdArgOption,
|
||||||
|
},
|
||||||
matcher::MainMatcher,
|
matcher::MainMatcher,
|
||||||
};
|
};
|
||||||
use crate::util::{bin_name, env_var_present, quit_error_msg, ErrorHintsBuilder};
|
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.
|
/// Check whether to archive the file to upload.
|
||||||
#[cfg(feature = "archive")]
|
#[cfg(feature = "archive")]
|
||||||
pub fn archive(&self) -> bool {
|
pub fn archive(&self) -> bool {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
use clap::{App, Arg, SubCommand};
|
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.
|
/// The upload command definition.
|
||||||
pub struct CmdUpload;
|
pub struct CmdUpload;
|
||||||
|
@ -22,7 +23,8 @@ impl CmdUpload {
|
||||||
)
|
)
|
||||||
.arg(ArgPassword::build().help("Protect the file with a password"))
|
.arg(ArgPassword::build().help("Protect the file with a password"))
|
||||||
.arg(ArgGenPassphrase::build())
|
.arg(ArgGenPassphrase::build())
|
||||||
.arg(ArgDownloadLimit::build().default_value(DOWNLOAD_DEFAULT))
|
.arg(ArgDownloadLimit::build())
|
||||||
|
.arg(ArgExpiryTime::build())
|
||||||
.arg(ArgHost::build())
|
.arg(ArgHost::build())
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("name")
|
Arg::with_name("name")
|
||||||
|
|
65
src/util.rs
65
src/util.rs
|
@ -4,6 +4,7 @@ extern crate colored;
|
||||||
extern crate directories;
|
extern crate directories;
|
||||||
extern crate fs2;
|
extern crate fs2;
|
||||||
extern crate open;
|
extern crate open;
|
||||||
|
extern crate regex;
|
||||||
#[cfg(feature = "clipboard-bin")]
|
#[cfg(feature = "clipboard-bin")]
|
||||||
extern crate which;
|
extern crate which;
|
||||||
|
|
||||||
|
@ -38,6 +39,7 @@ use ffsend_api::{
|
||||||
reqwest,
|
reqwest,
|
||||||
url::Url,
|
url::Url,
|
||||||
};
|
};
|
||||||
|
use regex::Regex;
|
||||||
use rpassword::prompt_password_stderr;
|
use rpassword::prompt_password_stderr;
|
||||||
#[cfg(feature = "clipboard-bin")]
|
#[cfg(feature = "clipboard-bin")]
|
||||||
use which::which;
|
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.
|
/// Format the given duration in a human readable format.
|
||||||
/// This method builds a string of time components to represent
|
/// This method builds a string of time components to represent
|
||||||
/// the given duration.
|
/// the given duration.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue