1pub mod cloudkit;
9pub mod dropbox;
10pub mod google_drive;
11pub mod http;
12pub mod oauth_session;
13pub mod onedrive;
14pub mod s3;
15pub mod setup;
16
17#[cfg(any(test, feature = "test-utils"))]
18pub mod test_utils;
19
20use async_trait::async_trait;
21use serde::{Deserialize, Serialize};
22
23#[derive(Debug, thiserror::Error)]
25pub enum CloudHomeError {
26 #[error("not found: {0}")]
27 NotFound(String),
28 #[error("storage error: {0}")]
29 Storage(String),
30 #[error("I/O error: {0}")]
31 Io(#[from] std::io::Error),
32}
33
34#[derive(Clone, Debug, Serialize, Deserialize)]
36pub enum CloudHomeJoinInfo {
37 S3 {
38 bucket: String,
39 region: String,
40 endpoint: Option<String>,
41 access_key: String,
42 secret_key: String,
43 #[serde(default)]
44 key_prefix: Option<String>,
45 },
46 GoogleDrive {
47 folder_id: String,
48 },
49 Dropbox {
50 shared_folder_id: String,
51 },
52 OneDrive {
53 drive_id: String,
54 folder_id: String,
55 },
56 HttpProxy {
57 url: String,
58 },
59 CloudKit {
60 share_url: String,
61 },
62}
63
64impl CloudHomeJoinInfo {
65 pub fn cloud_provider(&self) -> crate::config::CloudProvider {
66 use crate::config::CloudProvider;
67 match self {
68 CloudHomeJoinInfo::S3 { .. } => CloudProvider::S3,
69 CloudHomeJoinInfo::GoogleDrive { .. } => CloudProvider::GoogleDrive,
70 CloudHomeJoinInfo::Dropbox { .. } => CloudProvider::Dropbox,
71 CloudHomeJoinInfo::OneDrive { .. } => CloudProvider::OneDrive,
72 CloudHomeJoinInfo::HttpProxy { .. } => CloudProvider::HttpProxy,
73 CloudHomeJoinInfo::CloudKit { .. } => CloudProvider::CloudKit,
74 }
75 }
76}
77
78#[async_trait]
82pub trait CloudHome: Send + Sync {
83 async fn probe(&self) -> Result<(), CloudHomeError> {
90 self.list("__bae_probe__").await.map(drop)
91 }
92
93 async fn write(&self, key: &str, data: Vec<u8>) -> Result<(), CloudHomeError>;
95
96 async fn read(&self, key: &str) -> Result<Vec<u8>, CloudHomeError>;
98
99 async fn read_range(&self, key: &str, start: u64, end: u64) -> Result<Vec<u8>, CloudHomeError>;
101
102 async fn list(&self, prefix: &str) -> Result<Vec<String>, CloudHomeError>;
104
105 async fn delete(&self, key: &str) -> Result<(), CloudHomeError>;
107
108 async fn exists(&self, key: &str) -> Result<bool, CloudHomeError>;
110
111 async fn grant_access(&self, member_id: &str) -> Result<CloudHomeJoinInfo, CloudHomeError>;
116
117 async fn revoke_access(&self, member_id: &str) -> Result<(), CloudHomeError>;
120}
121
122fn require_oauth_token(
124 key_service: &crate::keys::KeyService,
125 provider_name: &str,
126) -> Result<String, CloudHomeError> {
127 match key_service
128 .get_cloud_home_credentials()
129 .map_err(|e| CloudHomeError::Storage(format!("{provider_name} credentials error: {e}")))?
130 {
131 Some(crate::keys::CloudHomeCredentials::OAuth { token_json }) => Ok(token_json),
132 _ => Err(CloudHomeError::Storage(format!(
133 "{provider_name} OAuth token not in keyring"
134 ))),
135 }
136}
137
138fn parse_oauth_tokens(
139 key_service: &crate::keys::KeyService,
140 provider_name: &str,
141) -> Result<crate::oauth::OAuthTokens, CloudHomeError> {
142 let token_json = require_oauth_token(key_service, provider_name)?;
143 serde_json::from_str(&token_json)
144 .map_err(|e| CloudHomeError::Storage(format!("invalid OAuth token JSON: {e}")))
145}
146
147pub async fn create_cloud_home(
150 config: &crate::config::Config,
151 key_service: &crate::keys::KeyService,
152 clock: crate::clock::ClockRef,
153) -> Result<Box<dyn CloudHome>, CloudHomeError> {
154 use crate::config::CloudProvider;
155
156 match config.cloud_home.provider {
157 Some(CloudProvider::S3) | None => {
158 let bucket =
159 config.cloud_home.s3_bucket.clone().ok_or_else(|| {
160 CloudHomeError::Storage("S3 bucket not configured".to_string())
161 })?;
162 let region =
163 config.cloud_home.s3_region.clone().ok_or_else(|| {
164 CloudHomeError::Storage("S3 region not configured".to_string())
165 })?;
166 let endpoint = config.cloud_home.s3_endpoint.clone();
167
168 let (access_key, secret_key) = match key_service
169 .get_cloud_home_credentials()
170 .map_err(|e| CloudHomeError::Storage(format!("S3 credentials error: {e}")))?
171 {
172 Some(crate::keys::CloudHomeCredentials::S3 {
173 access_key,
174 secret_key,
175 }) => (access_key, secret_key),
176 _ => {
177 return Err(CloudHomeError::Storage(
178 "S3 credentials not in keyring".to_string(),
179 ))
180 }
181 };
182
183 let s3 = s3::S3CloudHome::new(
184 bucket,
185 region,
186 endpoint,
187 access_key,
188 secret_key,
189 config.cloud_home.s3_key_prefix.clone(),
190 )
191 .await?;
192 Ok(Box::new(s3))
193 }
194 Some(CloudProvider::GoogleDrive) => {
195 let folder_id = config
196 .cloud_home
197 .google_drive_folder_id
198 .clone()
199 .ok_or_else(|| {
200 CloudHomeError::Storage("Google Drive folder ID not configured".to_string())
201 })?;
202 let tokens = parse_oauth_tokens(key_service, "Google Drive")?;
203 Ok(Box::new(google_drive::GoogleDriveCloudHome::new(
204 folder_id,
205 tokens,
206 key_service.clone(),
207 clock,
208 )))
209 }
210 Some(CloudProvider::Dropbox) => {
211 let folder_path = config
212 .cloud_home
213 .dropbox_folder_path
214 .clone()
215 .ok_or_else(|| {
216 CloudHomeError::Storage("Dropbox folder path not configured".to_string())
217 })?;
218 let tokens = parse_oauth_tokens(key_service, "Dropbox")?;
219 Ok(Box::new(dropbox::DropboxCloudHome::new(
220 folder_path,
221 tokens,
222 key_service.clone(),
223 clock,
224 )))
225 }
226 Some(CloudProvider::OneDrive) => {
227 let drive_id = config.cloud_home.onedrive_drive_id.clone().ok_or_else(|| {
228 CloudHomeError::Storage("OneDrive drive ID not configured".to_string())
229 })?;
230 let folder_id = config
231 .cloud_home
232 .onedrive_folder_id
233 .clone()
234 .ok_or_else(|| {
235 CloudHomeError::Storage("OneDrive folder ID not configured".to_string())
236 })?;
237 let tokens = parse_oauth_tokens(key_service, "OneDrive")?;
238 Ok(Box::new(onedrive::OneDriveCloudHome::new(
239 drive_id,
240 folder_id,
241 tokens,
242 key_service.clone(),
243 clock,
244 )))
245 }
246 Some(CloudProvider::HttpProxy) => {
247 let url = config.cloud_home.http_url.clone().ok_or_else(|| {
248 CloudHomeError::Storage("HTTP proxy URL not configured".to_string())
249 })?;
250 let keypair = key_service
251 .get_or_create_user_keypair()
252 .map_err(|e| CloudHomeError::Storage(format!("keypair: {e}")))?;
253 Ok(Box::new(http::HttpCloudHome::new(url, keypair, clock)))
254 }
255 Some(CloudProvider::CloudKit) => Err(CloudHomeError::Storage(
256 "CloudKit requires the native Swift driver; construct via bae-bridge".to_string(),
257 )),
258 }
259}