diff --git a/Cargo.lock b/Cargo.lock index de8778d3..17028a72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1200,6 +1200,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive-getters" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74ef43543e701c01ad77d3a5922755c6a1d71b22d942cb8042be4994b380caff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "derive_destructure2" version = "0.1.3" @@ -2744,15 +2755,6 @@ dependencies = [ "libc", ] -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", -] - [[package]] name = "number_prefix" version = "0.4.0" @@ -3836,6 +3838,7 @@ dependencies = [ "assert_cmd", "bytesize", "cached", + "canonical-path", "chrono", "clap", "clap_complete", @@ -3845,6 +3848,7 @@ dependencies = [ "crossterm 0.28.1", "dateparser", "dav-server", + "derive-getters", "derive_more", "dialoguer", "dircmp", @@ -3859,7 +3863,6 @@ dependencies = [ "itertools", "jemallocator-global", "libc", - "log", "mimalloc", "open", "predicates", @@ -3870,7 +3873,7 @@ dependencies = [ "rhai", "rstest", "rustic_backend", - "rustic_core", + "rustic_core 0.5.5 (git+https://github.com/rustic-rs/rustic_core?branch=feat/tracing-logging)", "rustic_testing", "scopeguard", "self_update", @@ -3878,7 +3881,6 @@ dependencies = [ "serde", "serde_json", "serde_with", - "simplelog", "tar", "tempfile", "thiserror", @@ -3891,8 +3893,7 @@ dependencies = [ [[package]] name = "rustic_backend" version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "505fe0c120bb2ad90aa24192790c97a0a1fbed82b8d7c873e4fde589f5758478" +source = "git+https://github.com/rustic-rs/rustic_core?branch=feat/tracing-logging#350eed0a572f039d1715cc59c957e12b3196fe67" dependencies = [ "aho-corasick", "anyhow", @@ -3906,18 +3907,18 @@ dependencies = [ "hex", "humantime", "itertools", - "log", "opendal", "rand", "rayon", "reqwest 0.12.8", - "rustic_core", + "rustic_core 0.5.5 (git+https://github.com/rustic-rs/rustic_core?branch=feat/tracing-logging)", "semver", "serde", "strum", "strum_macros", "thiserror", "tokio", + "tracing", "typed-path", "url", "walkdir", @@ -3928,6 +3929,59 @@ name = "rustic_core" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc1d8eff26bc13c3b0adffc0506b087771fcb54af25ace3ac6d5914aca84d5d" +dependencies = [ + "aes256ctr_poly1305aes", + "anyhow", + "binrw", + "bytes", + "bytesize", + "cached", + "cachedir", + "chrono", + "crossbeam-channel", + "derivative", + "derive_more", + "derive_setters", + "dirs", + "displaydoc", + "dunce", + "enum-map", + "enum-map-derive", + "enumset", + "filetime", + "gethostname", + "hex", + "humantime", + "ignore", + "integer-sqrt", + "itertools", + "log", + "nix", + "pariter", + "path-dedot", + "quick_cache", + "rand", + "rayon", + "runtime-format", + "scrypt", + "serde", + "serde-aux", + "serde_derive", + "serde_json", + "serde_with", + "sha2", + "shell-words", + "strum", + "thiserror", + "walkdir", + "xattr", + "zstd", +] + +[[package]] +name = "rustic_core" +version = "0.5.5" +source = "git+https://github.com/rustic-rs/rustic_core?branch=feat/tracing-logging#350eed0a572f039d1715cc59c957e12b3196fe67" dependencies = [ "aes256ctr_poly1305aes", "anyhow", @@ -3958,7 +4012,6 @@ dependencies = [ "ignore", "integer-sqrt", "itertools", - "log", "nix", "pariter", "path-dedot", @@ -3976,6 +4029,7 @@ dependencies = [ "shell-words", "strum", "thiserror", + "tracing", "walkdir", "xattr", "zstd", @@ -3991,7 +4045,7 @@ dependencies = [ "anyhow", "bytes", "enum-map", - "rustic_core", + "rustic_core 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", "tempfile", ] @@ -4451,17 +4505,6 @@ dependencies = [ "time", ] -[[package]] -name = "simplelog" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" -dependencies = [ - "log", - "termcolor", - "time", -] - [[package]] name = "slab" version = "0.4.9" @@ -4783,9 +4826,7 @@ checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", - "libc", "num-conv", - "num_threads", "powerfmt", "serde", "time-core", diff --git a/Cargo.toml b/Cargo.toml index 532c91f1..8f7f1388 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,8 +46,8 @@ rustdoc-args = ["--document-private-items", "--generate-link-to-definition"] [dependencies] abscissa_core = { version = "0.8.1", default-features = false, features = ["application"] } -rustic_backend = { version = "0.4.2", features = ["cli"] } -rustic_core = { version = "0.5.5", features = ["cli"] } +rustic_backend = { git = "https://github.com/rustic-rs/rustic_core", package = "rustic_backend", branch = "feat/tracing-logging", version = "0.4.2", features = ["cli"] } +rustic_core = { git = "https://github.com/rustic-rs/rustic_core", package = "rustic_core", branch = "feat/tracing-logging", version = "0.5.5", features = ["cli"] } # allocators jemallocator-global = { version = "0.3.2", optional = true } @@ -63,9 +63,6 @@ crossterm = { version = "0.28", optional = true } ratatui = { version = "0.29.0", optional = true } tui-textarea = { version = "0.7.0", optional = true } -# logging -log = "0.4" - # errors anyhow = "1" displaydoc = "0.2.5" @@ -83,16 +80,17 @@ comfy-table = "7" rhai = { version = "1", features = ["sync", "serde", "no_optimize", "no_module", "no_custom_syntax", "only_i64"] } scopeguard = "1" semver = { version = "1", optional = true } -simplelog = "0.12" # commands bytesize = "1" cached = "0.53.1" +canonical-path = "2.0.2" clap = { version = "4", features = ["derive", "env", "wrap_help"] } clap_complete = "4" conflate = "0.2" convert_case = "0.6.0" dateparser = "0.2.1" +derive-getters = { version = "0.5.0", features = ["auto_copy_getters"] } derive_more = { version = "1", features = ["debug"] } dialoguer = "0.11.0" directories = "5" diff --git a/src/application.rs b/src/application.rs index 028783cc..4a5d0307 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,17 +1,28 @@ //! Rustic Abscissa Application -use std::{env, process}; +use std::{ + env, + path::{Path, PathBuf}, + process, +}; use abscissa_core::{ application::{self, fatal_error, AppCell}, config::{self, CfgCell}, + path::{AbsPath, AbsPathBuf, ExePath, RootPath, SecretsPath}, terminal::component::Terminal, - Application, Component, FrameworkError, FrameworkErrorKind, Shutdown, StandardPaths, + trace::{self, Tracing}, + tracing::debug, + Application, Component, Configurable, FrameworkError, FrameworkErrorKind, Shutdown, }; - use anyhow::Result; +use derive_getters::Getters; +use directories::ProjectDirs; // use crate::helpers::*; -use crate::{commands::EntryPoint, config::RusticConfig}; +use crate::{ + commands::EntryPoint, + config::{get_global_config_path, RusticConfig}, +}; /// Application state pub static RUSTIC_APP: AppCell = AppCell::new(); @@ -22,8 +33,10 @@ pub mod constants { pub const RUSTIC_DEV_DOCS_URL: &str = "https://rustic.cli.rs/dev-docs"; pub const RUSTIC_CONFIG_DOCS_URL: &str = "https://github.com/rustic-rs/rustic/blob/main/config/README.md"; + /// Name of the application's secrets directory + pub(crate) const SECRETS_DIR: &str = "secrets"; + pub(crate) const LOGS_DIR: &str = "logs"; } - /// Rustic Application #[derive(Debug)] pub struct RusticApp { @@ -34,6 +47,123 @@ pub struct RusticApp { state: application::State, } +#[derive(Clone, Debug, Getters)] +pub struct RusticPaths { + /// Path to the application's executable. + exe: AbsPathBuf, + + /// Path to the application's root directory + root: AbsPathBuf, + + /// Path to the application's secrets + secrets: AbsPathBuf, + + /// Path to the application's cache directory + cache: AbsPathBuf, + + /// Path to the application's log directory + logs: AbsPathBuf, + + /// Path to the application's configuration directories + configs: Vec, +} + +impl ExePath for RusticPaths { + fn exe(&self) -> &AbsPath { + self.exe.as_ref() + } +} + +impl RootPath for RusticPaths { + fn root(&self) -> &AbsPath { + self.root.as_ref() + } +} + +impl SecretsPath for RusticPaths { + fn secrets(&self) -> &AbsPath { + self.secrets.as_ref() + } +} + +impl RusticPaths { + fn from_project_dirs() -> Result { + let project_dirs = ProjectDirs::from("", "", "rustic").ok_or_else(|| { + FrameworkErrorKind::PathError { + name: Some("project_dirs".into()), + } + .context("failed to determine project directories") + })?; + + let data = project_dirs.data_dir(); + let root = data.parent().ok_or_else(|| { + FrameworkErrorKind::PathError { + name: Some(data.to_path_buf()), + } + .context("failed to determine parent directory") + })?; + let secrets = data.join(constants::SECRETS_DIR); + let logs = root.join(constants::LOGS_DIR); + let config = project_dirs.config_dir(); + let cache = project_dirs.cache_dir(); + + let global_config = get_global_config_path().ok_or_else(|| { + FrameworkErrorKind::PathError { + name: Some("global config path".into()), + } + .context("failed to determine global config paths") + })?; + + let tmp_config = PathBuf::from("."); + + let dirs = [ + root, + data, + secrets.as_path(), + logs.as_path(), + config, + global_config.as_path(), + cache, + ]; + + Self::create_dirs(&dirs)?; + + Ok(Self { + exe: canonical_path::current_exe()?, + root: canonical_path::CanonicalPathBuf::new(root.canonicalize()?)?, + secrets: canonical_path::CanonicalPathBuf::new(secrets.canonicalize()?)?, + logs: canonical_path::CanonicalPathBuf::new(logs.canonicalize()?)?, + cache: canonical_path::CanonicalPathBuf::new(cache.canonicalize()?)?, + configs: vec![ + canonical_path::CanonicalPathBuf::new(config.canonicalize()?)?, + canonical_path::CanonicalPathBuf::new(global_config.canonicalize()?)?, + canonical_path::CanonicalPathBuf::new(tmp_config.canonicalize()?)?, + ], + }) + } + + fn create_dirs(dirs: &[&Path]) -> Result<(), FrameworkError> { + for dir in dirs { + if !dir.exists() { + debug!("Creating directory: {}", dir.display()); + std::fs::create_dir_all(dir).map_err(|err| { + FrameworkErrorKind::PathError { + name: Some(dir.to_path_buf()), + } + .context(err) + })?; + } + } + Ok(()) + } +} + +impl Default for RusticPaths { + fn default() -> Self { + Self::from_project_dirs().expect("failed to populate default impl for RusticPaths") + } +} + /// Initialize a new application instance. /// /// By default no configuration is loaded, and the framework state is @@ -55,7 +185,7 @@ impl Application for RusticApp { type Cfg = RusticConfig; /// Paths to resources within the application. - type Paths = StandardPaths; + type Paths = RusticPaths; /// Accessor for application configuration. fn config(&self) -> config::Reader { @@ -67,15 +197,34 @@ impl Application for RusticApp { &self.state } - /// Returns the framework components used by this application. + /// Load this application's configuration and initialize its components. + fn init(&mut self, command: &Self::Cmd) -> Result<(), FrameworkError> { + // Create and register components with the application. + // We do this first to calculate a proper dependency ordering before + // application configuration is processed + self.register_components(command)?; + + // Load configuration + let config = command.config.clone(); + + // Fire callback regardless of whether any config was loaded to + // in order to signal state in the application lifecycle + self.after_config(command.process_config(config)?)?; + + Ok(()) + } + + /// Initialize the framework's default set of components, potentially + /// sourcing terminal and tracing options from command line arguments. fn framework_components( &mut self, command: &Self::Cmd, ) -> Result>>, FrameworkError> { - // we only ue the terminal component let terminal = Terminal::new(self.term_colors(command)); + let tracing = Tracing::new(self.tracing_config(command), self.term_colors(command)) + .expect("tracing subsystem failed to initialize"); - Ok(vec![Box::new(terminal)]) + Ok(vec![Box::new(terminal), Box::new(tracing)]) } /// Register all components used by this application. @@ -137,4 +286,20 @@ impl Application for RusticApp { process::exit(exit_code); } + + /// Get tracing configuration from command-line options + fn tracing_config(&self, command: &EntryPoint) -> trace::Config { + if command.verbose { + trace::Config::verbose() + } else { + command + .config + .global + .log_level + .as_ref() + .map_or_else(trace::Config::default, |level| { + trace::Config::from(level.to_owned()) + }) + } + } } diff --git a/src/commands.rs b/src/commands.rs index d991eb3f..8dee07c5 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -30,28 +30,26 @@ pub(crate) mod tui; 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::{ + application::RusticPaths, commands::{ backup::BackupCmd, cat::CatCmd, check::CheckCmd, completions::CompletionsCmd, config::ConfigCmd, copy::CopyCmd, diff::DiffCmd, docs::DocsCmd, 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, + find::FindCmd, 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::RusticConfig, - Application, RUSTIC_APP, }; use abscissa_core::{ - config::Override, terminal::ColorChoice, Command, Configurable, FrameworkError, - FrameworkErrorKind, Runnable, Shutdown, + config::Override, + tracing::log::{log, Level}, + Command, Configurable, FrameworkError, Runnable, }; use anyhow::Result; use clap::builder::{ @@ -60,10 +58,6 @@ use clap::builder::{ }; use convert_case::{Case, Casing}; use human_panic::setup_panic; -use log::{log, Level}; -use simplelog::{CombinedLogger, LevelFilter, TermLogger, TerminalMode, WriteLogger}; - -use self::find::FindCmd; /// Rustic Subcommands /// Subcommands need to be listed in an enum. @@ -164,6 +158,14 @@ pub struct EntryPoint { #[command(subcommand)] commands: RusticCmd, + + /// Enable verbose logging + #[arg(short, long)] + pub verbose: bool, + + /// Paths used by the application + #[clap(skip)] + pub paths: RusticPaths, } impl Runnable for EntryPoint { @@ -172,27 +174,11 @@ impl Runnable for EntryPoint { setup_panic!(); self.commands.run(); - RUSTIC_APP.shutdown(Shutdown::Graceful) } } -/// This trait allows you to define how application configuration is loaded. -impl Configurable for EntryPoint { - /// Location of the configuration file - fn config_path(&self) -> Option { - // Actually abscissa itself reads a config from `config_path`, but I have now returned None, - // i.e. no config is read. - None - } - - /// Apply changes to the config after it's been loaded, e.g. overriding - /// values in a config file using command-line options. - fn process_config(&self, _config: RusticConfig) -> Result { - // Note: The config that is "not read" is then read here in `process_config()` by the - // rustic logic and merged with the CLI options. - // That's why it says `_config`, because it's not read at all and therefore not needed. - let mut config = self.config.clone(); - +impl Override for EntryPoint { + fn override_config(&self, mut config: RusticConfig) -> Result { // collect "RUSTIC_REPO_OPT*" and "OPENDAL_*" env variables for (var, value) in std::env::vars() { if let Some(var) = var.strip_prefix("RUSTIC_REPO_OPT_") { @@ -213,58 +199,14 @@ impl Configurable for EntryPoint { // collect logs during merging as we start the logger *after* merging let mut merge_logs = Vec::new(); + let config_paths = self.paths.configs(); + // get global options from command line / env and config file if config.global.use_profiles.is_empty() { - config.merge_profile("rustic", &mut merge_logs, Level::Info)?; + config.merge_profile("rustic", &mut merge_logs, Level::Info, config_paths)?; } else { for profile in &config.global.use_profiles.clone() { - config.merge_profile(profile, &mut merge_logs, Level::Warn)?; - } - } - - // start logger - 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))?; + config.merge_profile(profile, &mut merge_logs, Level::Warn, config_paths)?; } } @@ -285,6 +227,15 @@ impl Configurable for EntryPoint { } } +/// This trait allows you to define how application configuration is loaded. +impl Configurable for EntryPoint { + /// Apply changes to the config after it's been loaded, e.g. overriding + /// values in a config file using command-line options. + fn process_config(&self, config: RusticConfig) -> Result { + self.override_config(config) + } +} + #[cfg(test)] mod tests { use crate::commands::EntryPoint; diff --git a/src/commands/backup.rs b/src/commands/backup.rs index 31dca322..337bc52d 100644 --- a/src/commands/backup.rs +++ b/src/commands/backup.rs @@ -10,12 +10,14 @@ use crate::{ status_err, Application, RUSTIC_APP, }; -use abscissa_core::{Command, Runnable, Shutdown}; +use abscissa_core::{ + tracing::{debug, error, info, warn}, + Command, Runnable, Shutdown, +}; use anyhow::{anyhow, bail, Context, Result}; use clap::ValueHint; use comfy_table::Cell; use conflate::Merge; -use log::{debug, error, info, warn}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; diff --git a/src/commands/copy.rs b/src/commands/copy.rs index 8cc728ca..8b814555 100644 --- a/src/commands/copy.rs +++ b/src/commands/copy.rs @@ -6,10 +6,16 @@ use crate::{ repository::{CliIndexedRepo, CliRepo}, status_err, Application, RusticConfig, RUSTIC_APP, }; -use abscissa_core::{config::Override, Command, FrameworkError, Runnable, Shutdown}; +use abscissa_core::{ + config::Override, + tracing::{ + error, info, + log::{log, Level}, + }, + Command, FrameworkError, Runnable, Shutdown, +}; use anyhow::{bail, Result}; use conflate::Merge; -use log::{error, info, log, Level}; use serde::{Deserialize, Serialize}; use rustic_core::{repofile::SnapshotFile, CopySnapshot, Id, KeyOptions}; @@ -82,7 +88,12 @@ impl CopyCmd { for target in &config.copy.targets { let mut merge_logs = Vec::new(); let mut target_config = RusticConfig::default(); - target_config.merge_profile(target, &mut merge_logs, Level::Error)?; + target_config.merge_profile( + target, + &mut merge_logs, + Level::Error, + RUSTIC_APP.state().paths().configs(), + )?; // display logs from merging for (level, merge_log) in merge_logs { log!(level, "{}", merge_log); diff --git a/src/commands/diff.rs b/src/commands/diff.rs index 5333ba9c..5ed8748f 100644 --- a/src/commands/diff.rs +++ b/src/commands/diff.rs @@ -2,9 +2,8 @@ use crate::{repository::CliIndexedRepo, status_err, Application, RUSTIC_APP}; -use abscissa_core::{Command, Runnable, Shutdown}; +use abscissa_core::{tracing::debug, Command, Runnable, Shutdown}; use clap::ValueHint; -use log::debug; use std::{ fmt::Display, diff --git a/src/commands/dump.rs b/src/commands/dump.rs index 9476f491..7784ae58 100644 --- a/src/commands/dump.rs +++ b/src/commands/dump.rs @@ -4,9 +4,8 @@ use std::io::{Read, Write}; use crate::{repository::CliIndexedRepo, status_err, Application, RUSTIC_APP}; -use abscissa_core::{Command, Runnable, Shutdown}; +use abscissa_core::{tracing::warn, Command, Runnable, Shutdown}; use anyhow::Result; -use log::warn; use rustic_core::{ repofile::{Node, NodeType}, vfs::OpenFile, diff --git a/src/commands/key.rs b/src/commands/key.rs index 9670a378..b184efec 100644 --- a/src/commands/key.rs +++ b/src/commands/key.rs @@ -4,10 +4,9 @@ use crate::{repository::CliOpenRepo, status_err, Application, RUSTIC_APP}; use std::path::PathBuf; -use abscissa_core::{Command, Runnable, Shutdown}; +use abscissa_core::{tracing::info, Command, Runnable, Shutdown}; use anyhow::Result; use dialoguer::Password; -use log::info; use rustic_core::{CommandInput, KeyOptions, RepositoryOptions}; diff --git a/src/commands/merge.rs b/src/commands/merge.rs index bb1525e0..74b6e440 100644 --- a/src/commands/merge.rs +++ b/src/commands/merge.rs @@ -1,9 +1,8 @@ //! `merge` subcommand use crate::{repository::CliOpenRepo, status_err, Application, RUSTIC_APP}; -use abscissa_core::{Command, Runnable, Shutdown}; +use abscissa_core::{tracing::info, Command, Runnable, Shutdown}; use anyhow::Result; -use log::info; use chrono::Local; diff --git a/src/commands/prune.rs b/src/commands/prune.rs index 5fcc6f7b..de336e4a 100644 --- a/src/commands/prune.rs +++ b/src/commands/prune.rs @@ -3,8 +3,7 @@ use crate::{ helpers::bytes_size_to_string, repository::CliOpenRepo, status_err, Application, RUSTIC_APP, }; -use abscissa_core::{Command, Runnable, Shutdown}; -use log::debug; +use abscissa_core::{tracing::debug, Command, Runnable, Shutdown}; use anyhow::Result; diff --git a/src/commands/restore.rs b/src/commands/restore.rs index b651a24a..56b65f65 100644 --- a/src/commands/restore.rs +++ b/src/commands/restore.rs @@ -4,9 +4,8 @@ use crate::{ helpers::bytes_size_to_string, repository::CliIndexedRepo, status_err, Application, RUSTIC_APP, }; -use abscissa_core::{Command, Runnable, Shutdown}; +use abscissa_core::{tracing::info, Command, Runnable, Shutdown}; use anyhow::Result; -use log::info; use rustic_core::{LocalDestination, LsOptions, RestoreOptions}; diff --git a/src/config.rs b/src/config.rs index 9672ea62..f3597f7e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,13 +10,12 @@ pub(crate) mod progress_options; use std::fmt::Debug; use std::{collections::HashMap, path::PathBuf}; -use abscissa_core::{config::Config, path::AbsPathBuf, FrameworkError}; +use abscissa_core::{config::Config, path::AbsPathBuf, tracing::log::Level, FrameworkError}; use anyhow::Result; +use canonical_path::CanonicalPathBuf; use clap::{Parser, ValueHint}; use conflate::Merge; -use directories::ProjectDirs; use itertools::Itertools; -use log::Level; use serde::{Deserialize, Serialize}; #[cfg(feature = "webdav")] @@ -82,20 +81,31 @@ impl RusticConfig { profile: &str, merge_logs: &mut Vec<(Level, String)>, level_missing: Level, + paths: &[CanonicalPathBuf], ) -> Result<(), FrameworkError> { - let profile_filename = profile.to_string() + ".toml"; - let paths = get_config_paths(&profile_filename); - - if let Some(path) = paths.iter().find(|path| path.exists()) { + let paths_with_filenames: Vec = paths + .iter() + .map(|path| { + let mut path = (*path).clone().into_path_buf(); + path.push(profile.to_string() + ".toml"); + path + }) + .collect(); + + if let Some(path) = paths_with_filenames.iter().find(|path| path.exists()) { merge_logs.push((Level::Info, format!("using config {}", path.display()))); + let mut config = Self::load_toml_file(AbsPathBuf::canonicalize(path)?)?; // if "use_profile" is defined in config file, merge the referenced profiles first for profile in &config.global.use_profiles.clone() { - config.merge_profile(profile, merge_logs, Level::Warn)?; + config.merge_profile(profile, merge_logs, Level::Warn, paths)?; } self.merge(config); } else { - let paths_string = paths.iter().map(|path| path.display()).join(", "); + let paths_string = paths_with_filenames + .iter() + .map(|path| path.display()) + .join(", "); merge_logs.push(( level_missing, format!( @@ -165,32 +175,6 @@ pub struct GlobalOptions { pub env: HashMap, } -/// Get the paths to the config file -/// -/// # Arguments -/// -/// * `filename` - name of the config file -/// -/// # Returns -/// -/// A vector of [`PathBuf`]s to the config files -fn get_config_paths(filename: &str) -> Vec { - [ - ProjectDirs::from("", "", "rustic") - .map(|project_dirs| project_dirs.config_dir().to_path_buf()), - get_global_config_path(), - Some(PathBuf::from(".")), - ] - .into_iter() - .filter_map(|path| { - path.map(|mut p| { - p.push(filename); - p - }) - }) - .collect() -} - /// Get the path to the global config directory on Windows. /// /// # Returns @@ -198,7 +182,7 @@ fn get_config_paths(filename: &str) -> Vec { /// The path to the global config directory on Windows. /// If the environment variable `PROGRAMDATA` is not set, `None` is returned. #[cfg(target_os = "windows")] -fn get_global_config_path() -> Option { +pub(crate) fn get_global_config_path() -> Option { std::env::var_os("PROGRAMDATA").map(|program_data| { let mut path = PathBuf::from(program_data); path.push(r"rustic\config"); @@ -212,7 +196,7 @@ fn get_global_config_path() -> Option { /// /// `None` is returned. #[cfg(any(target_os = "ios", target_arch = "wasm32"))] -fn get_global_config_path() -> Option { +pub(crate) fn get_global_config_path() -> Option { None } @@ -223,6 +207,6 @@ fn get_global_config_path() -> Option { /// /// "/etc/rustic" is returned. #[cfg(not(any(target_os = "windows", target_os = "ios", target_arch = "wasm32")))] -fn get_global_config_path() -> Option { +pub(crate) fn get_global_config_path() -> Option { Some(PathBuf::from("/etc/rustic")) } diff --git a/src/filtering.rs b/src/filtering.rs index 93327c0e..18bc4623 100644 --- a/src/filtering.rs +++ b/src/filtering.rs @@ -1,8 +1,8 @@ use crate::error::RhaiErrorKinds; +use abscissa_core::tracing::warn; use bytesize::ByteSize; use derive_more::derive::Display; -use log::warn; use rustic_core::{repofile::SnapshotFile, StringList}; use std::{ error::Error, diff --git a/tests/backup_restore.rs b/tests/backup_restore.rs index 0beaf5b6..762af054 100644 --- a/tests/backup_restore.rs +++ b/tests/backup_restore.rs @@ -37,8 +37,8 @@ fn setup() -> TestResult { .args(["init"]) .assert() .success() - .stderr(predicate::str::contains("successfully created.")) - .stderr(predicate::str::contains("successfully added.")); + .stdout(predicate::str::contains("successfully created.")) + .stdout(predicate::str::contains("successfully added.")); Ok(temp_dir) } @@ -93,8 +93,8 @@ fn test_backup_and_check_passes() -> TestResult<()> { .args(["check", "--read-data"]) .assert() .success() - .stderr(predicate::str::contains("WARN").not()) - .stderr(predicate::str::contains("ERROR").not()); + .stdout(predicate::str::contains("WARN").not()) + .stdout(predicate::str::contains("ERROR").not()); } Ok(()) diff --git a/tests/config.rs b/tests/config.rs index 1741c71e..9f2fc85c 100644 --- a/tests/config.rs +++ b/tests/config.rs @@ -15,3 +15,8 @@ fn test_parse_rustic_configs_is_ok( Ok(()) } + +#[test] +fn test_debug_config_passes() { + insta::assert_debug_snapshot!(RusticConfig::default()); +} diff --git a/tests/hooks.rs b/tests/hooks.rs index 897f32c1..52ec8330 100644 --- a/tests/hooks.rs +++ b/tests/hooks.rs @@ -82,8 +82,8 @@ fn setup(with_backup: BackupAction) -> TestResult { .args(["init"]) .assert() .success() - .stderr(predicate::str::contains("successfully created.")) - .stderr(predicate::str::contains("successfully added.")); + .stdout(predicate::str::contains("successfully created.")) + .stdout(predicate::str::contains("successfully added.")); match with_backup { BackupAction::WithBackup => { diff --git a/tests/show-config.rs b/tests/show-config.rs index 582228e3..0e43ad12 100644 --- a/tests/show-config.rs +++ b/tests/show-config.rs @@ -10,15 +10,11 @@ // unused_qualifications // )] -use std::{ - io::{Read, Write}, - path::PathBuf, - sync::LazyLock, -}; +use std::{io::Read, sync::LazyLock}; use abscissa_core::testing::prelude::*; -use rustic_testing::{files_differ, get_temp_file, TestResult}; +use rustic_testing::TestResult; // Storing this value as a [`Lazy`] static ensures that all instances of // the runner acquire a mutex when executing commands and inspecting @@ -34,32 +30,22 @@ fn cmd_runner() -> CmdRunner { LAZY_RUNNER.clone() } -fn fixture() -> PathBuf { - ["tests", "show-config-fixtures", "empty.txt"] - .iter() - .collect() -} - #[test] fn test_show_config_passes() -> TestResult<()> { - let fixture_file = fixture(); - let mut file = get_temp_file()?; + let mut output = String::new(); { - let file = file.as_file_mut(); - let mut runner = cmd_runner(); - let mut cmd = runner.args(["show-config"]).run(); - - let mut output = String::new(); - cmd.stdout().read_to_string(&mut output)?; - file.write_all(output.as_bytes())?; - file.sync_all()?; - cmd.wait()?.expect_success(); + _ = cmd_runner() + .args(["show-config"]) + .run() + .stdout() + .read_to_string(&mut output)?; } - if files_differ(fixture_file, file.path())? { - panic!("generated empty.txt differs, breaking change!"); - } + // remove the first three lines of the output + output = output.lines().skip(3).collect::>().join("\n"); + + insta::assert_snapshot!(output); Ok(()) } diff --git a/tests/snapshots/config__debug_config_passes.snap b/tests/snapshots/config__debug_config_passes.snap new file mode 100644 index 00000000..96fcabf0 --- /dev/null +++ b/tests/snapshots/config__debug_config_passes.snap @@ -0,0 +1,194 @@ +--- +source: tests/config.rs +expression: "RusticConfig::default()" +--- +RusticConfig { + global: GlobalOptions { + use_profiles: [], + dry_run: false, + check_index: false, + log_level: None, + log_file: None, + progress_options: ProgressOptions { + no_progress: false, + progress_interval: None, + }, + hooks: Hooks { + run_before: [], + run_after: [], + run_failed: [], + run_finally: [], + context: "", + }, + env: {}, + }, + repository: AllRepositoryOptions { + be: BackendOptions { + repository: None, + repo_hot: None, + options: {}, + options_hot: {}, + options_cold: {}, + }, + repo: RepositoryOptions { + password: None, + password_file: None, + password_command: None, + no_cache: false, + cache_dir: None, + warm_up: false, + warm_up_command: None, + warm_up_wait: None, + }, + hooks: Hooks { + run_before: [], + run_after: [], + run_failed: [], + run_finally: [], + context: "", + }, + }, + snapshot_filter: SnapshotFilter { + filter_hosts: [], + filter_labels: [], + filter_paths: [], + filter_paths_exact: [], + filter_tags: [], + filter_tags_exact: [], + filter_after: None, + filter_before: None, + filter_size: None, + filter_size_added: None, + filter_fn: None, + }, + backup: BackupCmd { + cli_sources: [], + stdin_filename: "", + stdin_command: None, + as_path: None, + ignore_save_opts: LocalSourceSaveOptions { + with_atime: false, + ignore_devid: false, + }, + no_scan: false, + json: false, + long: false, + quiet: false, + init: false, + parent_opts: ParentOptions { + group_by: None, + parent: None, + skip_identical_parent: false, + force: false, + ignore_ctime: false, + ignore_inode: false, + }, + ignore_filter_opts: LocalSourceFilterOptions { + globs: [], + iglobs: [], + glob_files: [], + iglob_files: [], + git_ignore: false, + no_require_git: false, + custom_ignorefiles: [], + exclude_if_present: [], + one_file_system: false, + exclude_larger_than: None, + }, + snap_opts: SnapshotOptions { + label: None, + tags: [], + description: None, + description_from: None, + time: None, + delete_never: false, + delete_after: None, + host: None, + command: None, + }, + key_opts: KeyOptions { + hostname: None, + username: None, + with_created: false, + }, + config_opts: ConfigOptions { + set_compression: None, + set_version: None, + set_append_only: None, + set_treepack_size: None, + set_treepack_size_limit: None, + set_treepack_growfactor: None, + set_datapack_size: None, + set_datapack_growfactor: None, + set_datapack_size_limit: None, + set_min_packsize_tolerate_percent: None, + set_max_packsize_tolerate_percent: None, + set_extra_verify: None, + }, + hooks: Hooks { + run_before: [], + run_after: [], + run_failed: [], + run_finally: [], + context: "", + }, + snapshots: [], + sources: [], + }, + copy: CopyCmd { + ids: [], + init: false, + targets: [], + key_opts: KeyOptions { + hostname: None, + username: None, + with_created: false, + }, + }, + forget: ForgetOptions { + group_by: None, + prune: false, + filter: SnapshotFilter { + filter_hosts: [], + filter_labels: [], + filter_paths: [], + filter_paths_exact: [], + filter_tags: [], + filter_tags_exact: [], + filter_after: None, + filter_before: None, + filter_size: None, + filter_size_added: None, + filter_fn: None, + }, + keep: KeepOptions { + keep_tags: [], + keep_ids: [], + keep_last: None, + keep_hourly: None, + keep_daily: None, + keep_weekly: None, + keep_monthly: None, + keep_quarter_yearly: None, + keep_half_yearly: None, + keep_yearly: None, + keep_within: None, + keep_within_hourly: None, + keep_within_daily: None, + keep_within_weekly: None, + keep_within_monthly: None, + keep_within_quarter_yearly: None, + keep_within_half_yearly: None, + keep_within_yearly: None, + keep_none: false, + }, + }, + webdav: WebDavCmd { + address: None, + path_template: None, + time_template: None, + symlinks: false, + file_access: None, + snapshot_path: None, + }, +} diff --git a/tests/show-config-fixtures/empty.txt b/tests/snapshots/show_config__show_config_passes.snap similarity index 95% rename from tests/show-config-fixtures/empty.txt rename to tests/snapshots/show_config__show_config_passes.snap index c7ede0be..1233b6d4 100644 --- a/tests/show-config-fixtures/empty.txt +++ b/tests/snapshots/show_config__show_config_passes.snap @@ -1,3 +1,7 @@ +--- +source: tests/show-config.rs +expression: output +--- [global] use-profiles = [] dry-run = false @@ -83,4 +87,3 @@ filter-tags-exact = [] [webdav] symlinks = false -