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, 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 #[clap(flatten)]
35 #[serde(flatten)]
36 pub be: BackendOptions,
37
38 #[clap(flatten)]
40 #[serde(flatten)]
41 pub repo: RepositoryOptions,
42
43 #[clap(flatten, next_help_heading = "credential options")]
45 #[serde(flatten)]
46 pub credential_opts: CredentialOptions,
47
48 #[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 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 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
185pub 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
199pub 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}