1use super::runtime::{CloudRuntime, CloudRuntimeError};
2use super::{cloudkit, CloudHomeError, ExactCloudHome};
3
4use std::sync::Arc;
5
6use coven_keys::keys::{CloudHomeCredentialCustody, CloudHomeCredentials};
7
8#[derive(Clone)]
9pub struct CloudHomeFactory {
10 oauth_clients: crate::oauth::OAuthClients,
11 runtime: CloudRuntime,
12}
13
14#[cfg(feature = "oauth-providers")]
15pub struct PreparedOAuthCloudHome {
16 pub cloud_home: coven_foundation::config::CloudHomeConfig,
17 pub credentials: CloudHomeCredentials,
18}
19
20impl CloudHomeFactory {
21 pub fn new(oauth_clients: crate::oauth::OAuthClients) -> Self {
22 Self {
23 oauth_clients,
24 runtime: CloudRuntime::new(),
25 }
26 }
27
28 pub async fn execute<T, E, F>(
29 &self,
30 operation: impl FnOnce() -> F + Send + 'static,
31 ) -> Result<Result<T, E>, CloudRuntimeError>
32 where
33 T: Send + 'static,
34 E: Send + 'static,
35 F: std::future::Future<Output = Result<T, E>> + Send + 'static,
36 {
37 self.runtime.run(operation).await
38 }
39
40 #[allow(clippy::too_many_arguments)]
41 pub async fn open_s3(
42 &self,
43 bucket: String,
44 region: String,
45 endpoint: Option<String>,
46 access_key: String,
47 secret_key: String,
48 key_prefix: Option<String>,
49 exact_upload_verification: coven_foundation::config::ExactUploadVerification,
50 clock: coven_foundation::clock::ClockRef,
51 ) -> Result<super::s3::S3CloudHome, CloudHomeError> {
52 super::s3::open_cloud_home(
53 self.runtime.clone(),
54 bucket,
55 region,
56 endpoint,
57 access_key,
58 secret_key,
59 key_prefix,
60 exact_upload_verification,
61 clock,
62 )
63 .await
64 }
65
66 #[cfg(feature = "oauth-providers")]
67 pub fn oauth_config_for(
68 &self,
69 provider: coven_foundation::config::CloudProvider,
70 ) -> Result<crate::oauth::OAuthConfig, crate::oauth::OAuthClientCredsError> {
71 self.oauth_clients.config_for(provider)
72 }
73
74 #[cfg(feature = "oauth-providers")]
75 pub async fn prepare_oauth_cloud_home(
76 &self,
77 mut cloud_home: coven_foundation::config::CloudHomeConfig,
78 store_name: &str,
79 cancel: tokio::sync::watch::Receiver<bool>,
80 clock: &dyn coven_foundation::clock::Clock,
81 ) -> Result<PreparedOAuthCloudHome, crate::cloud::SetupError> {
82 use crate::oauth::OAuthCloudHomeLocation;
83 use coven_foundation::config::CloudProvider;
84
85 let prepared = match cloud_home.provider {
86 Some(CloudProvider::GoogleDrive) => {
87 self.oauth_clients
88 .prepare_google_drive(store_name, cancel, clock)
89 .await?
90 }
91 Some(CloudProvider::Dropbox) => {
92 self.oauth_clients
93 .prepare_dropbox(store_name, cancel, clock)
94 .await?
95 }
96 Some(CloudProvider::OneDrive) => {
97 self.oauth_clients.prepare_onedrive(cancel, clock).await?
98 }
99 Some(provider) => {
100 return Err(crate::cloud::SetupError::Configuration(format!(
101 "provider {provider:?} does not use OAuth"
102 )))
103 }
104 None => {
105 return Err(crate::cloud::SetupError::Configuration(
106 "OAuth cloud-home setup requires a provider".to_string(),
107 ))
108 }
109 };
110 match prepared.location {
111 OAuthCloudHomeLocation::GoogleDrive { folder_id } => {
112 cloud_home.google_drive_folder_id = Some(folder_id);
113 }
114 OAuthCloudHomeLocation::Dropbox { folder_path } => {
115 cloud_home.dropbox_folder_path = Some(folder_path);
116 }
117 OAuthCloudHomeLocation::OneDrive {
118 drive_id,
119 folder_id,
120 } => {
121 cloud_home.onedrive_drive_id = Some(drive_id);
122 cloud_home.onedrive_folder_id = Some(folder_id);
123 }
124 }
125 Ok(PreparedOAuthCloudHome {
126 cloud_home,
127 credentials: CloudHomeCredentials::OAuth {
128 tokens: prepared.tokens,
129 },
130 })
131 }
132
133 pub async fn create(
142 &self,
143 config: &coven_foundation::config::Config,
144 clock: coven_foundation::clock::ClockRef,
145 cloudkit_ops: Option<std::sync::Arc<dyn cloudkit::CloudKitOps>>,
146 credential_custody: Arc<dyn CloudHomeCredentialCustody>,
147 ) -> Result<Box<dyn ExactCloudHome>, CloudHomeError> {
148 Ok(Box::new(super::CountingCloudHome::new(Arc::from(
149 self.open(config, clock, cloudkit_ops, credential_custody)
150 .await?,
151 ))))
152 }
153
154 async fn open(
155 &self,
156 config: &coven_foundation::config::Config,
157 clock: coven_foundation::clock::ClockRef,
158 cloudkit_ops: Option<std::sync::Arc<dyn cloudkit::CloudKitOps>>,
159 credential_custody: Arc<dyn CloudHomeCredentialCustody>,
160 ) -> Result<Box<dyn ExactCloudHome>, CloudHomeError> {
161 use coven_foundation::config::CloudProvider;
162
163 #[cfg(not(feature = "oauth-providers"))]
164 let _ = (&clock, &self.oauth_clients);
165
166 #[cfg(feature = "oauth-providers")]
167 let oauth_tokens = |provider_name: &str| {
168 credential_custody
169 .unlock()
170 .map_err(|error| {
171 CloudHomeError::configuration(
172 format!("read {provider_name} credentials"),
173 error,
174 )
175 })?
176 .and_then(|credentials| match credentials {
177 CloudHomeCredentials::OAuth { tokens } => Some(tokens),
178 CloudHomeCredentials::S3 { .. } => None,
179 })
180 .ok_or_else(|| {
181 CloudHomeError::Configuration(format!(
182 "{provider_name} OAuth token not in keyring"
183 ))
184 })
185 };
186
187 match config.cloud_home.provider {
188 Some(CloudProvider::S3) | None => {
189 let bucket = config.cloud_home.s3_bucket.clone().ok_or_else(|| {
190 CloudHomeError::Configuration("S3 bucket not configured".to_string())
191 })?;
192 let region = config.cloud_home.s3_region.clone().ok_or_else(|| {
193 CloudHomeError::Configuration("S3 region not configured".to_string())
194 })?;
195 let endpoint = config.cloud_home.s3_endpoint.clone();
196
197 let (access_key, secret_key) = match credential_custody
198 .unlock()
199 .map_err(|error| CloudHomeError::configuration("read S3 credentials", error))?
200 {
201 Some(CloudHomeCredentials::S3 {
202 access_key,
203 secret_key,
204 }) => (access_key, secret_key),
205 _ => {
206 return Err(CloudHomeError::Configuration(
207 "S3 credentials not in keyring".to_string(),
208 ));
209 }
210 };
211
212 let s3 = self
213 .open_s3(
214 bucket,
215 region,
216 endpoint,
217 access_key,
218 secret_key,
219 config.cloud_home.s3_key_prefix.clone(),
220 config.cloud_home.exact_upload_verification,
221 clock.clone(),
222 )
223 .await?;
224 Ok(Box::new(s3))
225 }
226 #[cfg(feature = "oauth-providers")]
227 Some(CloudProvider::GoogleDrive) => {
228 let folder_id = config
229 .cloud_home
230 .google_drive_folder_id
231 .clone()
232 .ok_or_else(|| {
233 CloudHomeError::Configuration(
234 "Google Drive folder ID not configured".to_string(),
235 )
236 })?;
237 let tokens = oauth_tokens("Google Drive")?;
238 let oauth_config = self
239 .oauth_clients
240 .config_for(CloudProvider::GoogleDrive)
241 .map_err(|error| {
242 CloudHomeError::configuration(
243 "read Google Drive OAuth configuration",
244 error,
245 )
246 })?;
247 let session = super::oauth_session::OAuthSession::new(
248 tokens,
249 credential_custody.clone(),
250 clock,
251 oauth_config,
252 "Google Drive",
253 );
254 Ok(Box::new(super::google_drive::GoogleDriveCloudHome::new(
255 folder_id,
256 session,
257 config.cloud_home.exact_upload_verification,
258 )))
259 }
260 #[cfg(feature = "oauth-providers")]
261 Some(CloudProvider::Dropbox) => {
262 let folder_path =
263 config
264 .cloud_home
265 .dropbox_folder_path
266 .clone()
267 .ok_or_else(|| {
268 CloudHomeError::Configuration(
269 "Dropbox folder path not configured".to_string(),
270 )
271 })?;
272 let tokens = oauth_tokens("Dropbox")?;
273 let oauth_config = self
274 .oauth_clients
275 .config_for(CloudProvider::Dropbox)
276 .map_err(|error| {
277 CloudHomeError::configuration("read Dropbox OAuth configuration", error)
278 })?;
279 let session = super::oauth_session::OAuthSession::new(
280 tokens,
281 credential_custody.clone(),
282 clock,
283 oauth_config,
284 "Dropbox",
285 );
286 Ok(Box::new(super::dropbox::DropboxCloudHome::new(
287 folder_path,
288 session,
289 config.cloud_home.exact_upload_verification,
290 )))
291 }
292 #[cfg(feature = "oauth-providers")]
293 Some(CloudProvider::OneDrive) => {
294 let drive_id = config.cloud_home.onedrive_drive_id.clone().ok_or_else(|| {
295 CloudHomeError::Configuration("OneDrive drive ID not configured".to_string())
296 })?;
297 let folder_id = config
298 .cloud_home
299 .onedrive_folder_id
300 .clone()
301 .ok_or_else(|| {
302 CloudHomeError::Configuration(
303 "OneDrive folder ID not configured".to_string(),
304 )
305 })?;
306 let tokens = oauth_tokens("OneDrive")?;
307 let oauth_config = self
308 .oauth_clients
309 .config_for(CloudProvider::OneDrive)
310 .map_err(|error| {
311 CloudHomeError::configuration("read OneDrive OAuth configuration", error)
312 })?;
313 let session = super::oauth_session::OAuthSession::new(
314 tokens,
315 credential_custody.clone(),
316 clock,
317 oauth_config,
318 "OneDrive",
319 );
320 Ok(Box::new(super::onedrive::OneDriveCloudHome::new(
321 drive_id,
322 folder_id,
323 session,
324 config.cloud_home.exact_upload_verification,
325 )))
326 }
327 #[cfg(not(feature = "oauth-providers"))]
328 Some(CloudProvider::GoogleDrive | CloudProvider::Dropbox | CloudProvider::OneDrive) => {
329 Err(CloudHomeError::Configuration(
330 "OAuth cloud providers are not supported in this build".to_string(),
331 ))
332 }
333 Some(CloudProvider::CloudKit) => {
334 let ops = cloudkit_ops.ok_or_else(|| {
335 CloudHomeError::Configuration("CloudKit driver not provided".to_string())
336 })?;
337 match (
338 config.cloud_home.cloudkit_owner_name.as_ref(),
339 config.cloud_home.cloudkit_zone_name.as_ref(),
340 ) {
341 (None, None) => Ok(Box::new(cloudkit::CloudKitCloudHome::new_private(
342 ops,
343 config.cloud_home.exact_upload_verification,
344 ))),
345 (Some(owner_name), Some(zone_name)) => {
346 Ok(Box::new(cloudkit::CloudKitCloudHome::new_shared(
347 ops,
348 owner_name.clone(),
349 zone_name.clone(),
350 config.cloud_home.exact_upload_verification,
351 )))
352 }
353 _ => Err(CloudHomeError::Configuration(
354 "CloudKit share config requires both cloudkit_owner_name and cloudkit_zone_name"
355 .to_string(),
356 )),
357 }
358 }
359 }
360 }
361}
362
363#[cfg(test)]
364mod tests;