Skip to main content

rustic_rs/commands/
find.rs

1//! `find` subcommand
2
3use 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/// `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    ///
41    /// Snapshots can be identified the following ways: "01a2b3c4" or "latest" or "latest~N" (N >= 0)
42    #[clap(value_name = "ID")]
43    ids: Vec<String>,
44
45    /// Show all snapshots instead of summarizing snapshots with identical search results
46    #[clap(long)]
47    all: bool,
48
49    /// Also show snapshots which don't contain a search result.
50    #[clap(long)]
51    show_misses: bool,
52
53    /// Show uid/gid instead of user/group
54    #[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}