use crate::error::StorageError;
use crate::providers::disk::DiskService;
use crate::providers::memory::MemoryService;
use crate::service::Service;
use doido_core::Result;
use serde::{Deserialize, Deserializer};
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum ServiceBackend {
#[default]
Disk,
Memory,
S3,
R2,
Azure,
Gcs,
Custom(String),
}
impl ServiceBackend {
fn from_kind(kind: &str) -> Self {
match kind {
"disk" => ServiceBackend::Disk,
"memory" => ServiceBackend::Memory,
"s3" => ServiceBackend::S3,
"r2" => ServiceBackend::R2,
"azure" => ServiceBackend::Azure,
"gcs" | "google" => ServiceBackend::Gcs,
other => ServiceBackend::Custom(other.to_string()),
}
}
}
impl<'de> Deserialize<'de> for ServiceBackend {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
let kind = String::deserialize(deserializer)?;
Ok(ServiceBackend::from_kind(&kind))
}
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct ServiceConfig {
#[serde(default, rename = "type")]
pub backend: ServiceBackend,
#[serde(default)]
pub public: bool,
#[serde(default)]
pub root: Option<String>,
#[serde(default)]
pub bucket: Option<String>,
#[serde(default)]
pub region: Option<String>,
#[serde(default)]
pub endpoint: Option<String>,
#[serde(default)]
pub access_key_id: Option<String>,
#[serde(default)]
pub secret_access_key: Option<String>,
#[serde(default)]
pub container: Option<String>,
#[serde(default)]
pub account: Option<String>,
#[serde(default)]
pub access_key: Option<String>,
#[serde(flatten)]
pub options: HashMap<String, serde_norway::Value>,
}
impl ServiceConfig {
pub fn option_str(&self, key: &str) -> Option<&str> {
self.options.get(key).and_then(|v| v.as_str())
}
pub async fn build(&self, name: &str) -> Result<Arc<dyn Service>> {
match &self.backend {
ServiceBackend::Disk => {
let root = self.root.clone().unwrap_or_else(|| "storage".to_string());
Ok(Arc::new(DiskService::new(name, root).public(self.public)))
}
ServiceBackend::Memory => Ok(Arc::new(MemoryService::new(name))),
ServiceBackend::S3 => self.build_s3(name, false).await,
ServiceBackend::R2 => self.build_s3(name, true).await,
ServiceBackend::Azure => self.build_azure(name).await,
ServiceBackend::Gcs => self.build_gcs(name).await,
ServiceBackend::Custom(kind) => crate::registry::build_adapter(kind, name, self),
}
}
#[cfg(feature = "storage-s3")]
async fn build_s3(&self, name: &str, r2: bool) -> Result<Arc<dyn Service>> {
Ok(Arc::new(crate::providers::s3::S3Service::connect(
name, self, r2,
)?))
}
#[cfg(not(feature = "storage-s3"))]
async fn build_s3(&self, _name: &str, r2: bool) -> Result<Arc<dyn Service>> {
let kind = if r2 { "r2" } else { "s3" };
Err(StorageError::Config(format!(
"storage backend '{kind}' selected in config but doido-storage was built \
without the `storage-s3` feature"
))
.into())
}
#[cfg(feature = "storage-azure")]
async fn build_azure(&self, name: &str) -> Result<Arc<dyn Service>> {
Ok(Arc::new(
crate::providers::azure::AzureBlobService::connect(name, self)?,
))
}
#[cfg(not(feature = "storage-azure"))]
async fn build_azure(&self, _name: &str) -> Result<Arc<dyn Service>> {
Err(StorageError::Config(
"storage backend 'azure' selected in config but doido-storage was built \
without the `storage-azure` feature"
.to_string(),
)
.into())
}
#[cfg(feature = "storage-gcs")]
async fn build_gcs(&self, name: &str) -> Result<Arc<dyn Service>> {
Ok(Arc::new(
crate::providers::gcs::GcsService::connect(name, self).await?,
))
}
#[cfg(not(feature = "storage-gcs"))]
async fn build_gcs(&self, _name: &str) -> Result<Arc<dyn Service>> {
Err(StorageError::Config(
"storage backend 'gcs' selected in config but doido-storage was built \
without the `storage-gcs` feature"
.to_string(),
)
.into())
}
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct StorageConfig {
#[serde(default)]
pub service: Option<String>,
#[serde(default)]
pub services: HashMap<String, ServiceConfig>,
}
impl StorageConfig {
pub async fn build(&self) -> Result<Arc<dyn Service>> {
if self.services.is_empty() {
return Ok(Arc::new(DiskService::new("local", "storage")));
}
let name = self
.service
.clone()
.or_else(|| self.services.keys().next().cloned())
.ok_or_else(|| StorageError::Config("no storage service selected".to_string()))?;
self.build_named(&name).await
}
pub async fn build_named(&self, name: &str) -> Result<Arc<dyn Service>> {
let cfg = self
.services
.get(name)
.ok_or_else(|| StorageError::Config(format!("unknown storage service {name:?}")))?;
cfg.build(name).await
}
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct YamlConfig {
#[serde(default)]
pub storage: StorageConfig,
}
impl YamlConfig {
pub fn load() -> std::io::Result<Self> {
Self::load_env(doido_core::Environment::get_env())
}
pub fn load_env(env: doido_core::Environment) -> std::io::Result<Self> {
let path = format!("config/{}.yml", env.as_str());
let contents = std::fs::read_to_string(&path)?;
Self::from_yaml(&contents)
}
pub fn from_yaml(yaml: &str) -> std::io::Result<Self> {
serde_norway::from_str(yaml)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
}
pub fn load() -> StorageConfig {
YamlConfig::load().map(|c| c.storage).unwrap_or_default()
}