Skip to content

Commit

Permalink
add find command (#1136)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
aawsome committed Apr 30, 2024
1 parent a6bd54c commit 6bf5069
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 14 deletions.
27 changes: 14 additions & 13 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 6 additions & 0 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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),

Expand Down
151 changes: 151 additions & 0 deletions src/commands/find.rs
Original file line number Diff line number Diff line change
@@ -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),
};
}
}
}
}
2 changes: 1 addition & 1 deletion src/commands/ls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down

0 comments on commit 6bf5069

Please sign in to comment.