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