mirror of
https://github.com/rustic-rs/rustic.git
synced 2025-10-26 11:18:51 +00:00
Merge pull request #727 from rustic-rs/refactor-key-config-init
Refactor key, config and init command
This commit is contained in:
commit
abc42a1994
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2413,7 +2413,6 @@ dependencies = [
|
||||
"crossbeam-channel",
|
||||
"derivative",
|
||||
"derive_more",
|
||||
"dialoguer",
|
||||
"directories",
|
||||
"dirs",
|
||||
"displaydoc",
|
||||
|
||||
@ -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 }
|
||||
|
||||
24
crates/rustic_core/examples/config.rs
Normal file
24
crates/rustic_core/examples/config.rs
Normal 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();
|
||||
}
|
||||
23
crates/rustic_core/examples/init.rs
Normal file
23
crates/rustic_core/examples/init.rs
Normal 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
|
||||
}
|
||||
21
crates/rustic_core/examples/key.rs
Normal file
21
crates/rustic_core/examples/key.rs
Normal 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();
|
||||
}
|
||||
@ -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;
|
||||
|
||||
185
crates/rustic_core/src/commands/config.rs
Normal file
185
crates/rustic_core/src/commands/config.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
40
crates/rustic_core/src/commands/init.rs
Normal file
40
crates/rustic_core/src/commands/init.rs
Normal 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)
|
||||
}
|
||||
53
crates/rustic_core/src/commands/key.rs
Normal file
53
crates/rustic_core/src/commands/key.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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},
|
||||
},
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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, "")?,
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(())
|
||||
|
||||
@ -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)!");
|
||||
|
||||
@ -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, "");
|
||||
|
||||
@ -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))?;
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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)?;
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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()?,
|
||||
};
|
||||
|
||||
@ -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, ""));
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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)?;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user