pub(crate) mod backup;
pub(crate) mod cat;
pub(crate) mod check;
pub(crate) mod completions;
pub(crate) mod config;
pub(crate) mod copy;
pub(crate) mod diff;
pub(crate) mod dump;
pub(crate) mod find;
pub(crate) mod forget;
pub(crate) mod init;
pub(crate) mod key;
pub(crate) mod list;
pub(crate) mod ls;
pub(crate) mod merge;
pub(crate) mod prune;
pub(crate) mod repair;
pub(crate) mod repoinfo;
pub(crate) mod restore;
pub(crate) mod self_update;
pub(crate) mod show_config;
pub(crate) mod snapshots;
pub(crate) mod tag;
#[cfg(feature = "tui")]
pub(crate) mod tui;
#[cfg(feature = "webdav")]
pub(crate) mod webdav;
use std::fmt::Debug;
use std::fs::File;
use std::path::PathBuf;
use std::str::FromStr;
#[cfg(feature = "webdav")]
use crate::commands::webdav::WebDavCmd;
use crate::{
commands::{
backup::BackupCmd, cat::CatCmd, check::CheckCmd, completions::CompletionsCmd,
config::ConfigCmd, copy::CopyCmd, diff::DiffCmd, dump::DumpCmd, forget::ForgetCmd,
init::InitCmd, key::KeyCmd, list::ListCmd, ls::LsCmd, merge::MergeCmd, prune::PruneCmd,
repair::RepairCmd, repoinfo::RepoInfoCmd, restore::RestoreCmd, self_update::SelfUpdateCmd,
show_config::ShowConfigCmd, snapshots::SnapshotCmd, tag::TagCmd,
},
config::{progress_options::ProgressOptions, AllRepositoryOptions, RusticConfig},
{Application, RUSTIC_APP},
};
use abscissa_core::{
config::Override, terminal::ColorChoice, Command, Configurable, FrameworkError,
FrameworkErrorKind, Runnable, Shutdown,
};
use anyhow::{anyhow, Result};
use clap::builder::{
styling::{AnsiColor, Effects},
Styles,
};
use convert_case::{Case, Casing};
use dialoguer::Password;
use human_panic::setup_panic;
use log::{log, warn, Level};
use rustic_core::{IndexedFull, OpenStatus, ProgressBars, Repository};
use simplelog::{CombinedLogger, LevelFilter, TermLogger, TerminalMode, WriteLogger};
use self::find::FindCmd;
pub(super) mod constants {
pub(super) const MAX_PASSWORD_RETRIES: usize = 5;
}
#[derive(clap::Parser, Command, Debug, Runnable)]
enum RusticCmd {
Backup(BackupCmd),
Cat(CatCmd),
Config(ConfigCmd),
Completions(CompletionsCmd),
Check(CheckCmd),
Copy(CopyCmd),
Diff(DiffCmd),
Dump(DumpCmd),
Find(FindCmd),
Forget(ForgetCmd),
Init(InitCmd),
Key(KeyCmd),
List(ListCmd),
Ls(LsCmd),
Merge(MergeCmd),
Snapshots(SnapshotCmd),
ShowConfig(ShowConfigCmd),
#[cfg_attr(not(feature = "self-update"), clap(hide = true))]
SelfUpdate(SelfUpdateCmd),
Prune(PruneCmd),
Restore(RestoreCmd),
Repair(RepairCmd),
Repoinfo(RepoInfoCmd),
Tag(TagCmd),
#[cfg(feature = "webdav")]
Webdav(WebDavCmd),
}
fn styles() -> Styles {
Styles::styled()
.header(AnsiColor::Red.on_default() | Effects::BOLD)
.usage(AnsiColor::Red.on_default() | Effects::BOLD)
.literal(AnsiColor::Blue.on_default() | Effects::BOLD)
.placeholder(AnsiColor::Green.on_default())
}
#[derive(clap::Parser, Command, Debug)]
#[command(author, about, name="rustic", styles=styles(), version = option_env!("PROJECT_VERSION").unwrap_or(env!("CARGO_PKG_VERSION")))]
pub struct EntryPoint {
#[command(flatten)]
pub config: RusticConfig,
#[command(subcommand)]
commands: RusticCmd,
}
impl Runnable for EntryPoint {
fn run(&self) {
setup_panic!();
self.commands.run();
RUSTIC_APP.shutdown(Shutdown::Graceful)
}
}
impl Configurable<RusticConfig> for EntryPoint {
fn config_path(&self) -> Option<PathBuf> {
None
}
fn process_config(&self, _config: RusticConfig) -> Result<RusticConfig, FrameworkError> {
let mut config = self.config.clone();
for (var, value) in std::env::vars() {
if let Some(var) = var.strip_prefix("RUSTIC_REPO_OPT_") {
let var = var.from_case(Case::UpperSnake).to_case(Case::Kebab);
_ = config.repository.be.options.insert(var, value);
} else if let Some(var) = var.strip_prefix("OPENDAL_") {
let var = var.from_case(Case::UpperSnake).to_case(Case::Snake);
_ = config.repository.be.options.insert(var, value);
} else if let Some(var) = var.strip_prefix("RUSTIC_REPO_OPTHOT_") {
let var = var.from_case(Case::UpperSnake).to_case(Case::Kebab);
_ = config.repository.be.options_hot.insert(var, value);
} else if let Some(var) = var.strip_prefix("RUSTIC_REPO_OPTCOLD_") {
let var = var.from_case(Case::UpperSnake).to_case(Case::Kebab);
_ = config.repository.be.options_cold.insert(var, value);
}
}
let mut merge_logs = Vec::new();
if config.global.use_profile.is_empty() {
config.merge_profile("rustic", &mut merge_logs, Level::Info)?;
} else {
for profile in &config.global.use_profile.clone() {
config.merge_profile(profile, &mut merge_logs, Level::Warn)?;
}
}
let level_filter = match &config.global.log_level {
Some(level) => LevelFilter::from_str(level)
.map_err(|e| FrameworkErrorKind::ConfigError.context(e))?,
None => LevelFilter::Info,
};
let term_config = simplelog::ConfigBuilder::new()
.set_time_level(LevelFilter::Off)
.build();
match &config.global.log_file {
None => TermLogger::init(
level_filter,
term_config,
TerminalMode::Stderr,
ColorChoice::Auto,
)
.map_err(|e| FrameworkErrorKind::ConfigError.context(e))?,
Some(file) => {
let file_config = simplelog::ConfigBuilder::new()
.set_time_format_rfc3339()
.build();
let file = File::options()
.create(true)
.append(true)
.open(file)
.map_err(|e| {
FrameworkErrorKind::PathError {
name: Some(file.clone()),
}
.context(e)
})?;
let term_logger = TermLogger::new(
level_filter.min(LevelFilter::Warn),
term_config,
TerminalMode::Stderr,
ColorChoice::Auto,
);
CombinedLogger::init(vec![
term_logger,
WriteLogger::new(level_filter, file_config, file),
])
.map_err(|e| FrameworkErrorKind::ConfigError.context(e))?;
}
}
for (level, merge_log) in merge_logs {
log!(level, "{}", merge_log);
}
match &self.commands {
RusticCmd::Forget(cmd) => cmd.override_config(config),
#[cfg(feature = "webdav")]
RusticCmd::Webdav(cmd) => cmd.override_config(config),
_ => Ok(config),
}
}
}
fn get_repository_with_progress<P>(
repo_opts: &AllRepositoryOptions,
po: P,
) -> Result<Repository<P, ()>> {
let backends = repo_opts.be.to_backends()?;
let repo = Repository::new_with_progress(&repo_opts.repo, &backends, po)?;
Ok(repo)
}
fn get_repository(repo_opts: &AllRepositoryOptions) -> Result<Repository<ProgressOptions, ()>> {
let po = RUSTIC_APP.config().global.progress_options;
get_repository_with_progress(repo_opts, po)
}
fn open_repository_with_progress<P: Clone>(
repo_opts: &AllRepositoryOptions,
po: P,
) -> Result<Repository<P, OpenStatus>> {
if RUSTIC_APP.config().global.check_index {
warn!("Option check-index is not supported and will be ignored!");
}
let repo = get_repository_with_progress(repo_opts, po)?;
match repo.password()? {
Some(pass) => {
return Ok(repo.open_with_password(&pass)?);
}
None => {
for _ in 0..constants::MAX_PASSWORD_RETRIES {
let pass = Password::new()
.with_prompt("enter repository password")
.allow_empty_password(true)
.interact()?;
match repo.clone().open_with_password(&pass) {
Ok(repo) => return Ok(repo),
Err(err) if err.is_incorrect_password() => continue,
Err(err) => return Err(err.into()),
}
}
}
}
Err(anyhow!("incorrect password"))
}
fn open_repository(
repo_opts: &AllRepositoryOptions,
) -> Result<Repository<ProgressOptions, OpenStatus>> {
let po = RUSTIC_APP.config().global.progress_options;
open_repository_with_progress(repo_opts, po)
}
fn open_repository_indexed_with_progress<P: Clone + ProgressBars>(
repo_opts: &AllRepositoryOptions,
po: P,
) -> Result<Repository<P, impl IndexedFull + Debug>> {
let open = open_repository_with_progress(repo_opts, po)?;
let check_index = RUSTIC_APP.config().global.check_index;
let repo = if check_index {
open.to_indexed_checked()
} else {
open.to_indexed()
}?;
Ok(repo)
}
fn open_repository_indexed(
repo_opts: &AllRepositoryOptions,
) -> Result<Repository<ProgressOptions, impl IndexedFull + Debug>> {
let po = RUSTIC_APP.config().global.progress_options;
open_repository_indexed_with_progress(repo_opts, po)
}
#[cfg(test)]
mod tests {
use crate::commands::EntryPoint;
use clap::CommandFactory;
#[test]
fn verify_cli() {
EntryPoint::command().debug_assert();
}
}