rustic_rs/commands/
find.rs1use std::path::{Path, PathBuf};
4
5use crate::{
6 Application, RUSTIC_APP,
7 repository::{CliIndexedRepo, 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")]
41 ids: Vec<String>,
42
43 #[clap(long)]
45 all: bool,
46
47 #[clap(long)]
49 show_misses: bool,
50
51 #[clap(long, long("numeric-uid-gid"))]
53 numeric_id: bool,
54}
55
56impl Runnable for FindCmd {
57 fn run(&self) {
58 if let Err(err) = RUSTIC_APP
59 .config()
60 .repository
61 .run_indexed(|repo| self.inner_run(repo))
62 {
63 status_err!("{}", err);
64 RUSTIC_APP.shutdown(Shutdown::Crash);
65 };
66 }
67}
68
69impl FindCmd {
70 fn inner_run(&self, repo: CliIndexedRepo) -> Result<()> {
71 let groups = get_global_grouped_snapshots(&repo, &self.ids)?;
72 for (group, mut snapshots) in groups {
73 snapshots.sort_unstable();
74 if !group.is_empty() {
75 println!("\nsearching in snapshots group {group}...");
76 }
77 let ids = snapshots.iter().map(|sn| sn.tree);
78 if let Some(path) = &self.path {
79 let FindNode { nodes, matches } = repo.find_nodes_from_path(ids, path)?;
80 for (idx, g) in &matches
81 .iter()
82 .zip(snapshots.iter())
83 .chunk_by(|(idx, _)| *idx)
84 {
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
108 .iter()
109 .zip(snapshots.iter())
110 .chunk_by(|(idx, _)| *idx)
111 {
112 self.print_identical_snapshots(idx.iter(), g.into_iter().map(|(_, sn)| sn));
113 for (path_idx, node_idx) in idx {
114 print_node(&nodes[*node_idx], &paths[*path_idx], self.numeric_id);
115 }
116 }
117 }
118 }
119 Ok(())
120 }
121
122 fn print_identical_snapshots<'a>(
123 &self,
124 mut idx: impl Iterator,
125 mut g: impl Iterator<Item = &'a SnapshotFile>,
126 ) {
127 let empty_result = idx.next().is_none();
128 let not = if empty_result { "not " } else { "" };
129 if self.show_misses || !empty_result {
130 if self.all {
131 for sn in g {
132 let time = sn.time.format("%Y-%m-%d %H:%M:%S");
133 println!("{not}found in {} from {time}", sn.id);
134 }
135 } else {
136 let sn = g.next().unwrap();
137 let count = g.count();
138 let time = sn.time.format("%Y-%m-%d %H:%M:%S");
139 match count {
140 0 => println!("{not}found in {} from {time}", sn.id),
141 count => println!("{not}found in {} from {time} (+{count})", sn.id),
142 };
143 }
144 }
145 }
146}