diff --git a/Cargo.lock b/Cargo.lock index 0599a07..e307d19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -705,6 +705,7 @@ dependencies = [ "pbr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "prettytable-rs 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "qr2term 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "rpassword 4.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index 54cf019..fd62f2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/cmd/arg/expiry_time.rs b/src/cmd/arg/expiry_time.rs index b22e05a..f6bb538 100644 --- a/src/cmd/arg/expiry_time.rs +++ b/src/cmd/arg/expiry_time.rs @@ -1,10 +1,14 @@ +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::{highlight, prompt_yes, quit}; +use crate::util::{ + format_duration, highlight, parse_duration, prompt_yes, quit, quit_error, ErrorHints, +}; /// The download limit argument. pub struct ArgExpiryTime {} @@ -25,10 +29,13 @@ impl ArgExpiryTime { 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(|value| format!("{}", value)) + .map(|secs| format_secs(*secs)) .collect::>() .join(", "); eprintln!("The expiry time must be one of: {}", allowed_str); @@ -44,7 +51,10 @@ impl ArgExpiryTime { // 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?", closest), + &format!( + "Would you like to set expiry time to {} instead?", + format_secs(closest) + ), None, main_matcher, ) { @@ -78,8 +88,13 @@ impl<'a> CmdArgOption<'a> for ArgExpiryTime { type Value = Option; 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::().expect("invalid expiry time")) + 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(), + ), + }) } } diff --git a/src/util.rs b/src/util.rs index 67176be..aedc3e9 100644 --- a/src/util.rs +++ b/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 { + // 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::() + .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.