Browse Source

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

Resolve "Make upload expiry time configurable"

Closes #85

See merge request timvisee/ffsend!30
Tim Visée 5 years ago
parent
commit
4a89cc3f82
10 changed files with 401 additions and 132 deletions
  1. 191 124
      Cargo.lock
  2. 2 1
      Cargo.toml
  3. 3 2
      README.md
  4. 1 0
      src/action/upload.rs
  5. 1 1
      src/cmd/arg/download_limit.rs
  6. 115 0
      src/cmd/arg/expiry_time.rs
  7. 2 0
      src/cmd/arg/mod.rs
  8. 16 1
      src/cmd/matcher/upload.rs
  9. 5 3
      src/cmd/subcmd/upload.rs
  10. 65 0
      src/util.rs

File diff suppressed because it is too large
+ 191 - 124
Cargo.lock


+ 2 - 1
Cargo.toml

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

+ 3 - 2
README.md

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

+ 1 - 0
src/action/upload.rs

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

+ 1 - 1
src/cmd/arg/download_limit.rs

@@ -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 - 0
src/cmd/arg/expiry_time.rs

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

+ 2 - 0
src/cmd/arg/mod.rs

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

+ 16 - 1
src/cmd/matcher/upload.rs

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

+ 5 - 3
src/cmd/subcmd/upload.rs

@@ -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 - 0
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.

Some files were not shown because too many files changed in this diff