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