mirror of
https://github.com/rustic-rs/rustic.git
synced 2025-10-26 11:18:51 +00:00
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:
parent
a6bd54c7cb
commit
6bf5069d0c
27
Cargo.lock
generated
27
Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
151
src/commands/find.rs
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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(),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user