use derive_setters::Setters;
use std::{collections::BTreeMap, sync::Arc};
use strum::{Display, EnumString};
use rustic_core::{ErrorKind, RepositoryBackends, RusticError, RusticResult, WriteBackend};
use crate::{
local::LocalBackend,
util::{BackendLocation, location_to_type_and_path},
};
#[cfg(feature = "opendal")]
use crate::opendal::OpenDALBackend;
#[cfg(feature = "rclone")]
use crate::rclone::RcloneBackend;
#[cfg(feature = "rest")]
use crate::rest::RestBackend;
#[cfg(feature = "clap")]
use clap::ValueHint;
#[cfg_attr(feature = "clap", derive(clap::Parser))]
#[cfg_attr(feature = "merge", derive(conflate::Merge))]
#[derive(Clone, Default, Debug, serde::Deserialize, serde::Serialize, Setters)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
#[setters(into, strip_option)]
#[non_exhaustive]
pub struct BackendOptions {
#[cfg_attr(
feature = "clap",
clap(short, long, global = true, visible_alias = "repo", env = "RUSTIC_REPOSITORY", value_hint = ValueHint::DirPath)
)]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub repository: Option<String>,
#[cfg_attr(
feature = "clap",
clap(long, global = true, alias = "repository_hot", env = "RUSTIC_REPO_HOT")
)]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub repo_hot: Option<String>,
#[cfg_attr(feature = "clap", clap(skip))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::btreemap::append_or_ignore))]
pub options: BTreeMap<String, String>,
#[cfg_attr(feature = "clap", clap(skip))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::btreemap::append_or_ignore))]
pub options_hot: BTreeMap<String, String>,
#[cfg_attr(feature = "clap", clap(skip))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::btreemap::append_or_ignore))]
pub options_cold: BTreeMap<String, String>,
}
impl BackendOptions {
pub fn to_backends(&self) -> RusticResult<RepositoryBackends> {
let mut options = self.options.clone();
options.extend(self.options_cold.clone());
let be = self
.get_backend(self.repository.as_ref(), options)?
.ok_or_else(|| {
RusticError::new(
ErrorKind::Backend,
"No repository given. Please make sure, that you have set the repository.",
)
})?;
let mut options = self.options.clone();
options.extend(self.options_hot.clone());
let be_hot = self.get_backend(self.repo_hot.as_ref(), options)?;
Ok(RepositoryBackends::new(be, be_hot))
}
#[allow(clippy::unused_self)]
fn get_backend(
&self,
repo_string: Option<&String>,
options: BTreeMap<String, String>,
) -> RusticResult<Option<Arc<dyn WriteBackend>>> {
repo_string
.map(|string| {
let (be_type, location) = location_to_type_and_path(string)?;
be_type
.to_backend(location.clone(), options.into())
.map_err(|err| {
err
.prepend_guidance_line("Could not load the backend `{name}` at `{location}`. Please check the given backend and try again.")
.attach_context("name", be_type.to_string())
.attach_context("location", location.to_string())
})
})
.transpose()
}
}
pub trait BackendChoice {
fn to_backend(
&self,
location: BackendLocation,
options: Option<BTreeMap<String, String>>,
) -> RusticResult<Arc<dyn WriteBackend>>;
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, Display)]
pub enum SupportedBackend {
#[strum(serialize = "local", to_string = "Local Backend")]
Local,
#[cfg(feature = "rclone")]
#[strum(serialize = "rclone", to_string = "rclone Backend")]
Rclone,
#[cfg(feature = "rest")]
#[strum(serialize = "rest", to_string = "REST Backend")]
Rest,
#[cfg(feature = "opendal")]
#[strum(serialize = "opendal", to_string = "openDAL Backend")]
OpenDAL,
}
impl BackendChoice for SupportedBackend {
fn to_backend(
&self,
location: BackendLocation,
options: Option<BTreeMap<String, String>>,
) -> RusticResult<Arc<dyn WriteBackend>> {
let options = options.unwrap_or_default();
Ok(match self {
Self::Local => Arc::new(LocalBackend::new(location, options)?),
#[cfg(feature = "rclone")]
Self::Rclone => Arc::new(RcloneBackend::new(location, options)?),
#[cfg(feature = "rest")]
Self::Rest => Arc::new(RestBackend::new(location, options)?),
#[cfg(feature = "opendal")]
Self::OpenDAL => Arc::new(OpenDALBackend::new(location, options)?),
})
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case("local", SupportedBackend::Local)]
#[cfg(feature = "rclone")]
#[case("rclone", SupportedBackend::Rclone)]
#[cfg(feature = "rest")]
#[case("rest", SupportedBackend::Rest)]
#[cfg(feature = "opendal")]
#[case("opendal", SupportedBackend::OpenDAL)]
fn test_try_from_is_ok(#[case] input: &str, #[case] expected: SupportedBackend) {
assert_eq!(SupportedBackend::try_from(input).unwrap(), expected);
}
#[test]
fn test_try_from_unknown_is_err() {
assert!(SupportedBackend::try_from("unknown").is_err());
}
}