rustic_rs/commands/
find.rs

1//! `find` subcommand
2
3use 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/// `find` subcommand
25#[derive(clap::Parser, Command, Debug)]
26pub(crate) struct FindCmd {
27    /// pattern to find (can be specified multiple times)
28    #[clap(long, value_name = "PATTERN", conflicts_with = "path")]
29    glob: Vec<String>,
30
31    /// pattern to find case-insensitive (can be specified multiple times)
32    #[clap(long, value_name = "PATTERN", conflicts_with = "path")]
33    iglob: Vec<String>,
34
35    /// exact path to find
36    #[clap(long, value_name = "PATH", value_hint = ValueHint::AnyPath)]
37    path: Option<PathBuf>,
38
39    /// Snapshots to search in. If none is given, use filter options to filter from all snapshots
40    #[clap(value_name = "ID")]
41    ids: Vec<String>,
42
43    /// Show all snapshots instead of summarizing snapshots with identical search results
44    #[clap(long)]
45    all: bool,
46
47    /// Also show snapshots which don't contain a search result.
48    #[clap(long)]
49    show_misses: bool,
50
51    /// Show uid/gid instead of user/group
52    #[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}