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