diff --git a/Cargo.lock b/Cargo.lock index b46a1ddb6..8082ff127 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1304,9 +1304,9 @@ checksum = "cdeb3aa5e95cf9aabc17f060cfa0ced7b83f042390760ca53bf09df9968acaa1" [[package]] name = "flate2" -version = "1.0.29" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4556222738635b7a3417ae6130d8f52201e45a0c4d1a907f0826383adb5f85e7" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", @@ -1945,9 +1945,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.153" +version = "0.2.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" [[package]] name = "libm" @@ -3140,6 +3140,7 @@ dependencies = [ "directories", "displaydoc", "gethostname", + "globset", "human-panic", "humantime", "indicatif", @@ -3177,7 +3178,7 @@ dependencies = [ [[package]] name = "rustic_backend" version = "0.1.1" -source = "git+https://github.com/rustic-rs/rustic_core.git#649567404e16a84a8ebbbbbaf969e32b51608137" +source = "git+https://github.com/rustic-rs/rustic_core.git#f3ad6e95ac8761b9d037fcbfe4e7a483cde4b34a" dependencies = [ "aho-corasick", "anyhow", @@ -3210,7 +3211,7 @@ dependencies = [ [[package]] name = "rustic_core" version = "0.2.0" -source = "git+https://github.com/rustic-rs/rustic_core.git#649567404e16a84a8ebbbbbaf969e32b51608137" +source = "git+https://github.com/rustic-rs/rustic_core.git#f3ad6e95ac8761b9d037fcbfe4e7a483cde4b34a" dependencies = [ "aes256ctr_poly1305aes", "anyhow", @@ -3266,7 +3267,7 @@ dependencies = [ [[package]] name = "rustic_testing" version = "0.1.0" -source = "git+https://github.com/rustic-rs/rustic_core.git#649567404e16a84a8ebbbbbaf969e32b51608137" +source = "git+https://github.com/rustic-rs/rustic_core.git#f3ad6e95ac8761b9d037fcbfe4e7a483cde4b34a" dependencies = [ "aho-corasick", "anyhow", @@ -3546,9 +3547,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.8.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c85f8e96d1d6857f13768fcbd895fcb06225510022a2774ed8b5150581847b0" +checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" dependencies = [ "base64 0.22.0", "chrono", @@ -3564,9 +3565,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.8.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8b3a576c4eb2924262d5951a3b737ccaf16c931e39a2810c36f9a7e25575557" +checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" dependencies = [ "darling 0.20.8", "proc-macro2", @@ -3722,9 +3723,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.0", diff --git a/Cargo.toml b/Cargo.toml index 194c4ef81..14c0720e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,6 +88,7 @@ convert_case = "0.6.0" dialoguer = "0.11.0" directories = "5" gethostname = "0.4" +globset = "0.4.14" human-panic = "1.2.3" humantime = "2" indicatif = "0.17" diff --git a/src/commands.rs b/src/commands.rs index 8ac6b0a7f..66bbd87a0 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -8,6 +8,7 @@ pub(crate) mod config; pub(crate) mod copy; pub(crate) mod diff; pub(crate) mod dump; +pub(crate) mod find; pub(crate) mod forget; pub(crate) mod init; pub(crate) mod key; @@ -62,6 +63,8 @@ use log::{log, warn, Level}; use rustic_core::{IndexedFull, OpenStatus, ProgressBars, Repository}; use simplelog::{CombinedLogger, LevelFilter, TermLogger, TerminalMode, WriteLogger}; +use self::find::FindCmd; + pub(super) mod constants { pub(super) const MAX_PASSWORD_RETRIES: usize = 5; } @@ -95,6 +98,9 @@ enum RusticCmd { /// dump the contents of a file in a snapshot to stdout Dump(DumpCmd), + /// Find in given snapshots + Find(FindCmd), + /// Remove snapshots from the repository Forget(ForgetCmd), diff --git a/src/commands/find.rs b/src/commands/find.rs new file mode 100644 index 000000000..f56cc19c1 --- /dev/null +++ b/src/commands/find.rs @@ -0,0 +1,151 @@ +//! `find` subcommand + +use std::path::{Path, PathBuf}; + +use crate::{commands::open_repository_indexed, status_err, Application, RUSTIC_APP}; + +use abscissa_core::{Command, Runnable, Shutdown}; +use anyhow::Result; +use globset::{Glob, GlobBuilder, GlobSetBuilder}; +use itertools::Itertools; + +use rustic_core::{ + repofile::{Node, SnapshotFile}, + FindMatches, FindNode, SnapshotGroupCriterion, +}; + +use super::ls::print_node; + +/// `find` subcommand +#[derive(clap::Parser, Command, Debug)] +pub(crate) struct FindCmd { + /// pattern to find (can be specified multiple times) + #[clap(long, value_name = "PATTERN", conflicts_with = "path")] + glob: Vec, + + /// pattern to find case-insensitive (can be specified multiple times) + #[clap(long, value_name = "PATTERN", conflicts_with = "path")] + iglob: Vec, + + /// exact path to find + #[clap(long, value_name = "PATH")] + path: Option, + + /// Snapshots to serach in. If none is given, use filter options to filter from all snapshots + #[clap(value_name = "ID")] + ids: Vec, + + /// Group snapshots by any combination of host,label,paths,tags + #[clap( + long, + short = 'g', + value_name = "CRITERION", + default_value = "host,label,paths" + )] + group_by: SnapshotGroupCriterion, + + /// Show all snapshots instead of summarizing snapshots with identical search results + #[clap(long)] + all: bool, + + /// Also show snapshots which don't contain a search result. + #[clap(long)] + show_misses: bool, + + /// Show uid/gid instead of user/group + #[clap(long, long("numeric-uid-gid"))] + numeric_id: bool, +} + +impl Runnable for FindCmd { + fn run(&self) { + if let Err(err) = self.inner_run() { + status_err!("{}", err); + RUSTIC_APP.shutdown(Shutdown::Crash); + }; + } +} + +impl FindCmd { + fn inner_run(&self) -> Result<()> { + let config = RUSTIC_APP.config(); + let repo = open_repository_indexed(&config.repository)?; + + let groups = repo.get_snapshot_group(&self.ids, self.group_by, |sn| { + config.snapshot_filter.matches(sn) + })?; + for (group, mut snapshots) in groups { + snapshots.sort_unstable(); + if !group.is_empty() { + println!("\nsearching in snapshots group {group}..."); + } + let ids = snapshots.iter().map(|sn| sn.tree); + if let Some(path) = &self.path { + let FindNode { nodes, matches } = repo.find_nodes_from_path(ids, path)?; + for (idx, g) in &matches + .iter() + .zip(snapshots.iter()) + .group_by(|(idx, _)| *idx) + { + self.print_identical_snapshots(idx.iter(), g.into_iter().map(|(_, sn)| sn)); + if let Some(idx) = idx { + print_node(&nodes[*idx], path, self.numeric_id); + } + } + } else { + let mut builder = GlobSetBuilder::new(); + for glob in &self.glob { + _ = builder.add(Glob::new(glob)?); + } + for glob in &self.iglob { + _ = builder.add(GlobBuilder::new(glob).case_insensitive(true).build()?); + } + let globset = builder.build()?; + let matches = |path: &Path, _: &Node| { + globset.is_match(path) || path.file_name().is_some_and(|f| globset.is_match(f)) + }; + let FindMatches { + paths, + nodes, + matches, + } = repo.find_matching_nodes(ids, &matches)?; + for (idx, g) in &matches + .iter() + .zip(snapshots.iter()) + .group_by(|(idx, _)| *idx) + { + self.print_identical_snapshots(idx.iter(), g.into_iter().map(|(_, sn)| sn)); + for (path_idx, node_idx) in idx { + print_node(&nodes[*node_idx], &paths[*path_idx], self.numeric_id); + } + } + } + } + Ok(()) + } + + fn print_identical_snapshots<'a>( + &self, + mut idx: impl Iterator, + mut g: impl Iterator, + ) { + let empty_result = idx.next().is_none(); + let not = if empty_result { "not " } else { "" }; + if self.show_misses || !empty_result { + if self.all { + for sn in g { + let time = sn.time.format("%Y-%m-%d %H:%M:%S"); + println!("{not}found in {} from {time}", sn.id); + } + } else { + let sn = g.next().unwrap(); + let count = g.count(); + let time = sn.time.format("%Y-%m-%d %H:%M:%S"); + match count { + 0 => println!("{not}found in {} from {time}", sn.id), + count => println!("{not}found in {} from {time} (+{count})", sn.id), + }; + } + } + } +} diff --git a/src/commands/ls.rs b/src/commands/ls.rs index eff0c8104..ae79fdee1 100644 --- a/src/commands/ls.rs +++ b/src/commands/ls.rs @@ -162,7 +162,7 @@ impl LsCmd { /// /// * `node` - the node to print /// * `path` - the path of the node -fn print_node(node: &Node, path: &Path, numeric_uid_gid: bool) { +pub fn print_node(node: &Node, path: &Path, numeric_uid_gid: bool) { println!( "{:>10} {:>8} {:>8} {:>9} {:>12} {path:?} {}", node.mode_str(),