rustic_rs/
repository.rs

1//! Rustic Config
2//!
3//! See instructions in `commands.rs` to specify the path to your
4//! application's configuration file and/or command-line options
5//! for specifying it.
6
7use std::collections::HashMap;
8use std::fmt::Debug;
9use std::ops::Deref;
10
11use abscissa_core::Application;
12use anyhow::{Result, anyhow, bail};
13use clap::Parser;
14use conflate::Merge;
15use dialoguer::Password;
16use rustic_backend::BackendOptions;
17use rustic_core::{
18    FullIndex, IndexedStatus, Open, OpenStatus, ProgressBars, Repository, RepositoryOptions,
19    SnapshotGroup, SnapshotGroupCriterion, repofile::SnapshotFile,
20};
21use serde::{Deserialize, Serialize};
22
23use crate::{
24    RUSTIC_APP,
25    config::{hooks::Hooks, progress_options::ProgressOptions},
26};
27
28pub(super) mod constants {
29    pub(super) const MAX_PASSWORD_RETRIES: usize = 5;
30}
31
32#[derive(Clone, Default, Debug, Parser, Serialize, Deserialize, Merge)]
33#[serde(default, rename_all = "kebab-case")]
34pub struct AllRepositoryOptions {
35    /// Backend options
36    #[clap(flatten)]
37    #[serde(flatten)]
38    pub be: BackendOptions,
39
40    /// Repository options
41    #[clap(flatten)]
42    #[serde(flatten)]
43    pub repo: RepositoryOptions,
44
45    /// Hooks
46    #[clap(skip)]
47    pub hooks: Hooks,
48}
49
50pub type CliRepo = RusticRepo<ProgressOptions>;
51pub type CliOpenRepo = Repository<ProgressOptions, OpenStatus>;
52pub type RusticIndexedRepo<P> = Repository<P, IndexedStatus<FullIndex, OpenStatus>>;
53pub type CliIndexedRepo = RusticIndexedRepo<ProgressOptions>;
54
55impl AllRepositoryOptions {
56    pub fn repository<P>(&self, po: P) -> Result<RusticRepo<P>> {
57        let backends = self.be.to_backends()?;
58        let repo = Repository::new_with_progress(&self.repo, &backends, po)?;
59        Ok(RusticRepo(repo))
60    }
61
62    pub fn run_with_progress<P: Clone + ProgressBars, T>(
63        &self,
64        po: P,
65        f: impl FnOnce(RusticRepo<P>) -> Result<T>,
66    ) -> Result<T> {
67        let hooks = self
68            .hooks
69            .with_env(&HashMap::from([(
70                "RUSTIC_ACTION".to_string(),
71                "repository".to_string(),
72            )]))
73            .with_context("repository");
74        hooks.use_with(|| f(self.repository(po)?))
75    }
76
77    pub fn run<T>(&self, f: impl FnOnce(CliRepo) -> Result<T>) -> Result<T> {
78        let po = RUSTIC_APP.config().global.progress_options;
79        self.run_with_progress(po, f)
80    }
81
82    pub fn run_open<T>(&self, f: impl FnOnce(CliOpenRepo) -> Result<T>) -> Result<T> {
83        self.run(|repo| f(repo.open()?))
84    }
85
86    pub fn run_open_or_init_with<T: Clone>(
87        &self,
88        do_init: bool,
89        init: impl FnOnce(CliRepo) -> Result<CliOpenRepo>,
90        f: impl FnOnce(CliOpenRepo) -> Result<T>,
91    ) -> Result<T> {
92        self.run(|repo| f(repo.open_or_init_repository_with(do_init, init)?))
93    }
94
95    pub fn run_indexed_with_progress<P: Clone + ProgressBars, T>(
96        &self,
97        po: P,
98        f: impl FnOnce(RusticIndexedRepo<P>) -> Result<T>,
99    ) -> Result<T> {
100        self.run_with_progress(po, |repo| f(repo.indexed()?))
101    }
102
103    pub fn run_indexed<T>(&self, f: impl FnOnce(CliIndexedRepo) -> Result<T>) -> Result<T> {
104        self.run(|repo| f(repo.indexed()?))
105    }
106}
107
108#[derive(Debug)]
109pub struct RusticRepo<P>(pub Repository<P, ()>);
110
111impl<P> Deref for RusticRepo<P> {
112    type Target = Repository<P, ()>;
113    fn deref(&self) -> &Self::Target {
114        &self.0
115    }
116}
117
118impl<P: Clone + ProgressBars> RusticRepo<P> {
119    pub fn open(self) -> Result<Repository<P, OpenStatus>> {
120        match self.0.password()? {
121            // if password is given, directly return the result of find_key_in_backend and don't retry
122            Some(pass) => {
123                return Ok(self.0.open_with_password(&pass)?);
124            }
125            None => {
126                for _ in 0..constants::MAX_PASSWORD_RETRIES {
127                    let pass = Password::new()
128                        .with_prompt("enter repository password")
129                        .allow_empty_password(true)
130                        .interact()?;
131                    match self.0.clone().open_with_password(&pass) {
132                        Ok(repo) => return Ok(repo),
133                        Err(err) if err.is_incorrect_password() => continue,
134                        Err(err) => return Err(err.into()),
135                    }
136                }
137            }
138        }
139        Err(anyhow!("incorrect password"))
140    }
141
142    fn open_or_init_repository_with(
143        self,
144        do_init: bool,
145        init: impl FnOnce(Self) -> Result<Repository<P, OpenStatus>>,
146    ) -> Result<Repository<P, OpenStatus>> {
147        let dry_run = RUSTIC_APP.config().global.check_index;
148        // Initialize repository if --init is set and it is not yet initialized
149        let repo = if do_init && self.0.config_id()?.is_none() {
150            if dry_run {
151                bail!(
152                    "cannot initialize repository {} in dry-run mode!",
153                    self.0.name
154                );
155            }
156            init(self)?
157        } else {
158            self.open()?
159        };
160        Ok(repo)
161    }
162
163    fn indexed(self) -> Result<Repository<P, IndexedStatus<FullIndex, OpenStatus>>> {
164        let open = self.open()?;
165        let check_index = RUSTIC_APP.config().global.check_index;
166        let repo = if check_index {
167            open.to_indexed_checked()
168        } else {
169            open.to_indexed()
170        }?;
171        Ok(repo)
172    }
173}
174
175pub fn get_filtered_snapshots<P: ProgressBars, S: Open>(
176    repo: &Repository<P, S>,
177) -> Result<Vec<SnapshotFile>> {
178    let config = RUSTIC_APP.config();
179    let mut snapshots = repo.get_matching_snapshots(|sn| config.snapshot_filter.matches(sn))?;
180    config.snapshot_filter.post_process(&mut snapshots);
181    Ok(snapshots)
182}
183
184pub fn get_global_grouped_snapshots<P: ProgressBars, S: Open>(
185    repo: &Repository<P, S>,
186    ids: &[String],
187) -> Result<Vec<(SnapshotGroup, Vec<SnapshotFile>)>> {
188    let config = RUSTIC_APP.config();
189    get_grouped_snapshots(repo, config.global.group_by.unwrap_or_default(), ids)
190}
191
192pub fn get_grouped_snapshots<P: ProgressBars, S: Open>(
193    repo: &Repository<P, S>,
194    group_by: SnapshotGroupCriterion,
195    ids: &[String],
196) -> Result<Vec<(SnapshotGroup, Vec<SnapshotFile>)>> {
197    let config = RUSTIC_APP.config();
198    let mut groups =
199        repo.get_snapshot_group(ids, group_by, |sn| config.snapshot_filter.matches(sn))?;
200
201    for (_, snaps) in &mut groups {
202        config.snapshot_filter.post_process(snaps);
203    }
204    Ok(groups)
205}