Skip to main content

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    CredentialOptions, Credentials, Grouped, IndexedFullStatus, IndexedIdsStatus, Open, OpenStatus,
19    ProgressBars, Repository, RepositoryOptions, RusticResult, SnapshotGroupCriterion,
20    repofile::SnapshotFile,
21};
22use serde::{Deserialize, Serialize};
23
24use crate::{RUSTIC_APP, config::hooks::Hooks};
25
26pub(super) mod constants {
27    pub(super) const MAX_PASSWORD_RETRIES: usize = 5;
28}
29
30#[derive(Clone, Default, Debug, Parser, Serialize, Deserialize, Merge)]
31#[serde(default, rename_all = "kebab-case")]
32pub struct AllRepositoryOptions {
33    /// Backend options
34    #[clap(flatten)]
35    #[serde(flatten)]
36    pub be: BackendOptions,
37
38    /// Repository options
39    #[clap(flatten)]
40    #[serde(flatten)]
41    pub repo: RepositoryOptions,
42
43    /// Credential options
44    #[clap(flatten, next_help_heading = "credential options")]
45    #[serde(flatten)]
46    pub credential_opts: CredentialOptions,
47
48    /// Hooks
49    #[clap(skip)]
50    pub hooks: Hooks,
51}
52
53impl AllRepositoryOptions {
54    pub fn repository(&self, po: impl ProgressBars) -> Result<Repo> {
55        let backends = self.be.to_backends()?;
56        let repo = Repository::new_with_progress(&self.repo, &backends, po)?;
57        Ok(Repo(repo))
58    }
59
60    pub fn run_with_progress<T>(
61        &self,
62        po: impl ProgressBars,
63        f: impl FnOnce(Repo) -> Result<T>,
64    ) -> Result<T> {
65        let hooks = self
66            .hooks
67            .with_env(&HashMap::from([(
68                "RUSTIC_ACTION".to_string(),
69                "repository".to_string(),
70            )]))
71            .with_context("repository");
72        hooks.use_with(|| f(self.repository(po)?))
73    }
74
75    pub fn run<T>(&self, f: impl FnOnce(Repo) -> Result<T>) -> Result<T> {
76        let po = RUSTIC_APP.config().global.progress_options;
77        self.run_with_progress(po, f)
78    }
79
80    pub fn run_open<T>(&self, f: impl FnOnce(OpenRepo) -> Result<T>) -> Result<T> {
81        self.run(|repo| f(repo.open(&self.credential_opts)?))
82    }
83
84    pub fn run_open_or_init_with<T: Clone>(
85        &self,
86        do_init: bool,
87        init: impl FnOnce(Repo) -> Result<OpenRepo>,
88        f: impl FnOnce(OpenRepo) -> Result<T>,
89    ) -> Result<T> {
90        self.run(|repo| {
91            f(repo.open_or_init_repository_with(&self.credential_opts, do_init, init)?)
92        })
93    }
94
95    pub fn run_indexed_with_progress<T>(
96        &self,
97        po: impl ProgressBars,
98        f: impl FnOnce(IndexedRepo) -> Result<T>,
99    ) -> Result<T> {
100        self.run_with_progress(po, |repo| f(repo.indexed(&self.credential_opts)?))
101    }
102
103    pub fn run_indexed<T>(&self, f: impl FnOnce(IndexedRepo) -> Result<T>) -> Result<T> {
104        self.run(|repo| f(repo.indexed(&self.credential_opts)?))
105    }
106}
107
108pub type OpenRepo = Repository<OpenStatus>;
109pub type IndexedRepo = Repository<IndexedFullStatus>;
110pub type IndexedIdsRepo = Repository<IndexedIdsStatus>;
111
112#[derive(Debug)]
113pub struct Repo(pub Repository<()>);
114
115impl Deref for Repo {
116    type Target = Repository<()>;
117    fn deref(&self) -> &Self::Target {
118        &self.0
119    }
120}
121
122impl Repo {
123    pub fn open_with(
124        self,
125        credential_opts: &CredentialOptions,
126        open: impl Fn(Repository<()>, &Credentials) -> RusticResult<OpenRepo>,
127    ) -> Result<OpenRepo> {
128        match credential_opts.credentials()? {
129            // if credentials are given, directly open the repository and don't retry
130            Some(credentials) => Ok(open(self.0, &credentials)?),
131            None => {
132                for _ in 0..constants::MAX_PASSWORD_RETRIES {
133                    let pass = Password::new()
134                        .with_prompt("enter repository password")
135                        .allow_empty_password(true)
136                        .interact()?;
137                    match open(self.0.clone(), &Credentials::Password(pass)) {
138                        Ok(repo) => return Ok(repo),
139                        Err(err) if err.is_incorrect_password() => continue,
140                        Err(err) => return Err(err.into()),
141                    }
142                }
143                Err(anyhow!("incorrect password"))
144            }
145        }
146    }
147    pub fn open(self, credential_opts: &CredentialOptions) -> Result<OpenRepo> {
148        self.open_with(credential_opts, |repo, credentials| repo.open(credentials))
149    }
150
151    fn open_or_init_repository_with(
152        self,
153        credential_opts: &CredentialOptions,
154        do_init: bool,
155        init: impl FnOnce(Self) -> Result<OpenRepo>,
156    ) -> Result<OpenRepo> {
157        let dry_run = RUSTIC_APP.config().global.check_index;
158        // Initialize repository if --init is set and it is not yet initialized
159        let repo = if do_init && self.0.config_id()?.is_none() {
160            if dry_run {
161                bail!(
162                    "cannot initialize repository {} in dry-run mode!",
163                    self.0.name
164                );
165            }
166            init(self)?
167        } else {
168            self.open(credential_opts)?
169        };
170        Ok(repo)
171    }
172
173    fn indexed(self, credential_opts: &CredentialOptions) -> Result<IndexedRepo> {
174        let open = self.open(credential_opts)?;
175        let check_index = RUSTIC_APP.config().global.check_index;
176        let repo = if check_index {
177            open.to_indexed_checked()
178        } else {
179            open.to_indexed()
180        }?;
181        Ok(repo)
182    }
183}
184
185// get snapshots from ids allowing `latest`, if empty use all snapshots respecting the filters.
186pub fn get_snapots_from_ids<S: Open>(
187    repo: &Repository<S>,
188    ids: &[String],
189) -> Result<Vec<SnapshotFile>> {
190    let config = RUSTIC_APP.config();
191    let snapshots = if ids.is_empty() {
192        get_filtered_snapshots(repo)?
193    } else {
194        repo.get_snapshots_from_strs(ids, |sn| config.snapshot_filter.matches(sn))?
195    };
196    Ok(snapshots)
197}
198
199// get all snapshots respecting the filters
200pub fn get_filtered_snapshots<S: Open>(repo: &Repository<S>) -> Result<Vec<SnapshotFile>> {
201    let config = RUSTIC_APP.config();
202    let mut snapshots = repo.get_matching_snapshots(|sn| config.snapshot_filter.matches(sn))?;
203    config.snapshot_filter.post_process(&mut snapshots);
204    Ok(snapshots)
205}
206
207pub fn get_global_grouped_snapshots<S: Open>(
208    repo: &Repository<S>,
209    ids: &[String],
210) -> Result<Grouped<SnapshotFile>> {
211    let config = RUSTIC_APP.config();
212    get_grouped_snapshots(repo, config.global.group_by.unwrap_or_default(), ids)
213}
214
215pub fn get_grouped_snapshots<S: Open>(
216    repo: &Repository<S>,
217    group_by: SnapshotGroupCriterion,
218    ids: &[String],
219) -> Result<Grouped<SnapshotFile>> {
220    let config = RUSTIC_APP.config();
221    let snapshots = if ids.is_empty() {
222        repo.get_matching_snapshots(|sn| config.snapshot_filter.matches(sn))?
223    } else {
224        repo.get_snapshots_from_strs(ids, |sn| config.snapshot_filter.matches(sn))?
225    };
226    let mut group = Grouped::from_items(snapshots, group_by);
227    for group in &mut group.groups {
228        config.snapshot_filter.post_process(&mut group.items);
229    }
230
231    Ok(group)
232}