Skip to main content

coven_storage/cloud/
factory.rs

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    /// Open the configured provider's home, counting every operation asked of
134    /// it.
135    ///
136    /// Every home a running installation works through is built here, so
137    /// wrapping here is what makes each stage of a live run able to say how
138    /// many provider operations it cost. The homes tests construct directly are
139    /// not wrapped and count nothing, and the runs over them report their times
140    /// alone.
141    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;