1use 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 #[clap(flatten)]
34 #[serde(flatten)]
35 pub be: BackendOptions,
36
37 #[clap(flatten)]
39 #[serde(flatten)]
40 pub repo: RepositoryOptions,
41
42 #[clap(flatten, next_help_heading = "credential options")]
44 #[serde(flatten)]
45 pub credential_opts: CredentialOptions,
46
47 #[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 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() .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 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
181pub 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
195pub 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}