From 54c117d72f8476495d122a24750318d0574beabd Mon Sep 17 00:00:00 2001 From: Karim SENHAJI Date: Tue, 9 Oct 2018 13:47:42 +0200 Subject: [PATCH] Add support for --changed-before and --changed-with for modification time based search --- Cargo.lock | 35 ++++++++++++++++++++++++++ Cargo.toml | 3 +++ src/app.rs | 22 +++++++++++++++++ src/internal.rs | 51 +++++++++++++++++++++++++++++++++++++- src/main.rs | 22 ++++++++++++++++- src/walk.rs | 15 ++++++++++++ tests/tests.rs | 65 +++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 211 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5cbfbac..b70f6ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,6 +82,9 @@ dependencies = [ "clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)", "ctrlc 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "diff 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "filetime 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "humantime 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "if_chain 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "ignore 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", @@ -92,6 +95,16 @@ dependencies = [ "version_check 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "filetime" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "fnv" version = "1.0.6" @@ -123,6 +136,19 @@ dependencies = [ "regex 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "humantime" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "if_chain" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "ignore" version = "0.4.3" @@ -198,6 +224,11 @@ dependencies = [ "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "quick-error" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "rand" version = "0.4.3" @@ -387,10 +418,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum crossbeam 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "24ce9782d4d5c53674646a6a4c1863a21a8fc0cb649b3c94dfc16e45071dea19" "checksum ctrlc 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "630391922b1b893692c6334369ff528dcc3a9d8061ccf4c803aa8f83cb13db5e" "checksum diff 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "3c2b69f912779fbb121ceb775d74d51e915af17aaebc38d28a592843a2dd0a3a" +"checksum filetime 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "da4b9849e77b13195302c174324b5ba73eec9b236b24c221a61000daefb95c5f" "checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" "checksum globset 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8e49edbcc9c7fc5beb8c0a54e7319ff8bed353a2b55e85811c6281188c2a6c84" +"checksum humantime 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0484fda3e7007f2a4a0d9c3a703ca38c71c54c55602ce4660c419fd32e188c9e" +"checksum if_chain 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4bac95d9aa0624e7b78187d6fb8ab012b41d9f6f54b1bcb61e61c4845f8357ec" "checksum ignore 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3e9faa7c84064f07b40da27044af629f578bc7994b650d3e458d0c29183c1d91" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca488b89a5657b0a2ecd45b95609b3e848cf1755da332a0da46e2b2b1cb371a7" @@ -399,6 +433,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "796fba70e76612589ed2ce7f45282f5af869e0fdd7cc6199fa1aa1f1d591ba9d" "checksum nix 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d37e713a259ff641624b6cb20e3b12b2952313ba36b6823c0f16e6cfd9e5de17" "checksum num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c51a3322e4bca9d212ad9a158a02abc6934d005490c054a2778df73a70aa0a30" +"checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0" "checksum rand 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8356f47b32624fef5b3301c1be97e5944ecdd595409cc5da11d05f211db6cfbd" "checksum redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "c214e91d3ecf43e9a4e41e578973adeb14b474f2bee858742d127af75a0112b1" "checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" diff --git a/Cargo.toml b/Cargo.toml index b7bffb4..59f514b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,8 @@ num_cpus = "1.8" regex = "1.0.0" regex-syntax = "0.6" ctrlc = "3.1" +humantime = "1.1.1" +if_chain = "0.1.3" [dependencies.clap] version = "2.31.2" @@ -51,3 +53,4 @@ libc = "0.2" [dev-dependencies] diff = "0.1" tempdir = "0.3" +filetime = "0.2.1" diff --git a/src/app.rs b/src/app.rs index 55ca402..5e3cb06 100644 --- a/src/app.rs +++ b/src/app.rs @@ -169,6 +169,18 @@ pub fn build_app() -> App<'static, 'static> { .takes_value(true) .hidden(true), ) + .arg( + arg("changed-within") + .long("changed-within") + .takes_value(true) + .number_of_values(1), + ) + .arg( + arg("changed-before") + .long("changed-before") + .takes_value(true) + .number_of_values(1), + ) .arg(arg("pattern")) .arg(arg("path").multiple(true)) } @@ -292,5 +304,15 @@ fn usage() -> HashMap<&'static str, Help> { 'mi': mebibytes\n \ 'gi': gibibytes\n \ 'ti': tebibytes"); + doc!(h, "changed-before" + , "Limit results based on modification time older than duration or date provided." + , "Limit results based on modification time older than duration provided:\n \ + using a duration: d h m s (e.g. 10h, 1d, 35min...)\n \ + or a date and time: YYYY-MM-DD HH:MM:SS"); + doc!(h, "changed-within" + , "Limit results based on modification time within the duration provided or between date provided and now." + , "Limit results based on modification time within the duration provided:\n \ + using a duration: d h m s (e.g. 10h, 1d, 35min...)\n \ + or a date and time: YYYY-MM-DD HH:MM:SS"); h } diff --git a/src/internal.rs b/src/internal.rs index 197f4f6..7350efd 100644 --- a/src/internal.rs +++ b/src/internal.rs @@ -9,7 +9,7 @@ use std::ffi::OsString; use std::path::PathBuf; use std::process; -use std::time; +use std::time::{self, SystemTime}; use exec::CommandTemplate; use lscolors::LsColors; @@ -100,6 +100,38 @@ impl SizeFilter { } } +/// Filter based on time ranges +#[derive(Debug, PartialEq)] +pub enum TimeFilter { + Before(SystemTime), + After(SystemTime), +} + +impl TimeFilter { + fn from_str(s: &str) -> Option { + use humantime; + humantime::parse_duration(s) + .map(|duration| SystemTime::now() - duration) + .or_else(|_| humantime::parse_rfc3339_weak(s)) + .ok() + } + + pub fn before(s: &str) -> Option { + Some(TimeFilter::Before(TimeFilter::from_str(s)?)) + } + + pub fn after(s: &str) -> Option { + Some(TimeFilter::After(TimeFilter::from_str(s)?)) + } + + pub fn is_within(&self, t: &SystemTime) -> bool { + match self { + TimeFilter::Before(limit) => t <= limit, + TimeFilter::After(limit) => t >= limit, + } + } +} + /// Configuration options for *fd*. pub struct FdOptions { /// Whether the search is case-sensitive or case-insensitive. @@ -162,6 +194,9 @@ pub struct FdOptions { /// The given constraints on the size of returned files pub size_constraints: Vec, + + /// Constraints on last modification time of files + pub modification_constraints: Vec, } /// Print error message to stderr. @@ -468,4 +503,18 @@ mod tests { let f = SizeFilter::from_string("+1K").unwrap(); assert!(f.is_within(1000)); } + + #[test] + fn is_time_within() { + let now = SystemTime::now(); + assert!(TimeFilter::after("1min").unwrap().is_within(&now)); + assert!(!TimeFilter::before("1min").unwrap().is_within(&now)); + + let t1m_ago = SystemTime::now() - time::Duration::from_secs(60); + assert!(!TimeFilter::after("30sec").unwrap().is_within(&t1m_ago)); + assert!(TimeFilter::after("2min").unwrap().is_within(&t1m_ago)); + + assert!(TimeFilter::before("30sec").unwrap().is_within(&t1m_ago)); + assert!(!TimeFilter::before("2min").unwrap().is_within(&t1m_ago)); + } } diff --git a/src/main.rs b/src/main.rs index 64fe140..ef09d3a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,11 +13,14 @@ extern crate clap; extern crate ignore; #[macro_use] extern crate lazy_static; +extern crate humantime; #[cfg(all(unix, not(target_os = "redox")))] extern crate libc; extern crate num_cpus; extern crate regex; extern crate regex_syntax; +#[macro_use] +extern crate if_chain; mod app; mod exec; @@ -40,7 +43,7 @@ use regex::{RegexBuilder, RegexSetBuilder}; use exec::CommandTemplate; use internal::{ pattern_has_uppercase_char, print_error_and_exit, transform_args_with_exec, FdOptions, - FileTypes, SizeFilter, + FileTypes, SizeFilter, TimeFilter, }; use lscolors::LsColors; @@ -150,6 +153,22 @@ fn main() { }) .unwrap_or_else(|| vec![]); + let mut modification_constraints: Vec = Vec::new(); + if let Some(t) = matches.value_of("changed-within") { + if let Some(f) = TimeFilter::after(t) { + modification_constraints.push(f); + } else { + print_error_and_exit(&format!("Error: {} is not a valid time.", t)); + } + } + if let Some(t) = matches.value_of("changed-before") { + if let Some(f) = TimeFilter::before(t) { + modification_constraints.push(f); + } else { + print_error_and_exit(&format!("Error: {} is not a valid time.", t)); + } + } + let config = FdOptions { case_sensitive, search_full_path: matches.is_present("full-path"), @@ -226,6 +245,7 @@ fn main() { .map(|vs| vs.map(PathBuf::from).collect()) .unwrap_or_else(|| vec![]), size_constraints: size_limits, + modification_constraints, }; match RegexBuilder::new(&pattern_regex) diff --git a/src/walk.rs b/src/walk.rs index dfc68f4..815065e 100644 --- a/src/walk.rs +++ b/src/walk.rs @@ -289,6 +289,21 @@ pub fn scan(path_vec: &[PathBuf], pattern: Arc, config: Arc) { } } + // Filter out unwanted modification times + if !config.modification_constraints.is_empty() { + if_chain!{ + if entry_path.is_file(); + if let Ok(metadata) = entry_path.metadata(); + if let Ok(modified) = metadata.modified(); + if config.modification_constraints.iter().all(|tf| tf.is_within(&modified)); + then { + // When all is good, we just continue with pattern match + } else { + return ignore::WalkState::Continue; + } + } + } + let search_str_o = if config.search_full_path { match fshelper::path_absolute_form(entry_path) { Ok(path_abs_buf) => Some(path_abs_buf.to_string_lossy().into_owned().into()), diff --git a/tests/tests.rs b/tests/tests.rs index 137bd2b..e838bce 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -8,6 +8,8 @@ //! Integration tests for the CLI interface of fd. +extern crate filetime; +extern crate humantime; extern crate regex; mod testenv; @@ -16,6 +18,7 @@ use regex::escape; use std::fs; use std::io::Write; use std::path::Path; +use std::time::{Duration, SystemTime}; use testenv::TestEnv; static DEFAULT_DIRS: &'static [&'static str] = &["one/two/three", "one/two/three/directory_foo"]; @@ -1110,3 +1113,65 @@ fn test_size() { // Files with size equal 4 kibibytes. te.assert_output(&["", "--size", "+4ki", "--size", "-4ki"], "4_kibibytes.foo"); } + +#[cfg(test)] +fn create_file_with_modified>(path: P, duration_in_secs: u64) { + let st = SystemTime::now() - Duration::from_secs(duration_in_secs); + let ft = filetime::FileTime::from_system_time(st); + fs::File::create(&path).expect("creation failed"); + filetime::set_file_times(&path, ft, ft).expect("time modification failed"); +} + +#[test] +fn test_modified_relative() { + let te = TestEnv::new(&[], &[]); + create_file_with_modified(te.test_root().join("0_now"), 0); + create_file_with_modified(te.test_root().join("1_min"), 60); + create_file_with_modified(te.test_root().join("10_min"), 600); + create_file_with_modified(te.test_root().join("1_h"), 60 * 60); + create_file_with_modified(te.test_root().join("2_h"), 2 * 60 * 60); + create_file_with_modified(te.test_root().join("1_day"), 24 * 60 * 60); + + te.assert_output( + &["", "--changed-within", "15min"], + "0_now + 1_min + 10_min", + ); + + te.assert_output( + &["", "--changed-before", "15min"], + "1_h + 2_h + 1_day", + ); + + te.assert_output( + &["min", "--changed-within", "12h"], + "1_min + 10_min", + ); +} + +#[cfg(test)] +fn change_file_modified>(path: P, iso_date: &str) { + let st = humantime::parse_rfc3339(iso_date).expect("invalid date"); + let ft = filetime::FileTime::from_system_time(st); + filetime::set_file_times(path, ft, ft).expect("time modification failde"); +} + +#[test] +fn test_modified_asolute() { + let te = TestEnv::new(&[], &["15mar2018", "30dec2017"]); + change_file_modified(te.test_root().join("15mar2018"), "2018-03-15T12:00:00Z"); + change_file_modified(te.test_root().join("30dec2017"), "2017-12-30T23:59:00Z"); + + te.assert_output( + &["", "--changed-within", "2018-01-01 00:00:00"], + "15mar2018", + ); + te.assert_output( + &["", "--changed-before", "2018-01-01 00:00:00"], + "30dec2017", + ); +}