coven_storage/cloud/
setup.rs1use coven_foundation::config::{CloudProvider, Config};
7
8#[derive(Debug, thiserror::Error)]
14pub enum StorageSetupError {
15 #[error("failed to build cloud home: {0}")]
16 CloudHome(#[from] super::CloudHomeError),
17 #[error("key error: {0}")]
18 Key(#[from] coven_keys::keys::KeyError),
19 #[error("no encryption key found for an encrypted cloud home")]
20 NoEncryptionKey,
21 #[error("{provider:?} cannot provide exact protocol and blob slots with this configuration")]
22 ExactSlotsUnavailable { provider: CloudProvider },
23}
24
25pub fn require_exact_slot_capabilities_config(config: &Config) -> Result<(), StorageSetupError> {
26 let provider = config.cloud_home.provider.clone().ok_or_else(|| {
27 super::CloudHomeError::Configuration(
28 "sync requires a cloud provider with exact-slot storage".to_string(),
29 )
30 })?;
31 if exact_slot_capabilities_supported(&provider, config.cloud_home.exact_upload_verification) {
32 Ok(())
33 } else {
34 Err(StorageSetupError::ExactSlotsUnavailable { provider })
35 }
36}
37
38pub fn require_exact_slot_capabilities_join_info(
39 join_info: &crate::cloud::CloudHomeJoinInfo,
40 verification: coven_foundation::config::ExactUploadVerification,
41) -> Result<(), CloudProvider> {
42 let provider = join_info.cloud_provider();
43 if exact_slot_capabilities_supported(&provider, verification) {
44 Ok(())
45 } else {
46 Err(provider)
47 }
48}
49
50fn exact_slot_capabilities_supported(
51 provider: &CloudProvider,
52 verification: coven_foundation::config::ExactUploadVerification,
53) -> bool {
54 !matches!(
55 verification,
56 coven_foundation::config::ExactUploadVerification::UploadChecksum
57 ) || matches!(provider, CloudProvider::S3)
58}
59
60#[derive(Debug, thiserror::Error)]
62pub enum SetupError {
63 #[error("cloud setup configuration is invalid: {0}")]
64 Configuration(String),
65 #[cfg(feature = "oauth-providers")]
66 #[error("{provider} authorization failed: {source}")]
67 Authorization {
68 provider: &'static str,
69 #[source]
70 source: crate::oauth::OAuthError,
71 },
72 #[cfg(feature = "oauth-providers")]
73 #[error("{operation}: {source}")]
74 Http {
75 operation: &'static str,
76 #[source]
77 source: reqwest::Error,
78 },
79 #[cfg(feature = "oauth-providers")]
80 #[error("{operation} failed with HTTP {status}: {body}")]
81 ProviderResponse {
82 operation: &'static str,
83 status: reqwest::StatusCode,
84 body: String,
85 },
86 #[error("cloud setup key access failed: {0}")]
87 Key(#[from] coven_keys::keys::KeyError),
88}
89
90#[cfg(feature = "oauth-providers")]
91impl SetupError {
92 pub(crate) fn authorization(provider: &'static str, source: crate::oauth::OAuthError) -> Self {
93 Self::Authorization { provider, source }
94 }
95
96 pub(crate) fn http(operation: &'static str, source: reqwest::Error) -> Self {
97 Self::Http { operation, source }
98 }
99
100 pub(crate) fn provider_response(
101 operation: &'static str,
102 status: reqwest::StatusCode,
103 body: String,
104 ) -> Self {
105 Self::ProviderResponse {
106 operation,
107 status,
108 body,
109 }
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::cloud::CloudHomeJoinInfo;
117
118 #[test]
119 fn exact_slot_admission_rejects_upload_checksums_where_the_provider_cannot_enforce_them() {
120 let mut config = Config::with_defaults(
121 "store-1".to_string(),
122 "device-1".to_string(),
123 "Provider matrix".to_string(),
124 );
125
126 config.cloud_home.provider = Some(CloudProvider::S3);
127 config.cloud_home.s3_endpoint = Some("https://objects.example".to_string());
128 config.cloud_home.exact_upload_verification =
129 coven_foundation::config::ExactUploadVerification::UploadChecksum;
130 assert!(require_exact_slot_capabilities_config(&config).is_ok());
131
132 for provider in [
133 CloudProvider::GoogleDrive,
134 CloudProvider::Dropbox,
135 CloudProvider::OneDrive,
136 CloudProvider::CloudKit,
137 ] {
138 config.cloud_home.provider = Some(provider.clone());
139 assert!(matches!(
140 require_exact_slot_capabilities_config(&config),
141 Err(StorageSetupError::ExactSlotsUnavailable { provider: rejected })
142 if rejected == provider
143 ));
144 config.cloud_home.exact_upload_verification =
145 coven_foundation::config::ExactUploadVerification::MetadataHash;
146 assert!(require_exact_slot_capabilities_config(&config).is_ok());
147 config.cloud_home.exact_upload_verification =
148 coven_foundation::config::ExactUploadVerification::UploadChecksum;
149 }
150 }
151
152 #[test]
153 fn join_admission_uses_the_local_verification_policy() {
154 let join_info = CloudHomeJoinInfo::S3 {
155 bucket: "bucket".to_string(),
156 region: "region".to_string(),
157 endpoint: Some("https://objects.example".to_string()),
158 access_key: "access".to_string(),
159 secret_key: "secret".to_string(),
160 key_prefix: None,
161 };
162
163 assert!(require_exact_slot_capabilities_join_info(
164 &join_info,
165 coven_foundation::config::ExactUploadVerification::UploadChecksum,
166 )
167 .is_ok());
168 }
169}