Skip to main content

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 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;
32pub(crate) mod version;
33#[cfg(feature = "webdav")]
34pub(crate) mod webdav;
35
36use std::fmt::Debug;
37use std::path::PathBuf;
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        rewrite::RewriteCmd, self_update::SelfUpdateCmd, show_config::ShowConfigCmd,
52        snapshots::SnapshotCmd, tag::TagCmd,
53    },
54    config::RusticConfig,
55};
56
57use abscissa_core::{
58    Command, Configurable, FrameworkError, FrameworkErrorKind, Runnable, Shutdown, config::Override,
59};
60use anyhow::Result;
61use clap::builder::{
62    Styles,
63    styling::{AnsiColor, Effects},
64};
65use convert_case::{Case, Casing};
66use human_panic::setup_panic;
67use log::{Level, info, log};
68use reqwest::Url;
69
70use self::find::FindCmd;
71
72/// Rustic Subcommands
73/// Subcommands need to be listed in an enum.
74#[derive(clap::Parser, Command, Debug, Runnable)]
75enum RusticCmd {
76    /// Backup to the repository
77    Backup(Box<BackupCmd>),
78
79    /// Show raw data of files and blobs in a repository
80    Cat(Box<CatCmd>),
81
82    /// Change the repository configuration
83    Config(Box<ConfigCmd>),
84
85    /// Generate shell completions
86    Completions(Box<CompletionsCmd>),
87
88    /// Check the repository
89    Check(Box<CheckCmd>),
90
91    /// Copy snapshots to other repositories
92    Copy(Box<CopyCmd>),
93
94    /// Compare two snapshots or paths
95    Diff(Box<DiffCmd>),
96
97    /// Open the documentation
98    Docs(Box<DocsCmd>),
99
100    /// Dump the contents of a file within a snapshot to stdout
101    Dump(Box<DumpCmd>),
102
103    /// Find patterns in given snapshots
104    Find(Box<FindCmd>),
105
106    /// Remove snapshots from the repository
107    Forget(Box<ForgetCmd>),
108
109    /// Initialize a new repository
110    Init(Box<InitCmd>),
111
112    /// Manage keys for a repository
113    Key(Box<KeyCmd>),
114
115    /// List repository files by file type
116    List(Box<ListCmd>),
117
118    #[cfg(feature = "mount")]
119    /// Mount a repository as read-only filesystem
120    Mount(Box<MountCmd>),
121
122    /// List file contents of a snapshot
123    Ls(Box<LsCmd>),
124
125    /// Merge snapshots
126    Merge(Box<MergeCmd>),
127
128    /// Show a detailed overview of the snapshots within the repository
129    Snapshots(Box<SnapshotCmd>),
130
131    /// Show the configuration which has been read from the config file(s)
132    ShowConfig(Box<ShowConfigCmd>),
133
134    /// Update to the latest stable rustic release
135    #[cfg_attr(not(feature = "self-update"), clap(hide = true))]
136    SelfUpdate(Box<SelfUpdateCmd>),
137
138    /// Remove unused data or repack repository pack files
139    Prune(Box<PruneCmd>),
140
141    /// Restore (a path within) a snapshot
142    Restore(Box<RestoreCmd>),
143
144    /// Rewrite existing snapshot(s)
145    Rewrite(Box<RewriteCmd>),
146
147    /// Repair a snapshot or the repository index
148    Repair(Box<RepairCmd>),
149
150    /// Show general information about the repository
151    Repoinfo(Box<RepoInfoCmd>),
152
153    /// Change tags of snapshots
154    Tag(Box<TagCmd>),
155
156    /// Start a webdav server which allows to access the repository
157    #[cfg(feature = "webdav")]
158    Webdav(Box<WebDavCmd>),
159
160    /// Print version information
161    Version(Box<version::VersionCmd>),
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
176pub(crate) fn program_version() -> String {
177    format!("rustic {}", version())
178}
179
180/// Entry point for the application. It needs to be a struct to allow using subcommands!
181#[derive(clap::Parser, Command, Debug)]
182#[command(author, about, name="rustic", styles=styles(), version=version())]
183pub struct EntryPoint {
184    #[command(flatten)]
185    pub config: RusticConfig,
186
187    #[command(subcommand)]
188    commands: RusticCmd,
189}
190
191impl Runnable for EntryPoint {
192    fn run(&self) {
193        // Set up panic hook for better error messages and logs
194        setup_panic!();
195
196        // Set up Ctrl-C handler
197        let (tx, rx) = channel();
198
199        ctrlc::set_handler(move || tx.send(()).expect("Could not send signal on channel."))
200            .expect("Error setting Ctrl-C handler");
201
202        _ = std::thread::spawn(move || {
203            // Wait for Ctrl-C
204            rx.recv().expect("Could not receive from channel.");
205            info!("Ctrl-C received, shutting down...");
206            RUSTIC_APP.shutdown(Shutdown::Graceful)
207        });
208
209        // Run the subcommand
210        self.commands.run();
211        RUSTIC_APP.shutdown(Shutdown::Graceful)
212    }
213}
214
215/// This trait allows you to define how application configuration is loaded.
216impl Configurable<RusticConfig> for EntryPoint {
217    /// Location of the configuration file
218    fn config_path(&self) -> Option<PathBuf> {
219        // Actually abscissa itself reads a config from `config_path`, but I have now returned None,
220        // i.e. no config is read.
221        None
222    }
223
224    /// Apply changes to the config after it's been loaded, e.g. overriding
225    /// values in a config file using command-line options.
226    fn process_config(&self, _config: RusticConfig) -> Result<RusticConfig, FrameworkError> {
227        // Note: The config that is "not read" is then read here in `process_config()` by the
228        // rustic logic and merged with the CLI options.
229        // That's why it says `_config`, because it's not read at all and therefore not needed.
230        let mut config = self.config.clone();
231
232        // Completion generation only needs the command definition. In particular, it must not
233        // try to read a profile which may be inaccessible to the user generating completions.
234        if matches!(self.commands, RusticCmd::Completions(_)) {
235            return Ok(config);
236        }
237
238        // collect "RUSTIC_REPO_OPT*" and "OPENDAL*" env variables.
239        // also add the standardized OTEL variables manually
240        // since clap does not support multiple variables for a single arg
241        for (var, value) in std::env::vars() {
242            if let Some(var) = var.strip_prefix("RUSTIC_REPO_OPT_") {
243                let var = var.from_case(Case::UpperSnake).to_case(Case::Kebab);
244                _ = config.repository.be.options.insert(var, value);
245            } else if let Some(var) = var.strip_prefix("OPENDAL_") {
246                let var = var.from_case(Case::UpperSnake).to_case(Case::Snake);
247                _ = config.repository.be.options.insert(var, value);
248            } else if let Some(var) = var.strip_prefix("RUSTIC_REPO_OPTHOT_") {
249                let var = var.from_case(Case::UpperSnake).to_case(Case::Kebab);
250                _ = config.repository.be.options_hot.insert(var, value);
251            } else if let Some(var) = var.strip_prefix("RUSTIC_REPO_OPTCOLD_") {
252                let var = var.from_case(Case::UpperSnake).to_case(Case::Kebab);
253                _ = config.repository.be.options_cold.insert(var, value);
254            } else if let Some(var) = var.strip_prefix("OPENDALHOT_") {
255                let var = var.from_case(Case::UpperSnake).to_case(Case::Snake);
256                _ = config.repository.be.options_hot.insert(var, value);
257            } else if let Some(var) = var.strip_prefix("OPENDALCOLD_") {
258                let var = var.from_case(Case::UpperSnake).to_case(Case::Snake);
259                _ = config.repository.be.options_cold.insert(var, value);
260            } else if var == "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT" {
261                #[cfg(feature = "opentelemetry")]
262                if let Ok(url) = Url::parse(&value) {
263                    _ = config.global.opentelemetry.insert(url);
264                }
265            } else if var == "OTEL_SERVICE_NAME" && cfg!(feature = "opentelemetry") {
266                _ = config.backup.metrics_job.insert(value);
267            }
268        }
269
270        // collect logs during merging as we start the logger *after* merging
271        let mut merge_logs = Vec::new();
272
273        // get global options from command line / env and config file
274        if config.global.use_profiles.is_empty() {
275            config.merge_profile("rustic", &mut merge_logs, Level::Info)?;
276        } else {
277            for profile in &config.global.use_profiles.clone() {
278                config.merge_profile(profile, &mut merge_logs, Level::Warn)?;
279            }
280        }
281
282        // start logger also check if version command was supplied by the user
283        // if so skip logging for version
284        if !matches!(self.commands, RusticCmd::Version(_)) {
285            config
286                .global
287                .logging_options
288                .start_logger(config.global.dry_run)
289                .map_err(|e| FrameworkErrorKind::ConfigError.context(e))?;
290
291            if config.global.logging_options.log_file.is_some() {
292                info!("rustic {}", version());
293                info!("command: {:?}", std::env::args_os().collect::<Vec<_>>());
294            }
295
296            // display logs from merging
297            for (level, merge_log) in merge_logs {
298                log!(level, "{merge_log}");
299            }
300        }
301
302        match &self.commands {
303            RusticCmd::Forget(cmd) => cmd.override_config(config),
304            RusticCmd::Copy(cmd) => cmd.override_config(config),
305            #[cfg(feature = "webdav")]
306            RusticCmd::Webdav(cmd) => cmd.override_config(config),
307            #[cfg(feature = "mount")]
308            RusticCmd::Mount(cmd) => cmd.override_config(config),
309
310            // subcommands that don't need special overrides use a catch all
311            _ => Ok(config),
312        }
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use crate::commands::EntryPoint;
319    use clap::CommandFactory;
320
321    #[test]
322    fn verify_cli() {
323        EntryPoint::command().debug_assert();
324    }
325}