rustic_rs/commands/
find.rs1use std::path::{Path, PathBuf};
4
5use crate::{
6 Application, RUSTIC_APP,
7 repository::{IndexedRepo, get_global_grouped_snapshots},
8 status_err,
9};
10
11use abscissa_core::{Command, Runnable, Shutdown};
12use anyhow::Result;
13use clap::ValueHint;
14use globset::{Glob, GlobBuilder, GlobSetBuilder};
15use itertools::Itertools;
16
17use rustic_core::{
18 FindMatches, FindNode,
19 repofile::{Node, SnapshotFile},
20};
21
22use super::ls::print_node;
23
24#[derive(clap::Parser, Command, Debug)]
26pub(crate) struct FindCmd {
27 #[clap(long, value_name = "PATTERN", conflicts_with = "path")]
29 glob: Vec<String>,
30
31 #[clap(long, value_name = "PATTERN", conflicts_with = "path")]
33 iglob: Vec<String>,
34
35 #[clap(long, value_name = "PATH", value_hint = ValueHint::AnyPath)]
37 path: Option<PathBuf>,
38
39 #[clap(value_name = "ID")]
43 ids: Vec<String>,
44
45 #[clap(long)]
47 all: bool,
48
49 #[clap(long)]
51 show_misses: bool,
52
53 #[clap(long, long("numeric-uid-gid"))]
55 numeric_id: bool,
56}
57
58impl Runnable for FindCmd {
59 fn run(&self) {
60 if let Err(err) = RUSTIC_APP
61 .config()
62 .repository
63 .run_indexed(|repo| self.inner_run(repo))
64 {
65 status_err!("{}", err);
66 RUSTIC_APP.shutdown(Shutdown::Crash);
67 };
68 }
69}
70
71impl FindCmd {
72 fn inner_run(&self, repo: IndexedRepo) -> Result<()> {
73 let grouped = get_global_grouped_snapshots(&repo, &self.ids)?;
74 for group in grouped.groups {
75 let mut snaps = group.items;
76 let key = group.group_key;
77 snaps.sort_unstable();
78 if !key.is_empty() {
79 println!("\nsearching in snapshots group {key}...");
80 }
81 let ids = snaps.iter().map(|sn| sn.tree);
82 if let Some(path) = &self.path {
83 let FindNode { nodes, matches } = repo.find_nodes_from_path(ids, path)?;
84 for (idx, g) in &matches.iter().zip(snaps.iter()).chunk_by(|(idx, _)| *idx) {
85 self.print_identical_snapshots(idx.iter(), g.into_iter().map(|(_, sn)| sn));
86 if let Some(idx) = idx {
87 print_node(&nodes[*idx], path, self.numeric_id);
88 }
89 }
90 } else {
91 let mut builder = GlobSetBuilder::new();
92 for glob in &self.glob {
93 _ = builder.add(Glob::new(glob)?);
94 }
95 for glob in &self.iglob {
96 _ = builder.add(GlobBuilder::new(glob).case_insensitive(true).build()?);
97 }
98 let globset = builder.build()?;
99 let matches = |path: &Path, _: &Node| {
100 globset.is_match(path) || path.file_name().is_some_and(|f| globset.is_match(f))
101 };
102 let FindMatches {
103 paths,
104 nodes,
105 matches,
106 } = repo.find_matching_nodes(ids, &matches)?;
107 for (idx, g) in &matches.iter().zip(snaps.iter()).chunk_by(|(idx, _)| *idx) {
108 self.print_identical_snapshots(idx.iter(), g.into_iter().map(|(_, sn)| sn));
109 for (path_idx, node_idx) in idx {
110 print_node(&nodes[*node_idx], &paths[*path_idx], self.numeric_id);
111 }
112 }
113 }
114 }
115 Ok(())
116 }
117
118 fn print_identical_snapshots<'a>(
119 &self,
120 mut idx: impl Iterator,
121 mut g: impl Iterator<Item = &'a SnapshotFile>,
122 ) {
123 let config = RUSTIC_APP.config();
124 let empty_result = idx.next().is_none();
125 let not = if empty_result { "not " } else { "" };
126 if self.show_misses || !empty_result {
127 if self.all {
128 for sn in g {
129 let time = config.global.format_time(&sn.time);
130 println!("{not}found in {} from {time}", sn.id);
131 }
132 } else {
133 let sn = g.next().unwrap();
134 let count = g.count();
135 let time = config.global.format_time(&sn.time);
136 match count {
137 0 => println!("{not}found in {} from {time}", sn.id),
138 count => println!("{not}found in {} from {time} (+{count})", sn.id),
139 };
140 }
141 }
142 }
143}