add find command (#1136)

Adds the new command `find`.
This commands allows to search for glob pattern using `--glob`/`--iglob`
or given paths using `--path` in a list of snapshots.
It displays all finds and is able accumulate snapshots with identical
search result. This allows to use this command as a history search:
`rustic find --path /my/path` shows (only) all changes of that path.
This commit is contained in:
aawsome 2024-04-30 11:54:43 +02:00 committed by GitHub
parent a6bd54c7cb
commit 6bf5069d0c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 173 additions and 14 deletions

27
Cargo.lock generated
View File

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

View File

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

View File

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

151
src/commands/find.rs Normal file
View File

@ -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<String>,
/// pattern to find case-insensitive (can be specified multiple times)
#[clap(long, value_name = "PATTERN", conflicts_with = "path")]
iglob: Vec<String>,
/// exact path to find
#[clap(long, value_name = "PATH")]
path: Option<PathBuf>,
/// Snapshots to serach in. If none is given, use filter options to filter from all snapshots
#[clap(value_name = "ID")]
ids: Vec<String>,
/// 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<Item = &'a SnapshotFile>,
) {
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),
};
}
}
}
}

View File

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