diff --git a/Cargo.lock b/Cargo.lock
index ec5d300..37bcc9b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2413,7 +2413,6 @@ dependencies = [
"crossbeam-channel",
"derivative",
"derive_more",
- "dialoguer",
"directories",
"dirs",
"displaydoc",
diff --git a/crates/rustic_core/Cargo.toml b/crates/rustic_core/Cargo.toml
index 7305c3b..4aead8a 100644
--- a/crates/rustic_core/Cargo.toml
+++ b/crates/rustic_core/Cargo.toml
@@ -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 }
diff --git a/crates/rustic_core/examples/config.rs b/crates/rustic_core/examples/config.rs
new file mode 100644
index 0000000..84333f6
--- /dev/null
+++ b/crates/rustic_core/examples/config.rs
@@ -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();
+}
diff --git a/crates/rustic_core/examples/init.rs b/crates/rustic_core/examples/init.rs
new file mode 100644
index 0000000..152045e
--- /dev/null
+++ b/crates/rustic_core/examples/init.rs
@@ -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
+}
diff --git a/crates/rustic_core/examples/key.rs b/crates/rustic_core/examples/key.rs
new file mode 100644
index 0000000..55c528e
--- /dev/null
+++ b/crates/rustic_core/examples/key.rs
@@ -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();
+}
diff --git a/crates/rustic_core/src/commands.rs b/crates/rustic_core/src/commands.rs
index f450b03..465212f 100644
--- a/crates/rustic_core/src/commands.rs
+++ b/crates/rustic_core/src/commands.rs
@@ -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;
diff --git a/crates/rustic_core/src/commands/config.rs b/crates/rustic_core/src/commands/config.rs
new file mode 100644
index 0000000..1ee0cc6
--- /dev/null
+++ b/crates/rustic_core/src/commands/config.rs
@@ -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
(
+ repo: &Repository
,
+ opts: &ConfigOpts,
+) -> RusticResult {
+ 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(
+ repo: &Repository
,
+ 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 .
+ /// Note that 0 equals to no compression
+ #[cfg_attr(feature = "clap", clap(long, value_name = "LEVEL"))]
+ pub set_compression: Option,
+
+ /// Set repository version. Allowed versions: 1,2
+ #[cfg_attr(feature = "clap", clap(long, value_name = "VERSION"))]
+ pub set_version: Option,
+
+ /// 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,
+
+ /// 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,
+
+ /// 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,
+
+ /// 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,
+
+ /// 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,
+
+ /// 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,
+
+ /// 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,
+
+ /// 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,
+}
+
+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(())
+ }
+}
diff --git a/crates/rustic_core/src/commands/init.rs b/crates/rustic_core/src/commands/init.rs
new file mode 100644
index 0000000..13d7f11
--- /dev/null
+++ b/crates/rustic_core/src/commands/init.rs
@@ -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(
+ repo: &Repository
,
+ 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
(
+ repo: &Repository
,
+ pass: &str,
+ key_opts: &KeyOpts,
+ config: &ConfigFile,
+) -> RusticResult {
+ 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)
+}
diff --git a/crates/rustic_core/src/commands/key.rs b/crates/rustic_core/src/commands/key.rs
new file mode 100644
index 0000000..cbc72b7
--- /dev/null
+++ b/crates/rustic_core/src/commands/key.rs
@@ -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,
+
+ /// Set 'username' in public key information
+ #[cfg_attr(feature = "clap", clap(long))]
+ pub username: Option,
+
+ /// Add 'created' date in public key information
+ #[cfg_attr(feature = "clap", clap(long))]
+ pub with_created: bool,
+}
+
+impl KeyOpts {
+ pub(crate) fn add_key(
+ &self,
+ repo: &Repository
,
+ pass: &str,
+ ) -> RusticResult {
+ let key = repo.key();
+ self.add(repo, pass, *key)
+ }
+
+ pub(crate) fn init_key(
+ &self,
+ repo: &Repository
,
+ pass: &str,
+ ) -> RusticResult<(Key, Id)> {
+ // generate key
+ let key = Key::new();
+ Ok((key, self.add(repo, pass, key)?))
+ }
+
+ fn add
(&self, repo: &Repository
, pass: &str, key: Key) -> RusticResult {
+ 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)
+ }
+}
diff --git a/crates/rustic_core/src/error.rs b/crates/rustic_core/src/error.rs
index 13be3e1..c8a51ba 100644
--- a/crates/rustic_core/src/error.rs
+++ b/crates/rustic_core/src/error.rs
@@ -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),
+ /// 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),
+ /// 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
diff --git a/crates/rustic_core/src/lib.rs b/crates/rustic_core/src/lib.rs
index a876747..51b9d3a 100644
--- a/crates/rustic_core/src/lib.rs
+++ b/crates/rustic_core/src/lib.rs
@@ -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},
},
diff --git a/crates/rustic_core/src/repository.rs b/crates/rustic_core/src/repository.rs
index 3e71552..2fd430e 100644
--- a/crates/rustic_core/src/repository.rs
+++ b/crates/rustic_core/src/repository.rs
@@ -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 {
name: String,
pub be: HotColdBackend,
@@ -231,7 +228,8 @@ impl Repository
{
status: (),
})
}
-
+}
+impl
Repository
{
pub fn password(&self) -> RusticResult> {
match (
&self.opts.password,
@@ -279,17 +277,32 @@ impl Repository
{
}
}
- pub fn open(self) -> RusticResult> {
+ pub fn config_id(&self) -> RusticResult> {
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> {
+ let password = self
+ .password()?
+ .ok_or(RepositoryErrorKind::NoPasswordGiven)?;
+ self.open_with_password(&password)
+ }
+
+ pub fn open_with_password(self, password: &str) -> RusticResult> {
+ 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 Repository
{
}
}
- 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> {
+ 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> {
+ 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> {
+ 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> {
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 Repository {
}
}
-pub(crate) fn get_key(be: &impl ReadBackend, password: Option) -> RusticResult {
- 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 Repository
{
+ pub fn cat_file(&self, tpe: FileType, id: &str) -> RusticResult {
+ commands::cat::cat_file(self, tpe, id)
+ }
+
+ pub fn add_key(&self, pass: &str, opts: &KeyOpts) -> RusticResult {
+ opts.add_key(self, pass)
+ }
+
+ pub fn apply_config(&self, opts: &ConfigOpts) -> RusticResult {
+ commands::config::apply_config(self, opts)
+ }
+}
+
impl Repository {
pub fn get_snapshot_group(
&self,
@@ -460,10 +506,6 @@ impl Repository {
Ok(())
}
- pub fn cat_file(&self, tpe: FileType, id: &str) -> RusticResult {
- commands::cat::cat_file(self, tpe, id)
- }
-
pub fn check(&self, opts: CheckOpts) -> RusticResult<()> {
opts.run(self)
}
diff --git a/src/commands.rs b/src/commands.rs
index 26f0b04..8b75e4d 100644
--- a/src/commands.rs
+++ b/src/commands.rs
@@ -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 for EntryPoint {
}
}
-fn open_repository(repo: Repository
) -> Repository
{
- match repo.open() {
- Ok(it) => it,
- Err(err) => {
- status_err!("{}", err);
- RUSTIC_APP.shutdown(Shutdown::Crash);
- }
- }
-}
-
-fn get_repository(config: &Arc) -> Repository {
+fn open_repository(config: &Arc) -> Result> {
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]
diff --git a/src/commands/backup.rs b/src/commands/backup.rs
index 6a81ea0..ef11bfa 100644
--- a/src/commands/backup.rs
+++ b/src/commands/backup.rs
@@ -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() {
diff --git a/src/commands/cat.rs b/src/commands/cat.rs
index d590df3..63c4579 100644
--- a/src/commands/cat.rs
+++ b/src/commands/cat.rs
@@ -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, "")?,
diff --git a/src/commands/check.rs b/src/commands/check.rs
index 5cdd90c..3812148 100644
--- a/src/commands/check.rs
+++ b/src/commands/check.rs
@@ -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(())
+ }
+}
diff --git a/src/commands/config.rs b/src/commands/config.rs
index 3e13d00..7d882dd 100644
--- a/src/commands/config.rs
+++ b/src/commands/config.rs
@@ -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 .
- /// Note that 0 equals to no compression
- #[clap(long, value_name = "LEVEL")]
- pub(crate) set_compression: Option,
-
- /// Set repository version. Allowed versions: 1,2
- #[clap(long, value_name = "VERSION")]
- pub(crate) set_version: Option,
-
- /// 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,
-
- /// 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,
-
- /// 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,
-
- /// 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,
-
- /// 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,
-
- /// 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,
-
- /// 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,
-
- /// 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,
-}
-
-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(())
diff --git a/src/commands/copy.rs b/src/commands/copy.rs
index 32380ba..316b906 100644
--- a/src/commands/copy.rs
+++ b/src/commands/copy.rs
@@ -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)!");
diff --git a/src/commands/diff.rs b/src/commands/diff.rs
index 0614718..b4be360 100644
--- a/src/commands/diff.rs
+++ b/src/commands/diff.rs
@@ -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, "");
diff --git a/src/commands/dump.rs b/src/commands/dump.rs
index 451dd91..f74fa5f 100644
--- a/src/commands/dump.rs
+++ b/src/commands/dump.rs
@@ -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))?;
diff --git a/src/commands/forget.rs b/src/commands/forget.rs
index 1313eef..1d99a99 100644
--- a/src/commands/forget.rs
+++ b/src/commands/forget.rs
@@ -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();
diff --git a/src/commands/init.rs b/src/commands/init.rs
index 2470034..b13763b 100644
--- a/src/commands/init.rs
+++ b/src/commands/init.rs
@@ -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,
- key_opts: KeyOpts,
- password: Option,
+pub(crate) fn init(
+ repo: Repository
,
+ 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(())
}
diff --git a/src/commands/key.rs b/src/commands/key.rs
index 132558c..62ebc53 100644
--- a/src/commands/key.rs
+++ b/src/commands/key.rs
@@ -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,
+ pub(crate) new_password_file: Option,
#[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,
-
- /// Set 'username' in public key information
- #[clap(long)]
- pub(crate) username: Option,
-
- /// 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(())
}
diff --git a/src/commands/list.rs b/src/commands/list.rs
index eae7d51..798c96a 100644
--- a/src/commands/list.rs
+++ b/src/commands/list.rs
@@ -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
diff --git a/src/commands/ls.rs b/src/commands/ls.rs
index d45deee..9714f1b 100644
--- a/src/commands/ls.rs
+++ b/src/commands/ls.rs
@@ -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;
diff --git a/src/commands/merge.rs b/src/commands/merge.rs
index e2156fb..2a4c0de 100644
--- a/src/commands/merge.rs
+++ b/src/commands/merge.rs
@@ -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();
diff --git a/src/commands/prune.rs b/src/commands/prune.rs
index c58a659..2f3835b 100644
--- a/src/commands/prune.rs
+++ b/src/commands/prune.rs
@@ -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)?;
diff --git a/src/commands/repair.rs b/src/commands/repair.rs
index 775109d..62c8995 100644
--- a/src/commands/repair.rs
+++ b/src/commands/repair.rs
@@ -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();
diff --git a/src/commands/repoinfo.rs b/src/commands/repoinfo.rs
index 18d715e..7d43f55 100644
--- a/src/commands/repoinfo.rs
+++ b/src/commands/repoinfo.rs
@@ -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()?,
};
diff --git a/src/commands/restore.rs b/src/commands/restore.rs
index 3a9c750..5f9924b 100644
--- a/src/commands/restore.rs
+++ b/src/commands/restore.rs
@@ -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, ""));
diff --git a/src/commands/snapshots.rs b/src/commands/snapshots.rs
index f1c3aed..0971505 100644
--- a/src/commands/snapshots.rs
+++ b/src/commands/snapshots.rs
@@ -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)
diff --git a/src/commands/tag.rs b/src/commands/tag.rs
index 1836884..93e421b 100644
--- a/src/commands/tag.rs
+++ b/src/commands/tag.rs
@@ -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();
diff --git a/tests/backup_restore.rs b/tests/backup_restore.rs
index fe07cb9..4199bea 100644
--- a/tests/backup_restore.rs
+++ b/tests/backup_restore.rs
@@ -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 {
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)?;