rustic_rs/
commands.rs

1//! Rustic Subcommands
2
3pub(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/// Rustic Subcommands
75/// Subcommands need to be listed in an enum.
76#[derive(clap::Parser, Command, Debug, Runnable)]
77enum RusticCmd {
78    /// Backup to the repository
79    Backup(Box<BackupCmd>),
80
81    /// Show raw data of files and blobs in a repository
82    Cat(Box<CatCmd>),
83
84    /// Change the repository configuration
85    Config(Box<ConfigCmd>),
86
87    /// Generate shell completions
88    Completions(Box<CompletionsCmd>),
89
90    /// Check the repository
91    Check(Box<CheckCmd>),
92
93    /// Copy snapshots to other repositories
94    Copy(Box<CopyCmd>),
95
96    /// Compare two snapshots or paths
97    Diff(Box<DiffCmd>),
98
99    /// Open the documentation
100    Docs(Box<DocsCmd>),
101
102    /// Dump the contents of a file within a snapshot to stdout
103    Dump(Box<DumpCmd>),
104
105    /// Find patterns in given snapshots
106    Find(Box<FindCmd>),
107
108    /// Remove snapshots from the repository
109    Forget(Box<ForgetCmd>),
110
111    /// Initialize a new repository
112    Init(Box<InitCmd>),
113
114    /// Manage keys for a repository
115    Key(Box<KeyCmd>),
116
117    /// List repository files by file type
118    List(Box<ListCmd>),
119
120    #[cfg(feature = "mount")]
121    /// Mount a repository as read-only filesystem
122    Mount(Box<MountCmd>),
123
124    /// List file contents of a snapshot
125    Ls(Box<LsCmd>),
126
127    /// Merge snapshots
128    Merge(Box<MergeCmd>),
129
130    /// Show a detailed overview of the snapshots within the repository
131    Snapshots(Box<SnapshotCmd>),
132
133    /// Show the configuration which has been read from the config file(s)
134    ShowConfig(Box<ShowConfigCmd>),
135
136    /// Update to the latest stable rustic release
137    #[cfg_attr(not(feature = "self-update"), clap(hide = true))]
138    SelfUpdate(Box<SelfUpdateCmd>),
139
140    /// Remove unused data or repack repository pack files
141    Prune(Box<PruneCmd>),
142
143    /// Restore (a path within) a snapshot
144    Restore(Box<RestoreCmd>),
145
146    /// Repair a snapshot or the repository index
147    Repair(Box<RepairCmd>),
148
149    /// Show general information about the repository
150    Repoinfo(Box<RepoInfoCmd>),
151
152    /// Change tags of snapshots
153    Tag(Box<TagCmd>),
154
155    /// Start a webdav server which allows to access the repository
156    #[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/// Entry point for the application. It needs to be a struct to allow using subcommands!
173#[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        // Set up panic hook for better error messages and logs
186        setup_panic!();
187
188        // Set up Ctrl-C handler
189        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            // Wait for Ctrl-C
196            rx.recv().expect("Could not receive from channel.");
197            info!("Ctrl-C received, shutting down...");
198            RUSTIC_APP.shutdown(Shutdown::Graceful)
199        });
200
201        // Run the subcommand
202        self.commands.run();
203        RUSTIC_APP.shutdown(Shutdown::Graceful)
204    }
205}
206
207/// This trait allows you to define how application configuration is loaded.
208impl Configurable<RusticConfig> for EntryPoint {
209    /// Location of the configuration file
210    fn config_path(&self) -> Option<PathBuf> {
211        // Actually abscissa itself reads a config from `config_path`, but I have now returned None,
212        // i.e. no config is read.
213        None
214    }
215
216    /// Apply changes to the config after it's been loaded, e.g. overriding
217    /// values in a config file using command-line options.
218    fn process_config(&self, _config: RusticConfig) -> Result<RusticConfig, FrameworkError> {
219        // Note: The config that is "not read" is then read here in `process_config()` by the
220        // rustic logic and merged with the CLI options.
221        // That's why it says `_config`, because it's not read at all and therefore not needed.
222        let mut config = self.config.clone();
223
224        // collect "RUSTIC_REPO_OPT*" and "OPENDAL*" env variables.
225        // also add the standardized OTEL variables manually
226        // since clap does not support multiple variables for a single arg
227        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        // collect logs during merging as we start the logger *after* merging
257        let mut merge_logs = Vec::new();
258
259        // get global options from command line / env and config file
260        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        // start logger
269        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        // display logs from merging
317        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            // subcommands that don't need special overrides use a catch all
330            _ => 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}