1pub(crate) mod backup;
4pub(crate) mod cat;
5pub(crate) mod check;
6pub(crate) mod completions;
7pub(crate) mod config;
8pub(crate) mod copy;
9pub(crate) mod diff;
10pub(crate) mod docs;
11pub(crate) mod dump;
12pub(crate) mod find;
13pub(crate) mod forget;
14pub(crate) mod init;
15pub(crate) mod key;
16pub(crate) mod list;
17pub(crate) mod ls;
18pub(crate) mod merge;
19#[cfg(feature = "mount")]
20pub(crate) mod mount;
21pub(crate) mod prune;
22pub(crate) mod repair;
23pub(crate) mod repoinfo;
24pub(crate) mod restore;
25pub(crate) mod self_update;
26pub(crate) mod show_config;
27pub(crate) mod snapshots;
28pub(crate) mod tag;
29#[cfg(feature = "tui")]
30pub(crate) mod tui;
31#[cfg(feature = "webdav")]
32pub(crate) mod webdav;
33
34use std::fmt::Debug;
35use std::fs::File;
36use std::path::PathBuf;
37use std::str::FromStr;
38use std::sync::mpsc::channel;
39
40#[cfg(feature = "mount")]
41use crate::commands::mount::MountCmd;
42#[cfg(feature = "webdav")]
43use crate::commands::webdav::WebDavCmd;
44use crate::{
45 commands::{
46 backup::BackupCmd, cat::CatCmd, check::CheckCmd, completions::CompletionsCmd,
47 config::ConfigCmd, copy::CopyCmd, diff::DiffCmd, docs::DocsCmd, dump::DumpCmd,
48 forget::ForgetCmd, init::InitCmd, key::KeyCmd, list::ListCmd, ls::LsCmd, merge::MergeCmd,
49 prune::PruneCmd, repair::RepairCmd, repoinfo::RepoInfoCmd, restore::RestoreCmd,
50 self_update::SelfUpdateCmd, show_config::ShowConfigCmd, snapshots::SnapshotCmd,
51 tag::TagCmd,
52 },
53 config::RusticConfig,
54 Application, RUSTIC_APP,
55};
56
57use abscissa_core::{
58 config::Override, terminal::ColorChoice, Command, Configurable, FrameworkError,
59 FrameworkErrorKind, Runnable, Shutdown,
60};
61use anyhow::Result;
62use clap::builder::{
63 styling::{AnsiColor, Effects},
64 Styles,
65};
66use convert_case::{Case, Casing};
67use human_panic::setup_panic;
68use log::{info, log, Level};
69use simplelog::{CombinedLogger, LevelFilter, TermLogger, TerminalMode, WriteLogger};
70
71use self::find::FindCmd;
72
73#[derive(clap::Parser, Command, Debug, Runnable)]
76enum RusticCmd {
77 Backup(Box<BackupCmd>),
79
80 Cat(Box<CatCmd>),
82
83 Config(Box<ConfigCmd>),
85
86 Completions(Box<CompletionsCmd>),
88
89 Check(Box<CheckCmd>),
91
92 Copy(Box<CopyCmd>),
94
95 Diff(Box<DiffCmd>),
97
98 Docs(Box<DocsCmd>),
100
101 Dump(Box<DumpCmd>),
103
104 Find(Box<FindCmd>),
106
107 Forget(Box<ForgetCmd>),
109
110 Init(Box<InitCmd>),
112
113 Key(Box<KeyCmd>),
115
116 List(Box<ListCmd>),
118
119 #[cfg(feature = "mount")]
120 Mount(Box<MountCmd>),
122
123 Ls(Box<LsCmd>),
125
126 Merge(Box<MergeCmd>),
128
129 Snapshots(Box<SnapshotCmd>),
131
132 ShowConfig(Box<ShowConfigCmd>),
134
135 #[cfg_attr(not(feature = "self-update"), clap(hide = true))]
137 SelfUpdate(Box<SelfUpdateCmd>),
138
139 Prune(Box<PruneCmd>),
141
142 Restore(Box<RestoreCmd>),
144
145 Repair(Box<RepairCmd>),
147
148 Repoinfo(Box<RepoInfoCmd>),
150
151 Tag(Box<TagCmd>),
153
154 #[cfg(feature = "webdav")]
156 Webdav(Box<WebDavCmd>),
157}
158
159fn styles() -> Styles {
160 Styles::styled()
161 .header(AnsiColor::Red.on_default() | Effects::BOLD)
162 .usage(AnsiColor::Red.on_default() | Effects::BOLD)
163 .literal(AnsiColor::Blue.on_default() | Effects::BOLD)
164 .placeholder(AnsiColor::Green.on_default())
165}
166
167#[derive(clap::Parser, Command, Debug)]
169#[command(author, about, name="rustic", styles=styles(), version = option_env!("PROJECT_VERSION").unwrap_or(env!("CARGO_PKG_VERSION")))]
170pub struct EntryPoint {
171 #[command(flatten)]
172 pub config: RusticConfig,
173
174 #[command(subcommand)]
175 commands: RusticCmd,
176}
177
178impl Runnable for EntryPoint {
179 fn run(&self) {
180 setup_panic!();
182
183 let (tx, rx) = channel();
185
186 ctrlc::set_handler(move || tx.send(()).expect("Could not send signal on channel."))
187 .expect("Error setting Ctrl-C handler");
188
189 _ = std::thread::spawn(move || {
190 rx.recv().expect("Could not receive from channel.");
192 info!("Ctrl-C received, shutting down...");
193 RUSTIC_APP.shutdown(Shutdown::Graceful)
194 });
195
196 self.commands.run();
198 RUSTIC_APP.shutdown(Shutdown::Graceful)
199 }
200}
201
202impl Configurable<RusticConfig> for EntryPoint {
204 fn config_path(&self) -> Option<PathBuf> {
206 None
209 }
210
211 fn process_config(&self, _config: RusticConfig) -> Result<RusticConfig, FrameworkError> {
214 let mut config = self.config.clone();
218
219 for (var, value) in std::env::vars() {
221 if let Some(var) = var.strip_prefix("RUSTIC_REPO_OPT_") {
222 let var = var.from_case(Case::UpperSnake).to_case(Case::Kebab);
223 _ = config.repository.be.options.insert(var, value);
224 } else if let Some(var) = var.strip_prefix("OPENDAL_") {
225 let var = var.from_case(Case::UpperSnake).to_case(Case::Snake);
226 _ = config.repository.be.options.insert(var, value);
227 } else if let Some(var) = var.strip_prefix("RUSTIC_REPO_OPTHOT_") {
228 let var = var.from_case(Case::UpperSnake).to_case(Case::Kebab);
229 _ = config.repository.be.options_hot.insert(var, value);
230 } else if let Some(var) = var.strip_prefix("RUSTIC_REPO_OPTCOLD_") {
231 let var = var.from_case(Case::UpperSnake).to_case(Case::Kebab);
232 _ = config.repository.be.options_cold.insert(var, value);
233 }
234 }
235
236 let mut merge_logs = Vec::new();
238
239 if config.global.use_profiles.is_empty() {
241 config.merge_profile("rustic", &mut merge_logs, Level::Info)?;
242 } else {
243 for profile in &config.global.use_profiles.clone() {
244 config.merge_profile(profile, &mut merge_logs, Level::Warn)?;
245 }
246 }
247
248 let level_filter = match &config.global.log_level {
250 Some(level) => LevelFilter::from_str(level)
251 .map_err(|e| FrameworkErrorKind::ConfigError.context(e))?,
252 None => LevelFilter::Info,
253 };
254 let term_config = simplelog::ConfigBuilder::new()
255 .set_time_level(LevelFilter::Off)
256 .build();
257 match &config.global.log_file {
258 None => TermLogger::init(
259 level_filter,
260 term_config,
261 TerminalMode::Stderr,
262 ColorChoice::Auto,
263 )
264 .map_err(|e| FrameworkErrorKind::ConfigError.context(e))?,
265
266 Some(file) => {
267 let file_config = simplelog::ConfigBuilder::new()
268 .set_time_format_rfc3339()
269 .build();
270 let file = File::options()
271 .create(true)
272 .append(true)
273 .open(file)
274 .map_err(|e| {
275 FrameworkErrorKind::PathError {
276 name: Some(file.clone()),
277 }
278 .context(e)
279 })?;
280 let term_logger = TermLogger::new(
281 level_filter.min(LevelFilter::Warn),
282 term_config,
283 TerminalMode::Stderr,
284 ColorChoice::Auto,
285 );
286 CombinedLogger::init(vec![
287 term_logger,
288 WriteLogger::new(level_filter, file_config, file),
289 ])
290 .map_err(|e| FrameworkErrorKind::ConfigError.context(e))?;
291 }
292 }
293
294 for (level, merge_log) in merge_logs {
296 log!(level, "{}", merge_log);
297 }
298
299 match &self.commands {
300 RusticCmd::Forget(cmd) => cmd.override_config(config),
301 RusticCmd::Copy(cmd) => cmd.override_config(config),
302 #[cfg(feature = "webdav")]
303 RusticCmd::Webdav(cmd) => cmd.override_config(config),
304 #[cfg(feature = "mount")]
305 RusticCmd::Mount(cmd) => cmd.override_config(config),
306
307 _ => Ok(config),
309 }
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use crate::commands::EntryPoint;
316 use clap::CommandFactory;
317
318 #[test]
319 fn verify_cli() {
320 EntryPoint::command().debug_assert();
321 }
322}