use bytesize::ByteSize;
use derive_setters::Setters;
use crate::{
backend::decrypt::{DecryptBackend, DecryptWriteBackend},
chunker::rabin::check_rabin_params,
crypto::CryptoKey,
error::{ErrorKind, RusticError, RusticResult},
repofile::{ConfigFile, configfile::Chunker},
repository::{Open, Repository},
};
pub(crate) fn apply_config<S: Open>(
repo: &mut Repository<S>,
opts: &ConfigOptions,
) -> RusticResult<bool> {
if repo.config().append_only == Some(true) && opts.set_append_only != Some(false) {
return Err(RusticError::new(
ErrorKind::AppendOnly,
"Changing config is not allowed in append-only repositories. Please disable append-only mode first, if you know what you are doing. Aborting.",
));
}
let mut new_config = repo.config().clone();
opts.apply(&mut new_config)?;
if &new_config == repo.config() {
Ok(false)
} else {
repo.set_config(new_config.clone());
save_config(repo, new_config, *repo.dbe().key())?;
Ok(true)
}
}
pub(crate) fn save_config<S>(
repo: &Repository<S>,
mut new_config: ConfigFile,
key: impl CryptoKey,
) -> RusticResult<()> {
new_config.is_hot = None;
let dbe = DecryptBackend::new(repo.be.clone(), key);
_ = dbe.save_file_uncompressed(&new_config)?;
save_config_hot(repo, new_config, key)
}
pub(crate) fn save_config_hot<S>(
repo: &Repository<S>,
mut new_config: ConfigFile,
key: impl CryptoKey,
) -> RusticResult<()> {
if let Some(hot_be) = repo.be_hot.clone() {
let dbe = DecryptBackend::new(hot_be.clone(), key);
new_config.is_hot = Some(true);
_ = dbe.save_file_uncompressed(&new_config)?;
}
Ok(())
}
#[cfg_attr(feature = "clap", derive(clap::Parser))]
#[derive(Debug, Clone, Copy, Default, Setters)]
#[setters(into)]
#[non_exhaustive]
pub struct ConfigOptions {
#[cfg_attr(feature = "clap", clap(long, value_name = "VERSION"))]
pub set_version: Option<u32>,
#[cfg_attr(feature = "clap", clap(long, value_name = "CHUNKER"))]
pub set_chunker: Option<Chunker>,
#[cfg_attr(feature = "clap", clap(long, value_name = "SIZE"))]
pub set_chunk_size: Option<ByteSize>,
#[cfg_attr(feature = "clap", clap(long, value_name = "SIZE"))]
pub set_chunk_min_size: Option<ByteSize>,
#[cfg_attr(feature = "clap", clap(long, value_name = "SIZE"))]
pub set_chunk_max_size: Option<ByteSize>,
#[cfg_attr(feature = "clap", clap(long, value_name = "LEVEL"))]
pub set_compression: Option<i32>,
#[cfg_attr(feature = "clap", clap(long))]
pub set_append_only: Option<bool>,
#[cfg_attr(feature = "clap", clap(long, value_name = "SIZE"))]
pub set_treepack_size: Option<ByteSize>,
#[cfg_attr(feature = "clap", clap(long, value_name = "SIZE"))]
pub set_treepack_size_limit: Option<ByteSize>,
#[cfg_attr(feature = "clap", clap(long, value_name = "FACTOR"))]
pub set_treepack_growfactor: Option<u32>,
#[cfg_attr(feature = "clap", clap(long, value_name = "SIZE"))]
pub set_datapack_size: Option<ByteSize>,
#[cfg_attr(feature = "clap", clap(long, value_name = "FACTOR"))]
pub set_datapack_growfactor: Option<u32>,
#[cfg_attr(feature = "clap", clap(long, value_name = "SIZE"))]
pub set_datapack_size_limit: Option<ByteSize>,
#[cfg_attr(feature = "clap", clap(long, value_name = "PERCENT"))]
pub set_min_packsize_tolerate_percent: Option<u32>,
#[cfg_attr(feature = "clap", clap(long, value_name = "PERCENT"))]
pub set_max_packsize_tolerate_percent: Option<u32>,
#[cfg_attr(feature = "clap", clap(long))]
pub set_extra_verify: Option<bool>,
}
impl ConfigOptions {
#[allow(clippy::too_many_lines)]
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(RusticError::new(
ErrorKind::Unsupported,
"Config version unsupported. Allowed versions are `{allowed_versions}`. You provided `{current_version}`. Please use a supported version. ",
)
.attach_context("current_version", version.to_string())
.attach_context("allowed_versions", format!("{range:?}")));
} else if version < config.version {
return Err(RusticError::new(
ErrorKind::Unsupported,
"Downgrading config version is unsupported. You provided `{new_version}` which is smaller than `{current_version}`. Please use a version that is greater or equal to the current one.",
)
.attach_context("current_version", config.version.to_string())
.attach_context("new_version", version.to_string()));
}
config.version = version;
}
if let Some(chunker) = self.set_chunker {
config.chunker = Some(chunker);
}
if let Some(size) = self.set_chunk_size {
let chunk_size: usize = size
.as_u64()
.try_into()
.map_err(|err| construct_size_too_large_error(err, size))?;
config.chunk_size = Some(chunk_size);
}
if let Some(size) = self.set_chunk_min_size {
let chunk_min_size = size
.as_u64()
.try_into()
.map_err(|err| construct_size_too_large_error(err, size))?;
config.chunk_min_size = Some(chunk_min_size);
}
if let Some(size) = self.set_chunk_max_size {
let chunk_max_size = size
.as_u64()
.try_into()
.map_err(|err| construct_size_too_large_error(err, size))?;
config.chunk_max_size = Some(chunk_max_size);
}
if matches!(config.chunker(), Chunker::Rabin) {
check_rabin_params(
config.chunk_size(),
config.chunk_min_size(),
config.chunk_max_size(),
)?;
}
if let Some(compression) = self.set_compression {
if config.version == 1 && compression != 0 {
return Err(RusticError::new(
ErrorKind::Unsupported,
"Compression `{compression}` unsupported for v1 repos.",
)
.attach_context("compression", compression.to_string()));
}
let range = zstd::compression_level_range();
if !range.contains(&compression) {
return Err(RusticError::new(
ErrorKind::Unsupported,
"Compression level `{compression}` is unsupported. Allowed levels are `{allowed_levels}`. Please use a supported level.",
)
.attach_context("compression", compression.to_string())
.attach_context("allowed_levels", format!("{range:?}")));
}
config.compression = Some(compression);
}
if let Some(append_only) = self.set_append_only {
config.append_only = Some(append_only);
}
if let Some(size) = self.set_treepack_size {
config.treepack_size = Some(
size.as_u64()
.try_into()
.map_err(|err| construct_size_too_large_error(err, 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(|err| construct_size_too_large_error(err, size))?,
);
}
if let Some(size) = self.set_datapack_size {
config.datapack_size = Some(
size.as_u64()
.try_into()
.map_err(|err| construct_size_too_large_error(err, 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(|err| construct_size_too_large_error(err, size))?,
);
}
if let Some(percent) = self.set_min_packsize_tolerate_percent {
if percent > 100 {
return Err(RusticError::new(
ErrorKind::InvalidInput,
"`min_packsize_tolerate_percent` must be <= 100. You provided `{percent}`.",
)
.attach_context("percent", percent.to_string()));
}
config.min_packsize_tolerate_percent = Some(percent);
}
if let Some(percent) = self.set_max_packsize_tolerate_percent {
if percent < 100 && percent > 0 {
return Err(RusticError::new(
ErrorKind::InvalidInput,
"`max_packsize_tolerate_percent` must be >= 100 or 0. You provided `{percent}`.",
)
.attach_context("percent", percent.to_string()));
}
config.max_packsize_tolerate_percent = Some(percent);
}
config.extra_verify = self.set_extra_verify;
Ok(())
}
}
fn construct_size_too_large_error(
err: std::num::TryFromIntError,
size: ByteSize,
) -> Box<RusticError> {
RusticError::with_source(
ErrorKind::Internal,
"Failed to convert ByteSize `{size}` to u64. Size is too large.",
err,
)
.attach_context("size", size.to_string())
}