1use crate::error::StorageError;
25use crate::providers::disk::DiskService;
26use crate::providers::memory::MemoryService;
27use crate::service::Service;
28use doido_core::Result;
29use serde::{Deserialize, Deserializer};
30use std::collections::HashMap;
31use std::sync::Arc;
32
33#[derive(Debug, Clone, Default, PartialEq, Eq)]
35pub enum ServiceBackend {
36 #[default]
38 Disk,
39 Memory,
41 S3,
43 R2,
45 Azure,
47 Gcs,
49 Custom(String),
52}
53
54impl ServiceBackend {
55 fn from_kind(kind: &str) -> Self {
58 match kind {
59 "disk" => ServiceBackend::Disk,
60 "memory" => ServiceBackend::Memory,
61 "s3" => ServiceBackend::S3,
62 "r2" => ServiceBackend::R2,
63 "azure" => ServiceBackend::Azure,
64 "gcs" | "google" => ServiceBackend::Gcs,
65 other => ServiceBackend::Custom(other.to_string()),
66 }
67 }
68}
69
70impl<'de> Deserialize<'de> for ServiceBackend {
71 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
72 let kind = String::deserialize(deserializer)?;
73 Ok(ServiceBackend::from_kind(&kind))
74 }
75}
76
77#[derive(Debug, Clone, Default, Deserialize)]
80pub struct ServiceConfig {
81 #[serde(default, rename = "type")]
83 pub backend: ServiceBackend,
84 #[serde(default)]
86 pub public: bool,
87
88 #[serde(default)]
90 pub root: Option<String>,
91
92 #[serde(default)]
94 pub bucket: Option<String>,
95 #[serde(default)]
97 pub region: Option<String>,
98 #[serde(default)]
100 pub endpoint: Option<String>,
101 #[serde(default)]
103 pub access_key_id: Option<String>,
104 #[serde(default)]
106 pub secret_access_key: Option<String>,
107
108 #[serde(default)]
110 pub container: Option<String>,
111 #[serde(default)]
113 pub account: Option<String>,
114 #[serde(default)]
116 pub access_key: Option<String>,
117
118 #[serde(flatten)]
121 pub options: HashMap<String, serde_norway::Value>,
122}
123
124impl ServiceConfig {
125 pub fn option_str(&self, key: &str) -> Option<&str> {
127 self.options.get(key).and_then(|v| v.as_str())
128 }
129
130 pub async fn build(&self, name: &str) -> Result<Arc<dyn Service>> {
132 match &self.backend {
133 ServiceBackend::Disk => {
134 let root = self.root.clone().unwrap_or_else(|| "storage".to_string());
135 Ok(Arc::new(DiskService::new(name, root).public(self.public)))
136 }
137 ServiceBackend::Memory => Ok(Arc::new(MemoryService::new(name))),
138 ServiceBackend::S3 => self.build_s3(name, false).await,
139 ServiceBackend::R2 => self.build_s3(name, true).await,
140 ServiceBackend::Azure => self.build_azure(name).await,
141 ServiceBackend::Gcs => self.build_gcs(name).await,
142 ServiceBackend::Custom(kind) => crate::registry::build_adapter(kind, name, self),
143 }
144 }
145
146 #[cfg(feature = "storage-s3")]
147 async fn build_s3(&self, name: &str, r2: bool) -> Result<Arc<dyn Service>> {
148 Ok(Arc::new(crate::providers::s3::S3Service::connect(
149 name, self, r2,
150 )?))
151 }
152
153 #[cfg(not(feature = "storage-s3"))]
154 async fn build_s3(&self, _name: &str, r2: bool) -> Result<Arc<dyn Service>> {
155 let kind = if r2 { "r2" } else { "s3" };
156 Err(StorageError::Config(format!(
157 "storage backend '{kind}' selected in config but doido-storage was built \
158 without the `storage-s3` feature"
159 ))
160 .into())
161 }
162
163 #[cfg(feature = "storage-azure")]
164 async fn build_azure(&self, name: &str) -> Result<Arc<dyn Service>> {
165 Ok(Arc::new(
166 crate::providers::azure::AzureBlobService::connect(name, self)?,
167 ))
168 }
169
170 #[cfg(not(feature = "storage-azure"))]
171 async fn build_azure(&self, _name: &str) -> Result<Arc<dyn Service>> {
172 Err(StorageError::Config(
173 "storage backend 'azure' selected in config but doido-storage was built \
174 without the `storage-azure` feature"
175 .to_string(),
176 )
177 .into())
178 }
179
180 #[cfg(feature = "storage-gcs")]
181 async fn build_gcs(&self, name: &str) -> Result<Arc<dyn Service>> {
182 Ok(Arc::new(
183 crate::providers::gcs::GcsService::connect(name, self).await?,
184 ))
185 }
186
187 #[cfg(not(feature = "storage-gcs"))]
188 async fn build_gcs(&self, _name: &str) -> Result<Arc<dyn Service>> {
189 Err(StorageError::Config(
190 "storage backend 'gcs' selected in config but doido-storage was built \
191 without the `storage-gcs` feature"
192 .to_string(),
193 )
194 .into())
195 }
196}
197
198#[derive(Debug, Clone, Default, Deserialize)]
200pub struct StorageConfig {
201 #[serde(default)]
204 pub service: Option<String>,
205 #[serde(default)]
207 pub services: HashMap<String, ServiceConfig>,
208}
209
210impl StorageConfig {
211 pub async fn build(&self) -> Result<Arc<dyn Service>> {
214 if self.services.is_empty() {
215 return Ok(Arc::new(DiskService::new("local", "storage")));
216 }
217 let name = self
218 .service
219 .clone()
220 .or_else(|| self.services.keys().next().cloned())
221 .ok_or_else(|| StorageError::Config("no storage service selected".to_string()))?;
222 self.build_named(&name).await
223 }
224
225 pub async fn build_named(&self, name: &str) -> Result<Arc<dyn Service>> {
227 let cfg = self
228 .services
229 .get(name)
230 .ok_or_else(|| StorageError::Config(format!("unknown storage service {name:?}")))?;
231 cfg.build(name).await
232 }
233}
234
235#[derive(Debug, Clone, Default, Deserialize)]
237pub struct YamlConfig {
238 #[serde(default)]
239 pub storage: StorageConfig,
240}
241
242impl YamlConfig {
243 pub fn load() -> std::io::Result<Self> {
245 Self::load_env(doido_core::Environment::get_env())
246 }
247
248 pub fn load_env(env: doido_core::Environment) -> std::io::Result<Self> {
250 let path = format!("config/{}.yml", env.as_str());
251 let contents = std::fs::read_to_string(&path)?;
252 Self::from_yaml(&contents)
253 }
254
255 pub fn from_yaml(yaml: &str) -> std::io::Result<Self> {
257 serde_norway::from_str(yaml)
258 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
259 }
260}
261
262pub fn load() -> StorageConfig {
265 YamlConfig::load().map(|c| c.storage).unwrap_or_default()
266}