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 Application, RUSTIC_APP,
46 commands::{
47 backup::BackupCmd, cat::CatCmd, check::CheckCmd, completions::CompletionsCmd,
48 config::ConfigCmd, copy::CopyCmd, diff::DiffCmd, docs::DocsCmd, dump::DumpCmd,
49 forget::ForgetCmd, init::InitCmd, key::KeyCmd, list::ListCmd, ls::LsCmd, merge::MergeCmd,
50 prune::PruneCmd, repair::RepairCmd, repoinfo::RepoInfoCmd, restore::RestoreCmd,
51 self_update::SelfUpdateCmd, show_config::ShowConfigCmd, snapshots::SnapshotCmd,
52 tag::TagCmd,
53 },
54 config::RusticConfig,
55};
56
57use abscissa_core::{
58 Command, Configurable, FrameworkError, FrameworkErrorKind, Runnable, Shutdown,
59 config::Override, terminal::ColorChoice,
60};
61use anyhow::Result;
62use clap::builder::{
63 Styles,
64 styling::{AnsiColor, Effects},
65};
66use convert_case::{Case, Casing};
67use human_panic::setup_panic;
68use log::{Level, info, log};
69use reqwest::Url;
70use simplelog::{CombinedLogger, LevelFilter, TermLogger, TerminalMode, WriteLogger};
71
72use self::find::FindCmd;
73
74#[derive(clap::Parser, Command, Debug, Runnable)]
77enum RusticCmd {
78 Backup(Box<BackupCmd>),
80
81 Cat(Box<CatCmd>),
83
84 Config(Box<ConfigCmd>),
86
87 Completions(Box<CompletionsCmd>),
89
90 Check(Box<CheckCmd>),
92
93 Copy(Box<CopyCmd>),
95
96 Diff(Box<DiffCmd>),
98
99 Docs(Box<DocsCmd>),
101
102 Dump(Box<DumpCmd>),
104
105 Find(Box<FindCmd>),
107
108 Forget(Box<ForgetCmd>),
110
111 Init(Box<InitCmd>),
113
114 Key(Box<KeyCmd>),
116
117 List(Box<ListCmd>),
119
120 #[cfg(feature = "mount")]
121 Mount(Box<MountCmd>),
123
124 Ls(Box<LsCmd>),
126
127 Merge(Box<MergeCmd>),
129
130 Snapshots(Box<SnapshotCmd>),
132
133 ShowConfig(Box<ShowConfigCmd>),
135
136 #[cfg_attr(not(feature = "self-update"), clap(hide = true))]
138 SelfUpdate(Box<SelfUpdateCmd>),
139
140 Prune(Box<PruneCmd>),
142
143 Restore(Box<RestoreCmd>),
145
146 Repair(Box<RepairCmd>),
148
149 Repoinfo(Box<RepoInfoCmd>),
151
152 Tag(Box<TagCmd>),
154
155 #[cfg(feature = "webdav")]
157 Webdav(Box<WebDavCmd>),
158}
159
160fn styles() -> Styles {
161 Styles::styled()
162 .header(AnsiColor::Red.on_default() | Effects::BOLD)
163 .usage(AnsiColor::Red.on_default() | Effects::BOLD)
164 .literal(AnsiColor::Blue.on_default() | Effects::BOLD)
165 .placeholder(AnsiColor::Green.on_default())
166}
167
168fn version() -> &'static str {
169 option_env!("PROJECT_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"))
170}
171
172#[derive(clap::Parser, Command, Debug)]
174#[command(author, about, name="rustic", styles=styles(), version=version())]
175pub struct EntryPoint {
176 #[command(flatten)]
177 pub config: RusticConfig,
178
179 #[command(subcommand)]
180 commands: RusticCmd,
181}
182
183impl Runnable for EntryPoint {
184 fn run(&self) {
185 setup_panic!();
187
188 let (tx, rx) = channel();
190
191 ctrlc::set_handler(move || tx.send(()).expect("Could not send signal on channel."))
192 .expect("Error setting Ctrl-C handler");
193
194 _ = std::thread::spawn(move || {
195 rx.recv().expect("Could not receive from channel.");
197 info!("Ctrl-C received, shutting down...");
198 RUSTIC_APP.shutdown(Shutdown::Graceful)
199 });
200
201 self.commands.run();
203 RUSTIC_APP.shutdown(Shutdown::Graceful)
204 }
205}
206
207impl Configurable<RusticConfig> for EntryPoint {
209 fn config_path(&self) -> Option<PathBuf> {
211 None
214 }
215
216 fn process_config(&self, _config: RusticConfig) -> Result<RusticConfig, FrameworkError> {
219 let mut config = self.config.clone();
223
224 for (var, value) in std::env::vars() {
228 if let Some(var) = var.strip_prefix("RUSTIC_REPO_OPT_") {
229 let var = var.from_case(Case::UpperSnake).to_case(Case::Kebab);
230 _ = config.repository.be.options.insert(var, value);
231 } else if let Some(var) = var.strip_prefix("OPENDAL_") {
232 let var = var.from_case(Case::UpperSnake).to_case(Case::Snake);
233 _ = config.repository.be.options.insert(var, value);
234 } else if let Some(var) = var.strip_prefix("RUSTIC_REPO_OPTHOT_") {
235 let var = var.from_case(Case::UpperSnake).to_case(Case::Kebab);
236 _ = config.repository.be.options_hot.insert(var, value);
237 } else if let Some(var) = var.strip_prefix("RUSTIC_REPO_OPTCOLD_") {
238 let var = var.from_case(Case::UpperSnake).to_case(Case::Kebab);
239 _ = config.repository.be.options_cold.insert(var, value);
240 } else if let Some(var) = var.strip_prefix("OPENDALHOT_") {
241 let var = var.from_case(Case::UpperSnake).to_case(Case::Snake);
242 _ = config.repository.be.options_hot.insert(var, value);
243 } else if let Some(var) = var.strip_prefix("OPENDALCOLD_") {
244 let var = var.from_case(Case::UpperSnake).to_case(Case::Snake);
245 _ = config.repository.be.options_cold.insert(var, value);
246 } else if var == "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT" {
247 #[cfg(feature = "opentelemetry")]
248 if let Ok(url) = Url::parse(&value) {
249 _ = config.global.opentelemetry.insert(url);
250 }
251 } else if var == "OTEL_SERVICE_NAME" && cfg!(feature = "opentelemetry") {
252 _ = config.backup.metrics_job.insert(value);
253 }
254 }
255
256 let mut merge_logs = Vec::new();
258
259 if config.global.use_profiles.is_empty() {
261 config.merge_profile("rustic", &mut merge_logs, Level::Info)?;
262 } else {
263 for profile in &config.global.use_profiles.clone() {
264 config.merge_profile(profile, &mut merge_logs, Level::Warn)?;
265 }
266 }
267
268 let level_filter = match &config.global.log_level {
270 Some(level) => LevelFilter::from_str(level)
271 .map_err(|e| FrameworkErrorKind::ConfigError.context(e))?,
272 None => LevelFilter::Info,
273 };
274 let term_config = simplelog::ConfigBuilder::new()
275 .set_time_level(LevelFilter::Off)
276 .build();
277 match &config.global.log_file {
278 None => TermLogger::init(
279 level_filter,
280 term_config,
281 TerminalMode::Stderr,
282 ColorChoice::Auto,
283 )
284 .map_err(|e| FrameworkErrorKind::ConfigError.context(e))?,
285
286 Some(file) => {
287 let file_config = simplelog::ConfigBuilder::new()
288 .set_time_format_rfc3339()
289 .build();
290 let file = File::options()
291 .create(true)
292 .append(true)
293 .open(file)
294 .map_err(|e| {
295 FrameworkErrorKind::PathError {
296 name: Some(file.clone()),
297 }
298 .context(e)
299 })?;
300 let term_logger = TermLogger::new(
301 level_filter.min(LevelFilter::Warn),
302 term_config,
303 TerminalMode::Stderr,
304 ColorChoice::Auto,
305 );
306 CombinedLogger::init(vec![
307 term_logger,
308 WriteLogger::new(level_filter, file_config, file),
309 ])
310 .map_err(|e| FrameworkErrorKind::ConfigError.context(e))?;
311 info!("rustic {}", version());
312 info!("command: {:?}", std::env::args_os().collect::<Vec<_>>());
313 }
314 }
315
316 for (level, merge_log) in merge_logs {
318 log!(level, "{merge_log}");
319 }
320
321 match &self.commands {
322 RusticCmd::Forget(cmd) => cmd.override_config(config),
323 RusticCmd::Copy(cmd) => cmd.override_config(config),
324 #[cfg(feature = "webdav")]
325 RusticCmd::Webdav(cmd) => cmd.override_config(config),
326 #[cfg(feature = "mount")]
327 RusticCmd::Mount(cmd) => cmd.override_config(config),
328
329 _ => Ok(config),
331 }
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use crate::commands::EntryPoint;
338 use clap::CommandFactory;
339
340 #[test]
341 fn verify_cli() {
342 EntryPoint::command().debug_assert();
343 }
344}