Merge pull request #727 from rustic-rs/refactor-key-config-init

Refactor key, config and init command
This commit is contained in:
aawsome 2023-07-07 20:21:21 +02:00 committed by GitHub
commit abc42a1994
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 605 additions and 452 deletions

1
Cargo.lock generated
View File

@ -2413,7 +2413,6 @@ dependencies = [
"crossbeam-channel",
"derivative",
"derive_more",
"dialoguer",
"directories",
"dirs",
"displaydoc",

View File

@ -94,7 +94,6 @@ clap = { workspace = true, optional = true }
clap_complete = { workspace = true, optional = true }
merge = { workspace = true, optional = true }
dialoguer = "0.10.4"
directories = { workspace = true }
nom = { workspace = true }
path-dedot = { workspace = true }

View File

@ -0,0 +1,24 @@
//! `config` example
use rustic_core::{ConfigOpts, Repository, RepositoryOptions};
use simplelog::{Config, LevelFilter, SimpleLogger};
fn main() {
// Display info logs
let _ = SimpleLogger::init(LevelFilter::Info, Config::default());
// Open repository
let repo_opts = RepositoryOptions {
repository: Some("/tmp/repo".to_string()),
password: Some("test".to_string()),
..Default::default()
};
let repo = Repository::new(&repo_opts).unwrap().open().unwrap();
// Set Config, e.g. Compression level
let config_opts = ConfigOpts {
set_compression: Some(22),
..Default::default()
};
repo.apply_config(&config_opts).unwrap();
}

View File

@ -0,0 +1,23 @@
//! `init` example
use rustic_core::{ConfigOpts, KeyOpts, Repository, RepositoryOptions};
use simplelog::{Config, LevelFilter, SimpleLogger};
fn main() {
// Display info logs
let _ = SimpleLogger::init(LevelFilter::Info, Config::default());
// Init repository
let repo_opts = RepositoryOptions {
repository: Some("/tmp/repo".to_string()),
password: Some("test".to_string()),
..Default::default()
};
let key_opts = KeyOpts::default();
let config_opts = ConfigOpts::default();
let _repo = Repository::new(&repo_opts)
.unwrap()
.init(&key_opts, &config_opts)
.unwrap();
// -> use _repo for any operation on an open repository
}

View File

@ -0,0 +1,21 @@
//! `key` example
use rustic_core::{KeyOpts, Repository, RepositoryOptions};
use simplelog::{Config, LevelFilter, SimpleLogger};
fn main() {
// Display info logs
let _ = SimpleLogger::init(LevelFilter::Info, Config::default());
// Open repository
let repo_opts = RepositoryOptions {
repository: Some("/tmp/repo".to_string()),
password: Some("test".to_string()),
..Default::default()
};
let repo = Repository::new(&repo_opts).unwrap().open().unwrap();
// Add a new key with the given password
let key_opts = KeyOpts::default();
repo.add_key("new_password", &key_opts).unwrap();
}

View File

@ -1,7 +1,10 @@
pub mod cat;
pub mod check;
pub mod config;
pub mod dump;
pub mod forget;
pub mod init;
pub mod key;
pub mod prune;
pub mod repoinfo;
pub mod snapshots;

View File

@ -0,0 +1,185 @@
//! `config` subcommand
use bytesize::ByteSize;
use crate::{
error::CommandErrorKind, ConfigFile, DecryptBackend, DecryptWriteBackend, Key, Open,
Repository, RusticResult,
};
pub(crate) fn apply_config<P, S: Open>(
repo: &Repository<P, S>,
opts: &ConfigOpts,
) -> RusticResult<bool> {
let mut new_config = repo.config().clone();
opts.apply(&mut new_config)?;
if &new_config == repo.config() {
Ok(false)
} else {
save_config(repo, new_config, *repo.key())?;
Ok(true)
}
}
pub(crate) fn save_config<P, S>(
repo: &Repository<P, S>,
mut new_config: ConfigFile,
key: Key,
) -> RusticResult<()> {
new_config.is_hot = None;
// don't compress the config file
let mut dbe = DecryptBackend::new(&repo.be, key);
dbe.set_zstd(None);
// for hot/cold backend, this only saves the config to the cold repo.
_ = dbe.save_file(&new_config)?;
if let Some(hot_be) = repo.be_hot.clone() {
// save config to hot repo
let mut dbe = DecryptBackend::new(&hot_be, key);
// don't compress the config file
dbe.set_zstd(None);
new_config.is_hot = Some(true);
_ = dbe.save_file(&new_config)?;
}
Ok(())
}
#[cfg_attr(feature = "clap", derive(clap::Parser))]
#[derive(Debug, Clone, Copy, Default)]
pub struct ConfigOpts {
/// Set compression level. Allowed levels are 1 to 22 and -1 to -7, see <https://facebook.github.io/zstd/>.
/// Note that 0 equals to no compression
#[cfg_attr(feature = "clap", clap(long, value_name = "LEVEL"))]
pub set_compression: Option<i32>,
/// Set repository version. Allowed versions: 1,2
#[cfg_attr(feature = "clap", clap(long, value_name = "VERSION"))]
pub set_version: Option<u32>,
/// Set default packsize for tree packs. rustic tries to always produce packs greater than this value.
/// Note that for large repos, this value is grown by the grown factor.
/// Defaults to 4 MiB if not set.
#[cfg_attr(feature = "clap", clap(long, value_name = "SIZE"))]
pub set_treepack_size: Option<ByteSize>,
/// Set upper limit for default packsize for tree packs.
/// Note that packs actually can get up to some MiBs larger.
/// If not set, pack sizes can grow up to approximately 4 GiB.
#[cfg_attr(feature = "clap", clap(long, value_name = "SIZE"))]
pub set_treepack_size_limit: Option<ByteSize>,
/// Set grow factor for tree packs. The default packsize grows by the square root of the total size of all
/// tree packs multiplied with this factor. This means 32 kiB times this factor per square root of total
/// treesize in GiB.
/// Defaults to 32 (= 1MB per square root of total treesize in GiB) if not set.
#[cfg_attr(feature = "clap", clap(long, value_name = "FACTOR"))]
pub set_treepack_growfactor: Option<u32>,
/// Set default packsize for data packs. rustic tries to always produce packs greater than this value.
/// Note that for large repos, this value is grown by the grown factor.
/// Defaults to 32 MiB if not set.
#[cfg_attr(feature = "clap", clap(long, value_name = "SIZE"))]
pub set_datapack_size: Option<ByteSize>,
/// Set grow factor for data packs. The default packsize grows by the square root of the total size of all
/// data packs multiplied with this factor. This means 32 kiB times this factor per square root of total
/// datasize in GiB.
/// Defaults to 32 (= 1MB per square root of total datasize in GiB) if not set.
#[cfg_attr(feature = "clap", clap(long, value_name = "FACTOR"))]
pub set_datapack_growfactor: Option<u32>,
/// Set upper limit for default packsize for tree packs.
/// Note that packs actually can get up to some MiBs larger.
/// If not set, pack sizes can grow up to approximately 4 GiB.
#[cfg_attr(feature = "clap", clap(long, value_name = "SIZE"))]
pub set_datapack_size_limit: Option<ByteSize>,
/// Set minimum tolerated packsize in percent of the targeted packsize.
/// Defaults to 30 if not set.
#[cfg_attr(feature = "clap", clap(long, value_name = "PERCENT"))]
pub set_min_packsize_tolerate_percent: Option<u32>,
/// Set maximum tolerated packsize in percent of the targeted packsize
/// A value of 0 means packs larger than the targeted packsize are always
/// tolerated. Default if not set: larger packfiles are always tolerated.
#[cfg_attr(feature = "clap", clap(long, value_name = "PERCENT"))]
pub set_max_packsize_tolerate_percent: Option<u32>,
}
impl ConfigOpts {
pub fn apply(&self, config: &mut ConfigFile) -> RusticResult<()> {
if let Some(version) = self.set_version {
let range = 1..=2;
if !range.contains(&version) {
return Err(CommandErrorKind::VersionNotSupported(version, range).into());
} else if version < config.version {
return Err(CommandErrorKind::CannotDowngrade(config.version, version).into());
}
config.version = version;
}
if let Some(compression) = self.set_compression {
if config.version == 1 && compression != 0 {
return Err(CommandErrorKind::NoCompressionV1Repo(compression).into());
}
let range = zstd::compression_level_range();
if !range.contains(&compression) {
return Err(
CommandErrorKind::CompressionLevelNotSupported(compression, range).into(),
);
}
config.compression = Some(compression);
}
if let Some(size) = self.set_treepack_size {
config.treepack_size = Some(
size.as_u64()
.try_into()
.map_err(|_| CommandErrorKind::SizeTooLarge(size))?,
);
}
if let Some(factor) = self.set_treepack_growfactor {
config.treepack_growfactor = Some(factor);
}
if let Some(size) = self.set_treepack_size_limit {
config.treepack_size_limit = Some(
size.as_u64()
.try_into()
.map_err(|_| CommandErrorKind::SizeTooLarge(size))?,
);
}
if let Some(size) = self.set_datapack_size {
config.datapack_size = Some(
size.as_u64()
.try_into()
.map_err(|_| CommandErrorKind::SizeTooLarge(size))?,
);
}
if let Some(factor) = self.set_datapack_growfactor {
config.datapack_growfactor = Some(factor);
}
if let Some(size) = self.set_datapack_size_limit {
config.datapack_size_limit = Some(
size.as_u64()
.try_into()
.map_err(|_| CommandErrorKind::SizeTooLarge(size))?,
);
}
if let Some(percent) = self.set_min_packsize_tolerate_percent {
if percent > 100 {
return Err(CommandErrorKind::MinPackSizeTolerateWrong.into());
}
config.min_packsize_tolerate_percent = Some(percent);
}
if let Some(percent) = self.set_max_packsize_tolerate_percent {
if percent < 100 && percent > 0 {
return Err(CommandErrorKind::MaxPackSizeTolerateWrong.into());
}
config.max_packsize_tolerate_percent = Some(percent);
}
Ok(())
}
}

View File

@ -0,0 +1,40 @@
//! `init` subcommand
use log::info;
use crate::{
commands::config::save_config, random_poly, ConfigFile, ConfigOpts, Id, Key, KeyOpts,
Repository, RusticResult, WriteBackend,
};
pub(crate) fn init<P, S>(
repo: &Repository<P, S>,
pass: &str,
key_opts: &KeyOpts,
config_opts: &ConfigOpts,
) -> RusticResult<(Key, ConfigFile)> {
// Create config first to allow catching errors from here without writing anything
let repo_id = Id::random();
let chunker_poly = random_poly()?;
let mut config = ConfigFile::new(2, repo_id, chunker_poly);
config_opts.apply(&mut config)?;
let key = init_with_config(repo, pass, key_opts, &config)?;
info!("repository {} successfully created.", repo_id);
Ok((key, config))
}
pub(crate) fn init_with_config<P, S>(
repo: &Repository<P, S>,
pass: &str,
key_opts: &KeyOpts,
config: &ConfigFile,
) -> RusticResult<Key> {
repo.be.create()?;
let (key, id) = key_opts.init_key(repo, pass)?;
info!("key {id} successfully added.");
save_config(repo, config.clone(), key)?;
Ok(key)
}

View File

@ -0,0 +1,53 @@
//! `key` subcommand
use crate::{
error::CommandErrorKind, hash, FileType, Id, Key, KeyFile, Open, Repository, RusticResult,
WriteBackend,
};
#[cfg_attr(feature = "clap", derive(clap::Parser))]
#[derive(Debug, Clone, Default)]
pub struct KeyOpts {
/// Set 'hostname' in public key information
#[cfg_attr(feature = "clap", clap(long))]
pub hostname: Option<String>,
/// Set 'username' in public key information
#[cfg_attr(feature = "clap", clap(long))]
pub username: Option<String>,
/// Add 'created' date in public key information
#[cfg_attr(feature = "clap", clap(long))]
pub with_created: bool,
}
impl KeyOpts {
pub(crate) fn add_key<P, S: Open>(
&self,
repo: &Repository<P, S>,
pass: &str,
) -> RusticResult<Id> {
let key = repo.key();
self.add(repo, pass, *key)
}
pub(crate) fn init_key<P, S>(
&self,
repo: &Repository<P, S>,
pass: &str,
) -> RusticResult<(Key, Id)> {
// generate key
let key = Key::new();
Ok((key, self.add(repo, pass, key)?))
}
fn add<P, S>(&self, repo: &Repository<P, S>, pass: &str, key: Key) -> RusticResult<Id> {
let ko = self.clone();
let keyfile = KeyFile::generate(key, &pass, ko.hostname, ko.username, ko.with_created)?;
let data = serde_json::to_vec(&keyfile).map_err(CommandErrorKind::FromJsonError)?;
let id = hash(&data);
repo.be
.write_bytes(FileType::Key, &id, false, data.into())?;
Ok(id)
}
}

View File

@ -7,6 +7,7 @@ use std::{
error::Error,
ffi::OsString,
num::{ParseIntError, TryFromIntError},
ops::RangeInclusive,
path::{PathBuf, StripPrefixError},
process::ExitStatus,
str::Utf8Error,
@ -162,6 +163,22 @@ pub enum CommandErrorKind {
FromOutOfRangeError(#[from] OutOfRangeError),
/// node type {0:?} not supported by dump
DumpNotSupported(NodeType),
/// {0:?}
FromJsonError(#[from] serde_json::Error),
/// version {0} is not supported. Allowed values: {1:?}
VersionNotSupported(u32, RangeInclusive<u32>),
/// cannot downgrade version from {0} to {1}
CannotDowngrade(u32, u32),
/// compression level {0} is not supported for repo v1
NoCompressionV1Repo(i32),
/// compression level {0} is not supported. Allowed values: {1:?}
CompressionLevelNotSupported(i32, RangeInclusive<i32>),
/// Size is too large: {0}
SizeTooLarge(bytesize::ByteSize),
/// min_packsize_tolerate_percent must be <= 100
MinPackSizeTolerateWrong,
/// max_packsize_tolerate_percent must be >= 100 or 0"
MaxPackSizeTolerateWrong,
}
/// [`CryptoErrorKind`] describes the errors that can happen while dealing with Cryptographic functions
@ -205,6 +222,8 @@ pub enum IdErrorKind {
pub enum RepositoryErrorKind {
/// No repository given. Please use the --repository option.
NoRepositoryGiven,
/// No password given. Please use one of the --password-* options.
NoPasswordGiven,
/// warm-up command must contain %id!
NoIDSpecified,
/// error opening password file `{0:?}`
@ -241,6 +260,8 @@ pub enum RepositoryErrorKind {
ReadingPasswordFromReaderFailed(std::io::Error),
/// reading Password from prompt failed: `{0:?}`
ReadingPasswordFromPromptFailed(std::io::Error),
/// Config file already exists. Aborting.
ConfigFileExists,
}
/// [`IndexErrorKind`] describes the errors that can be returned by processing Indizes

View File

@ -122,7 +122,9 @@ pub use crate::{
chunker::random_poly,
commands::{
check::CheckOpts,
config::ConfigOpts,
forget::{ForgetGroup, ForgetGroups, ForgetSnapshot, KeepOptions},
key::KeyOpts,
prune::{PruneOpts, PrunePlan, PruneStats},
repoinfo::{BlobInfo, IndexInfos, PackInfo, RepoFileInfo, RepoFileInfos},
},

View File

@ -9,7 +9,6 @@ use std::{
use bytes::Bytes;
use log::{debug, error, info};
use dialoguer::Password;
use nom::{
branch::alt,
bytes::complete::{is_not, tag},
@ -31,21 +30,19 @@ use crate::{
commands::{
self,
check::CheckOpts,
config::ConfigOpts,
forget::{ForgetGroups, KeepOptions},
key::KeyOpts,
repoinfo::{IndexInfos, RepoFileInfos},
},
crypto::aespoly1305::Key,
error::RepositoryErrorKind,
error::{KeyFileErrorKind, RepositoryErrorKind, RusticErrorKind},
repofile::{configfile::ConfigFile, keyfile::find_key_in_backend},
BlobType, DecryptFullBackend, Id, IndexBackend, IndexedBackend, NoProgressBars, Node,
ProgressBars, PruneOpts, PrunePlan, RusticResult, SnapshotFile, SnapshotGroup,
SnapshotGroupCriterion, Tree,
};
pub(super) mod constants {
pub(super) const MAX_PASSWORD_RETRIES: usize = 5;
}
mod warm_up;
use warm_up::{warm_up, warm_up_wait};
@ -176,7 +173,7 @@ pub fn read_password_from_reader(file: &mut impl BufRead) -> RusticResult<String
Ok(password)
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Repository<P, S> {
name: String,
pub be: HotColdBackend<ChooseBackend>,
@ -231,7 +228,8 @@ impl<P> Repository<P, ()> {
status: (),
})
}
}
impl<P, S> Repository<P, S> {
pub fn password(&self) -> RusticResult<Option<String>> {
match (
&self.opts.password,
@ -279,17 +277,32 @@ impl<P> Repository<P, ()> {
}
}
pub fn open(self) -> RusticResult<Repository<P, OpenStatus>> {
pub fn config_id(&self) -> RusticResult<Option<Id>> {
let config_ids = self
.be
.list(FileType::Config)
.map_err(|_| RepositoryErrorKind::ListingRepositoryConfigFileFailed)?;
match config_ids.len() {
1 => {} // ok, continue
0 => return Err(RepositoryErrorKind::NoRepositoryConfigFound(self.name).into()),
_ => return Err(RepositoryErrorKind::MoreThanOneRepositoryConfig(self.name).into()),
1 => Ok(Some(config_ids[0])),
0 => Ok(None),
_ => Err(RepositoryErrorKind::MoreThanOneRepositoryConfig(self.name.clone()).into()),
}
}
pub fn open(self) -> RusticResult<Repository<P, OpenStatus>> {
let password = self
.password()?
.ok_or(RepositoryErrorKind::NoPasswordGiven)?;
self.open_with_password(&password)
}
pub fn open_with_password(self, password: &str) -> RusticResult<Repository<P, OpenStatus>> {
let config_id = self
.config_id()?
.ok_or(RepositoryErrorKind::NoRepositoryConfigFound(
self.name.clone(),
))?;
if let Some(be_hot) = &self.be_hot {
let mut keys = self.be.list_with_size(FileType::Key)?;
@ -301,11 +314,56 @@ impl<P> Repository<P, ()> {
}
}
let key = get_key(&self.be, self.password()?)?;
let key = find_key_in_backend(&self.be, &password, None).map_err(|err| {
match err.into_inner() {
RusticErrorKind::KeyFile(KeyFileErrorKind::NoSuitableKeyFound) => {
RepositoryErrorKind::IncorrectPassword.into()
}
err => err,
}
})?;
info!("repository {}: password is correct.", self.name);
let dbe = DecryptBackend::new(&self.be, key);
let config: ConfigFile = dbe.get_file(&config_ids[0])?;
let config: ConfigFile = dbe.get_file(&config_id)?;
self.open_raw(key, config)
}
pub fn init(
self,
key_opts: &KeyOpts,
config_opts: &ConfigOpts,
) -> RusticResult<Repository<P, OpenStatus>> {
let password = self
.password()?
.ok_or(RepositoryErrorKind::NoPasswordGiven)?;
self.init_with_password(&password, key_opts, config_opts)
}
pub fn init_with_password(
self,
pass: &str,
key_opts: &KeyOpts,
config_opts: &ConfigOpts,
) -> RusticResult<Repository<P, OpenStatus>> {
if self.config_id()?.is_some() {
return Err(RepositoryErrorKind::ConfigFileExists.into());
}
let (key, config) = commands::init::init(&self, pass, key_opts, config_opts)?;
self.open_raw(key, config)
}
pub fn init_with_config(
self,
pass: &str,
key_opts: &KeyOpts,
config: ConfigFile,
) -> RusticResult<Repository<P, OpenStatus>> {
let key = commands::init::init_with_config(&self, pass, key_opts, &config)?;
info!("repository {} successfully created.", config.id);
self.open_raw(key, config)
}
fn open_raw(self, key: Key, config: ConfigFile) -> RusticResult<Repository<P, OpenStatus>> {
match (config.is_hot == Some(true), self.be_hot.is_some()) {
(true, false) => return Err(RepositoryErrorKind::HotRepositoryFlagMissing.into()),
(false, true) => return Err(RepositoryErrorKind::IsNotHotRepository.into()),
@ -354,32 +412,6 @@ impl<P: ProgressBars, S> Repository<P, S> {
}
}
pub(crate) fn get_key(be: &impl ReadBackend, password: Option<String>) -> RusticResult<Key> {
for _ in 0..constants::MAX_PASSWORD_RETRIES {
match password {
// if password is given, directly return the result of find_key_in_backend and don't retry
Some(pass) => {
return find_key_in_backend(be, &pass, None).map_err(std::convert::Into::into)
}
None => {
// TODO: Differentiate between wrong password and other error!
if let Ok(key) = find_key_in_backend(
be,
&Password::new()
.with_prompt("enter repository password")
.allow_empty_password(true)
.interact()
.map_err(RepositoryErrorKind::ReadingPasswordFromPromptFailed)?,
None,
) {
return Ok(key);
}
}
}
}
Err(RepositoryErrorKind::IncorrectPassword.into())
}
pub trait Open {
type DBE: DecryptFullBackend;
fn key(&self) -> &Key;
@ -429,6 +461,20 @@ impl Open for OpenStatus {
}
}
impl<P, S: Open> Repository<P, S> {
pub fn cat_file(&self, tpe: FileType, id: &str) -> RusticResult<Bytes> {
commands::cat::cat_file(self, tpe, id)
}
pub fn add_key(&self, pass: &str, opts: &KeyOpts) -> RusticResult<Id> {
opts.add_key(self, pass)
}
pub fn apply_config(&self, opts: &ConfigOpts) -> RusticResult<bool> {
commands::config::apply_config(self, opts)
}
}
impl<P: ProgressBars, S: Open> Repository<P, S> {
pub fn get_snapshot_group(
&self,
@ -460,10 +506,6 @@ impl<P: ProgressBars, S: Open> Repository<P, S> {
Ok(())
}
pub fn cat_file(&self, tpe: FileType, id: &str) -> RusticResult<Bytes> {
commands::cat::cat_file(self, tpe, id)
}
pub fn check(&self, opts: CheckOpts) -> RusticResult<()> {
opts.run(self)
}

View File

@ -38,11 +38,15 @@ use crate::{
{Application, RUSTIC_APP},
};
use abscissa_core::{
config::Override, status_err, Command, Configurable, FrameworkError, Runnable, Shutdown,
};
use abscissa_core::{config::Override, Command, Configurable, FrameworkError, Runnable, Shutdown};
use anyhow::{anyhow, Result};
use dialoguer::Password;
use rustic_core::{OpenStatus, Repository};
pub(super) mod constants {
pub(super) const MAX_PASSWORD_RETRIES: usize = 5;
}
/// Rustic Subcommands
/// Subcommands need to be listed in an enum.
#[derive(clap::Parser, Command, Debug, Runnable)]
@ -167,25 +171,29 @@ impl Configurable<RusticConfig> for EntryPoint {
}
}
fn open_repository<P>(repo: Repository<P, ()>) -> Repository<P, OpenStatus> {
match repo.open() {
Ok(it) => it,
Err(err) => {
status_err!("{}", err);
RUSTIC_APP.shutdown(Shutdown::Crash);
}
}
}
fn get_repository(config: &Arc<RusticConfig>) -> Repository<ProgressOptions, ()> {
fn open_repository(config: &Arc<RusticConfig>) -> Result<Repository<ProgressOptions, OpenStatus>> {
let po = config.global.progress_options;
match Repository::new_with_progress(&config.repository, po) {
Ok(it) => it,
Err(err) => {
status_err!("{}", err);
RUSTIC_APP.shutdown(Shutdown::Crash);
let repo = Repository::new_with_progress(&config.repository, po)?;
match repo.password()? {
// if password is given, directly return the result of find_key_in_backend and don't retry
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),
// TODO: fail if error != Password incorrect
Err(_) => continue,
}
}
}
}
Err(anyhow!("incorrect password"))
}
#[test]

View File

@ -3,7 +3,7 @@
/// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()`
/// accessors along with logging macros. Customize as you see fit.
use crate::{
commands::{get_repository, open_repository},
commands::open_repository,
helpers::bytes_size_to_string,
{status_err, Application, RUSTIC_APP},
};
@ -153,7 +153,7 @@ impl BackupCmd {
let config = RUSTIC_APP.config();
let progress_options = &config.global.progress_options;
let repo = open_repository(get_repository(&config));
let repo = open_repository(&config)?;
// manually check for a "source" field, check is not done by serde, see above.
if !config.backup.source.is_empty() {

View File

@ -2,10 +2,7 @@
/// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()`
/// accessors along with logging macros. Customize as you see fit.
use crate::{
commands::{get_repository, open_repository},
status_err, Application, RUSTIC_APP,
};
use crate::{commands::open_repository, status_err, Application, RUSTIC_APP};
use abscissa_core::{Command, Runnable, Shutdown};
@ -61,7 +58,7 @@ impl Runnable for CatCmd {
impl CatCmd {
fn inner_run(&self) -> Result<()> {
let config = RUSTIC_APP.config();
let repo = open_repository(get_repository(&config));
let repo = open_repository(&config)?;
let data = match &self.cmd {
CatSubCmd::Config => repo.cat_file(FileType::Config, "")?,

View File

@ -2,13 +2,10 @@
/// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()`
/// accessors along with logging macros. Customize as you see fit.
use crate::{
commands::{get_repository, open_repository},
status_err, Application, RUSTIC_APP,
};
use crate::{commands::open_repository, status_err, Application, RUSTIC_APP};
use abscissa_core::{Command, Runnable, Shutdown};
use anyhow::Result;
use rustic_core::CheckOpts;
/// `check` subcommand
@ -20,12 +17,18 @@ pub(crate) struct CheckCmd {
impl Runnable for CheckCmd {
fn run(&self) {
let config = RUSTIC_APP.config();
let repo = open_repository(get_repository(&config));
if let Err(err) = repo.check(self.opts) {
if let Err(err) = self.inner_run() {
status_err!("{}", err);
RUSTIC_APP.shutdown(Shutdown::Crash);
};
}
}
impl CheckCmd {
fn inner_run(&self) -> Result<()> {
let config = RUSTIC_APP.config();
let repo = open_repository(&config)?;
repo.check(self.opts)?;
Ok(())
}
}

View File

@ -2,17 +2,13 @@
/// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()`
/// accessors along with logging macros. Customize as you see fit.
use crate::{
commands::{get_repository, open_repository},
status_err, Application, RUSTIC_APP,
};
use crate::{commands::open_repository, status_err, Application, RUSTIC_APP};
use abscissa_core::{Command, Runnable, Shutdown};
use anyhow::{bail, Result};
use bytesize::ByteSize;
use anyhow::Result;
use rustic_core::{ConfigFile, DecryptBackend, DecryptWriteBackend, Open};
use rustic_core::ConfigOpts;
/// `config` subcommand
#[derive(clap::Parser, Command, Debug)]
@ -33,163 +29,14 @@ impl Runnable for ConfigCmd {
impl ConfigCmd {
fn inner_run(&self) -> Result<()> {
let config = RUSTIC_APP.config();
let repo = open_repository(get_repository(&config));
let repo = open_repository(&config)?;
let mut new_config = repo.config().clone();
self.config_opts.apply(&mut new_config)?;
if &new_config == repo.config() {
println!("config is unchanged");
} else {
new_config.is_hot = None;
// don't compress the config file
let mut dbe = repo.dbe().clone();
dbe.set_zstd(None);
// for hot/cold backend, this only saves the config to the cold repo.
_ = dbe.save_file(&new_config)?;
if let Some(hot_be) = repo.be_hot.clone() {
// save config to hot repo
let mut dbe = DecryptBackend::new(&hot_be, *repo.key());
// don't compress the config file
dbe.set_zstd(None);
new_config.is_hot = Some(true);
_ = dbe.save_file(&new_config)?;
}
let changed = repo.apply_config(&self.config_opts)?;
if changed {
println!("saved new config");
}
Ok(())
}
}
#[derive(clap::Parser, Debug)]
pub(crate) struct ConfigOpts {
/// Set compression level. Allowed levels are 1 to 22 and -1 to -7, see <https://facebook.github.io/zstd/>.
/// Note that 0 equals to no compression
#[clap(long, value_name = "LEVEL")]
pub(crate) set_compression: Option<i32>,
/// Set repository version. Allowed versions: 1,2
#[clap(long, value_name = "VERSION")]
pub(crate) set_version: Option<u32>,
/// Set default packsize for tree packs. rustic tries to always produce packs greater than this value.
/// Note that for large repos, this value is grown by the grown factor.
/// Defaults to 4 MiB if not set.
#[clap(long, value_name = "SIZE")]
pub(crate) set_treepack_size: Option<ByteSize>,
/// Set upper limit for default packsize for tree packs.
/// Note that packs actually can get up to some MiBs larger.
/// If not set, pack sizes can grow up to approximately 4 GiB.
#[clap(long, value_name = "SIZE")]
pub(crate) set_treepack_size_limit: Option<ByteSize>,
/// Set grow factor for tree packs. The default packsize grows by the square root of the total size of all
/// tree packs multiplied with this factor. This means 32 kiB times this factor per square root of total
/// treesize in GiB.
/// Defaults to 32 (= 1MB per square root of total treesize in GiB) if not set.
#[clap(long, value_name = "FACTOR")]
pub(crate) set_treepack_growfactor: Option<u32>,
/// Set default packsize for data packs. rustic tries to always produce packs greater than this value.
/// Note that for large repos, this value is grown by the grown factor.
/// Defaults to 32 MiB if not set.
#[clap(long, value_name = "SIZE")]
pub(crate) set_datapack_size: Option<ByteSize>,
/// Set grow factor for data packs. The default packsize grows by the square root of the total size of all
/// data packs multiplied with this factor. This means 32 kiB times this factor per square root of total
/// datasize in GiB.
/// Defaults to 32 (= 1MB per square root of total datasize in GiB) if not set.
#[clap(long, value_name = "FACTOR")]
pub(crate) set_datapack_growfactor: Option<u32>,
/// Set upper limit for default packsize for tree packs.
/// Note that packs actually can get up to some MiBs larger.
/// If not set, pack sizes can grow up to approximately 4 GiB.
#[clap(long, value_name = "SIZE")]
pub(crate) set_datapack_size_limit: Option<ByteSize>,
/// Set minimum tolerated packsize in percent of the targeted packsize.
/// Defaults to 30 if not set.
#[clap(long, value_name = "PERCENT")]
pub(crate) set_min_packsize_tolerate_percent: Option<u32>,
/// Set maximum tolerated packsize in percent of the targeted packsize
/// A value of 0 means packs larger than the targeted packsize are always
/// tolerated. Default if not set: larger packfiles are always tolerated.
#[clap(long, value_name = "PERCENT")]
pub(crate) set_max_packsize_tolerate_percent: Option<u32>,
}
impl ConfigOpts {
pub(crate) fn apply(&self, config: &mut ConfigFile) -> Result<()> {
if let Some(version) = self.set_version {
let range = 1..=2;
if !range.contains(&version) {
bail!(
"version {version} is not supported. Allowed values: {}..{}",
range.start(),
range.end()
);
} else if version < config.version {
bail!(
"cannot downgrade version from {} to {version}",
config.version
);
}
config.version = version;
}
if let Some(compression) = self.set_compression {
if config.version == 1 && compression != 0 {
bail!("compression level {compression} is not supported for repo v1");
}
let range = zstd::compression_level_range();
if !range.contains(&compression) {
bail!(
"compression level {compression} is not supported. Allowed values: 0..{}",
range.end()
);
}
config.compression = Some(compression);
}
if let Some(size) = self.set_treepack_size {
config.treepack_size = Some(size.as_u64().try_into()?);
}
if let Some(factor) = self.set_treepack_growfactor {
config.treepack_growfactor = Some(factor);
}
if let Some(size) = self.set_treepack_size_limit {
config.treepack_size_limit = Some(size.as_u64().try_into()?);
}
if let Some(size) = self.set_datapack_size {
config.datapack_size = Some(size.as_u64().try_into()?);
}
if let Some(factor) = self.set_datapack_growfactor {
config.datapack_growfactor = Some(factor);
}
if let Some(size) = self.set_datapack_size_limit {
config.datapack_size_limit = Some(size.as_u64().try_into()?);
}
if let Some(percent) = self.set_min_packsize_tolerate_percent {
if percent > 100 {
bail!("set_min_packsize_tolerate_percent must be <= 100");
}
config.min_packsize_tolerate_percent = Some(percent);
}
if let Some(percent) = self.set_max_packsize_tolerate_percent {
if percent < 100 && percent > 0 {
bail!("set_max_packsize_tolerate_percent must be >= 100 or 0");
}
config.max_packsize_tolerate_percent = Some(percent);
} else {
println!("config is unchanged");
}
Ok(())

View File

@ -2,11 +2,7 @@
/// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()`
/// accessors along with logging macros. Customize as you see fit.
use crate::{
commands::{get_repository, init::save_config, open_repository},
helpers::copy,
status_err, Application, RUSTIC_APP,
};
use crate::{commands::open_repository, helpers::copy, status_err, Application, RUSTIC_APP};
use abscissa_core::{Command, Runnable, Shutdown};
use anyhow::{bail, Result};
use log::info;
@ -14,10 +10,8 @@ use log::info;
use merge::Merge;
use serde::Deserialize;
use crate::commands::key::KeyOpts;
use rustic_core::{
FileType, Id, IndexBackend, Open, ProgressBars, ReadBackend, Repository, RepositoryOptions,
SnapshotFile,
Id, IndexBackend, KeyOpts, Open, ProgressBars, Repository, RepositoryOptions, SnapshotFile,
};
/// `copy` subcommand
@ -54,7 +48,7 @@ impl CopyCmd {
fn inner_run(&self) -> Result<()> {
let config = RUSTIC_APP.config();
let repo = open_repository(get_repository(&config));
let repo = open_repository(&config)?;
if config.copy.targets.is_empty() {
status_err!("no [[copy.targets]] section in config file found!");
@ -74,23 +68,18 @@ impl CopyCmd {
let index = IndexBackend::new(be, &config.global.progress_options.progress_counter(""))?;
let poly = repo.config().poly()?;
for target_opt in &config.copy.targets {
let repo_dest = Repository::new(target_opt)?;
if self.init && repo_dest.be.list(FileType::Config)?.is_empty() {
let repo_dest = if self.init && repo_dest.config_id()?.is_none() {
let mut config_dest = repo.config().clone();
config_dest.id = Id::random();
save_config(
config_dest,
&repo_dest.be,
&repo_dest.be_hot,
self.key_opts.clone(),
repo_dest.password()?,
)?;
}
let pass = repo_dest.password()?.unwrap();
repo_dest.init_with_config(&pass, &self.key_opts, config_dest)?
} else {
repo_dest.open()?
};
let repo_dest = repo_dest.open()?;
info!("copying to target {:?}...", repo_dest); // TODO: repo_dest.name
if poly != repo_dest.config().poly()? {
bail!("cannot copy to repository with different chunker parameter (re-chunking not implemented)!");

View File

@ -2,10 +2,7 @@
/// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()`
/// accessors along with logging macros. Customize as you see fit.
use crate::{
commands::{get_repository, open_repository},
status_err, Application, RUSTIC_APP,
};
use crate::{commands::open_repository, status_err, Application, RUSTIC_APP};
use abscissa_core::{Command, Runnable, Shutdown};
@ -56,7 +53,7 @@ impl DiffCmd {
fn inner_run(&self) -> Result<()> {
let config = RUSTIC_APP.config();
let repo = open_repository(get_repository(&config));
let repo = open_repository(&config)?;
let be = repo.dbe();
let (id1, path1) = arg_to_snap_path(&self.snap1, "");

View File

@ -2,7 +2,7 @@
/// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()`
/// accessors along with logging macros. Customize as you see fit.
use crate::{commands::get_repository, status_err, Application, RUSTIC_APP};
use crate::{commands::open_repository, status_err, Application, RUSTIC_APP};
use abscissa_core::{Command, Runnable, Shutdown};
use anyhow::Result;
@ -28,7 +28,7 @@ impl DumpCmd {
fn inner_run(&self) -> Result<()> {
let config = RUSTIC_APP.config();
let repo = get_repository(&config).open()?.to_indexed()?;
let repo = open_repository(&config)?.to_indexed()?;
let node =
repo.node_from_snapshot_path(&self.snap, |sn| config.snapshot_filter.matches(sn))?;

View File

@ -3,7 +3,7 @@
/// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()`
/// accessors along with logging macros. Customize as you see fit.
use crate::{
commands::get_repository, helpers::table_with_titles, status_err, Application, RusticConfig,
commands::open_repository, helpers::table_with_titles, status_err, Application, RusticConfig,
RUSTIC_APP,
};
@ -88,7 +88,7 @@ impl Runnable for ForgetCmd {
impl ForgetCmd {
fn inner_run(&self) -> Result<()> {
let config = RUSTIC_APP.config();
let repo = get_repository(&config).open()?;
let repo = open_repository(&config)?;
let group_by = config.forget.group_by.unwrap_or_default();

View File

@ -5,17 +5,11 @@
use abscissa_core::{status_err, Command, Runnable, Shutdown};
use anyhow::{bail, Result};
use crate::{commands::get_repository, Application, RUSTIC_APP};
use crate::{Application, RUSTIC_APP};
use bytes::Bytes;
use dialoguer::Password;
use rustic_core::{
hash, random_poly, ConfigFile, DecryptBackend, DecryptWriteBackend, FileType, Id, Key, KeyFile,
ReadBackend, WriteBackend,
};
use crate::commands::{config::ConfigOpts, key::KeyOpts};
use rustic_core::{ConfigOpts, KeyOpts, Repository};
/// `init` subcommand
#[derive(clap::Parser, Command, Debug)]
@ -40,47 +34,25 @@ impl InitCmd {
fn inner_run(&self) -> Result<()> {
let config = RUSTIC_APP.config();
let repo = get_repository(&config);
let po = config.global.progress_options;
let repo = Repository::new_with_progress(&config.repository, po)?;
let config_ids = repo.be.list(FileType::Config)?;
let password = repo.password()?;
let be = &repo.be;
let hot_be = &repo.be_hot;
if !config_ids.is_empty() {
// Note: This is again checked in repo.init_with_password(), however we want to inform
// users before they are prompted to enter a password
if repo.config_id()?.is_some() {
bail!("Config file already exists. Aborting.");
}
// Create config first to allow catching errors from here without writing anything
let repo_id = Id::random();
let chunker_poly = random_poly()?;
let version = match self.config_opts.set_version {
None => 2,
Some(_) => 1, // will be changed later
};
let mut config = ConfigFile::new(version, repo_id, chunker_poly);
self.config_opts.apply(&mut config)?;
save_config(config, be, hot_be, self.key_opts.clone(), password)?;
Ok(())
init(repo, &self.key_opts, &self.config_opts)
}
}
pub(crate) fn save_config(
mut config: ConfigFile,
be: &impl WriteBackend,
hot_be: &Option<impl WriteBackend>,
key_opts: KeyOpts,
password: Option<String>,
pub(crate) fn init<P, S>(
repo: Repository<P, S>,
key_opts: &KeyOpts,
config_opts: &ConfigOpts,
) -> Result<()> {
// generate key
let key = Key::new();
let pass = password.map_or_else(
|| match Password::new()
let pass = repo.password()?.unwrap_or_else(|| {
match Password::new()
.with_prompt("enter password for new key")
.allow_empty_password(true)
.with_confirmation("confirm password", "passwords do not match")
@ -91,34 +63,10 @@ pub(crate) fn save_config(
status_err!("{}", err);
RUSTIC_APP.shutdown(Shutdown::Crash);
}
},
|pass| pass,
);
}
});
let keyfile = KeyFile::generate(
key,
&pass,
key_opts.hostname,
key_opts.username,
key_opts.with_created,
)?;
let data: Bytes = serde_json::to_vec(&keyfile)?.into();
let id = hash(&data);
be.create()?;
be.write_bytes(FileType::Key, &id, false, data)?;
println!("key {id} successfully added.");
// save config
let dbe = DecryptBackend::new(be, key);
config.is_hot = None;
_ = dbe.save_file(&config)?;
if let Some(hot_be) = hot_be {
let dbe = DecryptBackend::new(hot_be, key);
config.is_hot = Some(true);
_ = dbe.save_file(&config)?;
}
println!("repository {} successfully created.", config.id);
let _ = repo.init_with_password(&pass, key_opts, config_opts)?;
Ok(())
}

View File

@ -2,19 +2,16 @@
/// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()`
/// accessors along with logging macros. Customize as you see fit.
use crate::{
commands::{get_repository, open_repository},
status_err, Application, RUSTIC_APP,
};
use crate::{commands::open_repository, status_err, Application, RUSTIC_APP};
use std::path::PathBuf;
use abscissa_core::{Command, Runnable, Shutdown};
use anyhow::Result;
use std::{fs::File, io::BufReader};
use dialoguer::Password;
use log::info;
use rustic_core::{hash, read_password_from_reader, FileType, KeyFile, Open, WriteBackend};
use rustic_core::{KeyOpts, Repository, RepositoryOptions};
/// `key` subcommand
#[derive(clap::Parser, Command, Debug)]
@ -33,27 +30,12 @@ enum KeySubCmd {
pub(crate) struct AddCmd {
/// File from which to read the new password
#[clap(long)]
pub(crate) new_password_file: Option<String>,
pub(crate) new_password_file: Option<PathBuf>,
#[clap(flatten)]
pub(crate) key_opts: KeyOpts,
}
#[derive(clap::Parser, Debug, Clone)]
pub(crate) struct KeyOpts {
/// Set 'hostname' in public key information
#[clap(long)]
pub(crate) hostname: Option<String>,
/// Set 'username' in public key information
#[clap(long)]
pub(crate) username: Option<String>,
/// Add 'created' date in public key information
#[clap(long)]
pub(crate) with_created: bool,
}
impl Runnable for KeyCmd {
fn run(&self) {
self.cmd.run();
@ -73,48 +55,30 @@ impl AddCmd {
fn inner_run(&self) -> Result<()> {
let config = RUSTIC_APP.config();
let repo = open_repository(get_repository(&config));
let repo = open_repository(&config)?;
let be = repo.dbe();
let key = repo.key();
// create new "artificial" repo using the given password options
let repo_opts = RepositoryOptions {
password_file: self.new_password_file.clone(),
repository: Some(String::new()), // fake repository to make Repository::new() not bail
..Default::default()
};
let repo_newpass = Repository::new(&repo_opts)?;
let pass = self.new_password_file.as_ref().map_or_else(
|| match Password::new()
.with_prompt("enter password for new key")
.allow_empty_password(true)
.with_confirmation("confirm password", "passwords do not match")
.interact()
{
Ok(it) => it,
Err(err) => {
status_err!("{}", err);
RUSTIC_APP.shutdown(Shutdown::Crash);
}
},
|file| {
let mut file = BufReader::new(match File::open(file) {
Ok(it) => it,
Err(err) => {
status_err!("{}", err);
RUSTIC_APP.shutdown(Shutdown::Crash);
}
});
match read_password_from_reader(&mut file) {
Ok(it) => it,
Err(err) => {
status_err!("{}", err);
RUSTIC_APP.shutdown(Shutdown::Crash);
}
}
},
);
let ko = self.key_opts.clone();
let keyfile = KeyFile::generate(*key, &pass, ko.hostname, ko.username, ko.with_created)?;
let data = serde_json::to_vec(&keyfile)?;
let id = hash(&data);
be.write_bytes(FileType::Key, &id, false, data.into())?;
let pass = repo_newpass
.password()
.map_err(|err| err.into())
.transpose()
.unwrap_or_else(|| -> Result<_> {
Ok(Password::new()
.with_prompt("enter password for new key")
.allow_empty_password(true)
.with_confirmation("confirm password", "passwords do not match")
.interact()?)
})?;
println!("key {id} successfully added.");
let id = repo.add_key(&pass, &self.key_opts)?;
info!("key {id} successfully added.");
Ok(())
}

View File

@ -2,10 +2,7 @@
/// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()`
/// accessors along with logging macros. Customize as you see fit.
use crate::{
commands::{get_repository, open_repository},
status_err, Application, RUSTIC_APP,
};
use crate::{commands::open_repository, status_err, Application, RUSTIC_APP};
use abscissa_core::{Command, Runnable, Shutdown};
@ -34,7 +31,7 @@ impl ListCmd {
fn inner_run(&self) -> Result<()> {
let config = RUSTIC_APP.config();
let repo = open_repository(get_repository(&config));
let repo = open_repository(&config)?;
let tpe = match self.tpe.as_str() {
// special treatment for listing blobs: read the index and display it

View File

@ -2,10 +2,7 @@
/// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()`
/// accessors along with logging macros. Customize as you see fit.
use crate::{
commands::{get_repository, open_repository},
status_err, Application, RUSTIC_APP,
};
use crate::{commands::open_repository, status_err, Application, RUSTIC_APP};
use abscissa_core::{Command, Runnable, Shutdown};
use anyhow::Result;
@ -45,7 +42,7 @@ impl LsCmd {
let config = RUSTIC_APP.config();
let progress_options = &config.global.progress_options;
let repo = open_repository(get_repository(&config));
let repo = open_repository(&config)?;
let be = repo.dbe();
let mut recursive = self.recursive;

View File

@ -2,10 +2,7 @@
/// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()`
/// accessors along with logging macros. Customize as you see fit.
use crate::{
commands::{get_repository, open_repository},
status_err, Application, RUSTIC_APP,
};
use crate::{commands::open_repository, status_err, Application, RUSTIC_APP};
use abscissa_core::{Command, Runnable, Shutdown};
use anyhow::Result;
use log::info;
@ -57,7 +54,7 @@ impl MergeCmd {
let config = RUSTIC_APP.config();
let progress_options = &config.global.progress_options;
let repo = open_repository(get_repository(&config));
let repo = open_repository(&config)?;
let be = repo.dbe();

View File

@ -3,9 +3,7 @@
/// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()`
/// accessors along with logging macros. Customize as you see fit.
use crate::{
commands::{get_repository, open_repository},
helpers::bytes_size_to_string,
status_err, Application, RUSTIC_APP,
commands::open_repository, helpers::bytes_size_to_string, status_err, Application, RUSTIC_APP,
};
use abscissa_core::{Command, Runnable, Shutdown};
use log::debug;
@ -35,7 +33,7 @@ impl Runnable for PruneCmd {
impl PruneCmd {
fn inner_run(&self) -> Result<()> {
let config = RUSTIC_APP.config();
let repo = open_repository(get_repository(&config));
let repo = open_repository(&config)?;
let pruner = repo.prune_plan(&self.opts)?;

View File

@ -2,10 +2,7 @@
/// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()`
/// accessors along with logging macros. Customize as you see fit.
use crate::{
commands::{get_repository, open_repository},
status_err, Application, RUSTIC_APP,
};
use crate::{commands::open_repository, status_err, Application, RUSTIC_APP};
use abscissa_core::{Command, Runnable, Shutdown};
use log::{debug, info, warn};
@ -87,7 +84,7 @@ impl IndexSubCmd {
let config = RUSTIC_APP.config();
let progress_options = &config.global.progress_options;
let repo = open_repository(get_repository(&config));
let repo = open_repository(&config)?;
let be = repo.dbe();
let p = progress_options.progress_spinner("listing packs...");
@ -235,7 +232,7 @@ impl SnapSubCmd {
let config = RUSTIC_APP.config();
let progress_options = &config.global.progress_options;
let repo = open_repository(get_repository(&config));
let repo = open_repository(&config)?;
let be = repo.dbe();
let config_file = repo.config();

View File

@ -3,7 +3,7 @@
/// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()`
/// accessors along with logging macros. Customize as you see fit.
use crate::{
commands::get_repository, helpers::bytes_size_to_string, status_err, Application, RUSTIC_APP,
commands::open_repository, helpers::bytes_size_to_string, status_err, Application, RUSTIC_APP,
};
use abscissa_core::{Command, Runnable, Shutdown};
@ -11,7 +11,7 @@ use serde::Serialize;
use crate::helpers::table_right_from;
use anyhow::Result;
use rustic_core::{IndexInfos, RepoFileInfo, RepoFileInfos};
use rustic_core::{IndexInfos, RepoFileInfo, RepoFileInfos, Repository};
/// `repoinfo` subcommand
#[derive(clap::Parser, Command, Debug)]
@ -48,15 +48,19 @@ struct Infos {
impl RepoInfoCmd {
fn inner_run(&self) -> Result<()> {
let config = RUSTIC_APP.config();
let repo = get_repository(&config);
let infos = Infos {
files: (!self.only_index).then(|| repo.infos_files()).transpose()?,
files: (!self.only_index)
.then(|| {
let po = config.global.progress_options;
let repo = Repository::new_with_progress(&config.repository, po)?;
repo.infos_files()
})
.transpose()?,
index: (!self.only_files)
.then(|| -> Result<_> {
let repo = repo.open()?;
let info_index = repo.infos_index()?;
Ok(info_index)
let repo = open_repository(&config)?;
Ok(repo.infos_index()?)
})
.transpose()?,
};

View File

@ -3,9 +3,7 @@
/// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()`
/// accessors along with logging macros. Customize as you see fit.
use crate::{
commands::{get_repository, open_repository},
helpers::bytes_size_to_string,
status_err, Application, RUSTIC_APP,
commands::open_repository, helpers::bytes_size_to_string, status_err, Application, RUSTIC_APP,
};
use log::{debug, error, info, trace, warn};
@ -90,7 +88,7 @@ impl RestoreCmd {
fn inner_run(&self) -> Result<()> {
let config = RUSTIC_APP.config();
let progress_options = &config.global.progress_options;
let repo = open_repository(get_repository(&config));
let repo = open_repository(&config)?;
let be = repo.dbe();
let (id, path) = self.snap.split_once(':').unwrap_or((&self.snap, ""));

View File

@ -3,7 +3,7 @@
/// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()`
/// accessors along with logging macros. Customize as you see fit.
use crate::{
commands::get_repository,
commands::open_repository,
helpers::{bold_cell, bytes_size_to_string, table, table_right_from},
status_err, Application, RUSTIC_APP,
};
@ -56,7 +56,7 @@ impl Runnable for SnapshotCmd {
impl SnapshotCmd {
fn inner_run(&self) -> Result<()> {
let config = RUSTIC_APP.config();
let repo = get_repository(&config).open()?;
let repo = open_repository(&config)?;
let groups = repo.get_snapshot_group(&self.ids, self.group_by, |sn| {
config.snapshot_filter.matches(sn)

View File

@ -2,10 +2,7 @@
/// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()`
/// accessors along with logging macros. Customize as you see fit.
use crate::{
commands::{get_repository, open_repository},
status_err, Application, RUSTIC_APP,
};
use crate::{commands::open_repository, status_err, Application, RUSTIC_APP};
use abscissa_core::{Command, Runnable, Shutdown};
@ -79,7 +76,7 @@ impl Runnable for TagCmd {
impl TagCmd {
fn inner_run(&self) -> anyhow::Result<()> {
let config = RUSTIC_APP.config();
let repo = open_repository(get_repository(&config));
let repo = open_repository(&config)?;
let be = repo.dbe();

View File

@ -26,7 +26,8 @@ pub fn rustic_runner(temp_dir: &TempDir) -> CmdRunner {
.arg("--password")
.arg(password)
.arg("--no-progress")
.capture_stdout();
.capture_stdout()
.capture_stderr();
runner
}
@ -35,11 +36,13 @@ fn setup() -> TestResult<TempDir> {
let mut runner = rustic_runner(&temp_dir);
let mut cmd = runner.args(["init"]).run();
let mut output = String::new();
cmd.stdout().read_to_string(&mut output)?;
let mut stdout = String::new();
let mut stderr = String::new();
cmd.stdout().read_to_string(&mut stdout)?;
cmd.stderr().read_to_string(&mut stderr)?;
let patterns = &["successfully added.", "successfully created."];
let matches = get_matches(patterns, output)?;
let matches = get_matches(patterns, stderr)?;
assert_eq!(
matches,
@ -124,7 +127,7 @@ fn test_backup_and_check_passes() -> TestResult<()> {
let mut runner = rustic_runner(&temp_dir);
let mut cmd = runner.args(["check", "--read-data"]).run();
let mut output = String::new();
cmd.stdout().read_to_string(&mut output)?;
cmd.stderr().read_to_string(&mut output)?;
let patterns = &["WARN", "ERROR"];
let matches = get_matches(patterns, output)?;