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