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 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 #[clap(flatten)]
37 #[serde(flatten)]
38 pub be: BackendOptions,
39
40 #[clap(flatten)]
42 #[serde(flatten)]
43 pub repo: RepositoryOptions,
44
45 #[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 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 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}