1use tracing::{info, warn};
7
8use crate::config::{CloudProvider, Config};
9use crate::keys::{CloudHomeCredentials, KeyService};
10
11pub async fn sign_in_google_drive(
15 key_service: &KeyService,
16 library_name: &str,
17 oauth_cancel: tokio::sync::watch::Receiver<bool>,
18 clock: &dyn crate::clock::Clock,
19) -> Result<String, SetupError> {
20 let oauth_config = super::google_drive::GoogleDriveCloudHome::oauth_config();
21 let tokens = crate::oauth::authorize(&oauth_config, oauth_cancel, clock)
22 .await
23 .map_err(|e| SetupError(format!("Google Drive authorization failed: {e}")))?;
24
25 let client = reqwest::Client::new();
26
27 let folder_name = format!("bae - {library_name}");
29
30 let search_query = format!(
31 "name = '{}' and mimeType = 'application/vnd.google-apps.folder' and trashed = false",
32 folder_name.replace('\'', "\\'")
33 );
34 let search_resp = client
35 .get("https://www.googleapis.com/drive/v3/files")
36 .bearer_auth(&tokens.access_token)
37 .query(&[("q", &search_query), ("fields", &"files(id)".to_string())])
38 .send()
39 .await
40 .map_err(|e| {
41 SetupError(format!(
42 "Failed to search for existing Google Drive folder: {e}"
43 ))
44 })?;
45
46 if !search_resp.status().is_success() {
47 let body = search_resp.text().await.unwrap_or_default();
48 return Err(SetupError(format!(
49 "Failed to search for existing Google Drive folder: {body}"
50 )));
51 }
52
53 let search_json: serde_json::Value = search_resp
54 .json()
55 .await
56 .map_err(|e| SetupError(format!("Failed to parse Google Drive search response: {e}")))?;
57
58 let existing_folder_id = search_json["files"][0]["id"]
59 .as_str()
60 .map(|s| s.to_string());
61
62 let folder_id = if let Some(id) = existing_folder_id {
63 id
64 } else {
65 let create_body = serde_json::json!({
66 "name": folder_name,
67 "mimeType": "application/vnd.google-apps.folder",
68 });
69 let resp = client
70 .post("https://www.googleapis.com/drive/v3/files")
71 .bearer_auth(&tokens.access_token)
72 .json(&create_body)
73 .send()
74 .await
75 .map_err(|e| SetupError(format!("Failed to create Google Drive folder: {e}")))?;
76
77 if !resp.status().is_success() {
78 let body = resp
79 .text()
80 .await
81 .unwrap_or_else(|e| format!("<body read failed: {e}>"));
82 return Err(SetupError(format!(
83 "Failed to create Google Drive folder: {body}"
84 )));
85 }
86
87 let folder_resp: serde_json::Value = resp
88 .json()
89 .await
90 .map_err(|e| SetupError(format!("Failed to parse folder response: {e}")))?;
91 folder_resp["id"]
92 .as_str()
93 .ok_or_else(|| SetupError("Google Drive folder response missing 'id'".to_string()))?
94 .to_string()
95 };
96
97 let token_json = serde_json::to_string(&tokens)
99 .map_err(|e| SetupError(format!("Failed to serialize tokens: {e}")))?;
100 key_service
101 .set_cloud_home_credentials(&CloudHomeCredentials::OAuth { token_json })
102 .map_err(|e| SetupError(format!("Failed to save OAuth token: {e}")))?;
103
104 info!("Authorized Google Drive; folder ready");
105 Ok(folder_id)
106}
107
108pub async fn sign_in_dropbox(
111 key_service: &KeyService,
112 library_name: &str,
113 oauth_cancel: tokio::sync::watch::Receiver<bool>,
114 clock: &dyn crate::clock::Clock,
115) -> Result<String, SetupError> {
116 let oauth_config = super::dropbox::DropboxCloudHome::oauth_config();
117 let tokens = crate::oauth::authorize(&oauth_config, oauth_cancel, clock)
118 .await
119 .map_err(|e| SetupError(format!("Dropbox authorization failed: {e}")))?;
120
121 let client = reqwest::Client::new();
122
123 let folder_path = format!("/Apps/bae/{library_name}");
124
125 let create_body = serde_json::json!({
127 "path": folder_path,
128 "autorename": false,
129 });
130 let resp = client
131 .post("https://api.dropboxapi.com/2/files/create_folder_v2")
132 .bearer_auth(&tokens.access_token)
133 .json(&create_body)
134 .send()
135 .await
136 .map_err(|e| SetupError(format!("Failed to create Dropbox folder: {e}")))?;
137
138 let status = resp.status();
139 if !status.is_success() {
140 let body = resp
141 .text()
142 .await
143 .unwrap_or_else(|e| format!("<body read failed: {e}>"));
144 if !(status == reqwest::StatusCode::CONFLICT && body.contains("conflict")) {
146 return Err(SetupError(format!(
147 "Failed to create Dropbox folder (HTTP {status}): {body}"
148 )));
149 }
150 }
151
152 let token_json = serde_json::to_string(&tokens)
154 .map_err(|e| SetupError(format!("Failed to serialize tokens: {e}")))?;
155 key_service
156 .set_cloud_home_credentials(&CloudHomeCredentials::OAuth { token_json })
157 .map_err(|e| SetupError(format!("Failed to save OAuth token: {e}")))?;
158
159 info!("Authorized Dropbox; folder ready");
160 Ok(folder_path)
161}
162
163pub async fn sign_in_onedrive(
167 key_service: &KeyService,
168 oauth_cancel: tokio::sync::watch::Receiver<bool>,
169 clock: &dyn crate::clock::Clock,
170) -> Result<(String, String), SetupError> {
171 let oauth_config = super::onedrive::OneDriveCloudHome::oauth_config();
172 let tokens = crate::oauth::authorize(&oauth_config, oauth_cancel, clock)
173 .await
174 .map_err(|e| SetupError(format!("OneDrive authorization failed: {e}")))?;
175
176 let client = reqwest::Client::new();
177
178 let drive_resp = client
180 .get("https://graph.microsoft.com/v1.0/me/drive")
181 .bearer_auth(&tokens.access_token)
182 .send()
183 .await
184 .map_err(|e| SetupError(format!("Failed to get drive info: {e}")))?;
185
186 if !drive_resp.status().is_success() {
187 let body = drive_resp
188 .text()
189 .await
190 .unwrap_or_else(|e| format!("<body read failed: {e}>"));
191 return Err(SetupError(format!("Failed to get OneDrive info: {body}")));
192 }
193
194 let drive_json: serde_json::Value = drive_resp
195 .json()
196 .await
197 .map_err(|e| SetupError(format!("Failed to parse drive response: {e}")))?;
198
199 let drive_id = drive_json["id"]
200 .as_str()
201 .ok_or_else(|| SetupError("Drive response missing 'id' field".to_string()))?
202 .to_string();
203
204 let create_resp = client
206 .post(format!(
207 "https://graph.microsoft.com/v1.0/drives/{}/root/children",
208 drive_id
209 ))
210 .bearer_auth(&tokens.access_token)
211 .json(&serde_json::json!({
212 "name": "bae",
213 "folder": {},
214 "@microsoft.graph.conflictBehavior": "useExisting",
215 }))
216 .send()
217 .await
218 .map_err(|e| SetupError(format!("Failed to create OneDrive folder: {e}")))?;
219
220 if !create_resp.status().is_success() {
221 let body = create_resp
222 .text()
223 .await
224 .unwrap_or_else(|e| format!("<body read failed: {e}>"));
225 return Err(SetupError(format!(
226 "Failed to create OneDrive folder: {body}"
227 )));
228 }
229
230 let folder_json: serde_json::Value = create_resp
231 .json()
232 .await
233 .map_err(|e| SetupError(format!("Failed to parse folder response: {e}")))?;
234
235 let folder_id = folder_json["id"]
236 .as_str()
237 .ok_or_else(|| SetupError("Folder response missing 'id' field".to_string()))?
238 .to_string();
239
240 let token_json = serde_json::to_string(&tokens)
242 .map_err(|e| SetupError(format!("Failed to serialize tokens: {e}")))?;
243 key_service
244 .set_cloud_home_credentials(&CloudHomeCredentials::OAuth { token_json })
245 .map_err(|e| SetupError(format!("Failed to save OAuth token: {e}")))?;
246
247 info!("Authorized OneDrive; folder ready");
248 Ok((drive_id, folder_id))
249}
250
251pub fn cloud_account_display_for(config: &Config, key_service: &KeyService) -> Option<String> {
253 match config.cloud_home.provider.as_ref()? {
254 CloudProvider::HttpProxy => config.cloud_home.http_url.clone(),
255 CloudProvider::S3 => config
256 .cloud_home
257 .s3_bucket
258 .as_ref()
259 .map(|b| format!("s3://{b}")),
260 CloudProvider::CloudKit => Some("iCloud".to_string()),
261 CloudProvider::GoogleDrive | CloudProvider::Dropbox | CloudProvider::OneDrive => {
262 match key_service.get_cloud_home_credentials() {
263 Ok(Some(CloudHomeCredentials::OAuth { .. })) => Some("Connected".to_string()),
264 Ok(_) => None,
265 Err(e) => {
266 warn!("reading cloud home credentials for account display: {e}");
267 None
268 }
269 }
270 }
271 }
272}
273
274pub fn generate_restore_code(
276 config: &Config,
277 key_service: &KeyService,
278) -> Result<String, SetupError> {
279 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
280 use base64::Engine;
281
282 use crate::sync::restore_code::{encode_restore_code, RestoreCode, RestoreProvider};
283
284 let cloud_provider = config.cloud_home.provider.as_ref().ok_or_else(|| {
285 SetupError("No cloud provider configured. Set up sync first.".to_string())
286 })?;
287
288 let encryption_key_hex = key_service
289 .get_encryption_key()
290 .map_err(|e| SetupError(format!("Failed to read encryption key: {e}")))?
291 .ok_or_else(|| SetupError("No encryption key found".to_string()))?;
292
293 let keypair = key_service
294 .get_or_create_user_keypair()
295 .map_err(|e| SetupError(format!("Failed to get signing key: {e}")))?;
296
297 let provider = match cloud_provider {
298 CloudProvider::S3 => {
299 let creds = key_service
300 .get_cloud_home_credentials()
301 .map_err(|e| SetupError(format!("Failed to read cloud credentials: {e}")))?
302 .ok_or_else(|| SetupError("No S3 credentials found in keyring".to_string()))?;
303 let (access_key, secret_key) = match creds {
304 CloudHomeCredentials::S3 {
305 access_key,
306 secret_key,
307 } => (access_key, secret_key),
308 _ => {
309 return Err(SetupError(
310 "Expected S3 credentials but found different type".to_string(),
311 ))
312 }
313 };
314 let bucket = config
315 .cloud_home
316 .s3_bucket
317 .clone()
318 .ok_or_else(|| SetupError("S3 bucket not configured".to_string()))?;
319 let region = config
320 .cloud_home
321 .s3_region
322 .clone()
323 .ok_or_else(|| SetupError("S3 region not configured".to_string()))?;
324 RestoreProvider::S3 {
325 bucket,
326 region,
327 endpoint: config.cloud_home.s3_endpoint.clone(),
328 key_prefix: config.cloud_home.s3_key_prefix.clone(),
329 access_key,
330 secret_key,
331 }
332 }
333 CloudProvider::CloudKit => RestoreProvider::CloudKit,
334 CloudProvider::GoogleDrive => RestoreProvider::GoogleDrive {
335 folder_id: config
336 .cloud_home
337 .google_drive_folder_id
338 .clone()
339 .ok_or_else(|| SetupError("Google Drive folder ID not configured".to_string()))?,
340 },
341 CloudProvider::Dropbox => RestoreProvider::Dropbox {
342 folder_path: config
343 .cloud_home
344 .dropbox_folder_path
345 .clone()
346 .ok_or_else(|| SetupError("Dropbox folder path not configured".to_string()))?,
347 },
348 CloudProvider::OneDrive => {
349 let drive_id = config
350 .cloud_home
351 .onedrive_drive_id
352 .clone()
353 .ok_or_else(|| SetupError("OneDrive drive ID not configured".to_string()))?;
354 let folder_id = config
355 .cloud_home
356 .onedrive_folder_id
357 .clone()
358 .ok_or_else(|| SetupError("OneDrive folder ID not configured".to_string()))?;
359 RestoreProvider::OneDrive {
360 drive_id,
361 folder_id,
362 }
363 }
364 CloudProvider::HttpProxy => RestoreProvider::HttpProxy {
365 url: config
366 .cloud_home
367 .http_url
368 .clone()
369 .ok_or_else(|| SetupError("HTTP proxy URL not configured".to_string()))?,
370 },
371 };
372
373 let code = RestoreCode {
374 v: 1,
375 lid: config.library_id.clone(),
376 ek: encryption_key_hex,
377 name: config.library_name.clone(),
378 provider,
379 sk: URL_SAFE_NO_PAD.encode(keypair.signing_key),
380 };
381
382 Ok(encode_restore_code(&code))
383}
384
385pub async fn create_sync_storage(
391 config: &Config,
392 key_service: &KeyService,
393 encryption_service: &Option<crate::encryption::EncryptionService>,
394 clock: crate::clock::ClockRef,
395) -> Result<crate::sync::encrypted_storage::EncryptedSyncStorage, String> {
396 let cloud_home = super::create_cloud_home(config, key_service, clock)
397 .await
398 .map_err(|e| format!("{e}"))?;
399
400 let encryption = match encryption_service {
401 Some(enc) => enc.clone(),
402 None => {
403 let key = key_service
404 .get_encryption_key()
405 .map_err(|e| format!("Failed to read encryption key: {e}"))?
406 .ok_or("No encryption key found")?;
407 crate::encryption::EncryptionService::new(&key)
408 .map_err(|e| format!("Failed to create encryption service: {e}"))?
409 }
410 };
411
412 Ok(crate::sync::encrypted_storage::EncryptedSyncStorage::new(
413 cloud_home, encryption,
414 ))
415}
416
417#[derive(Debug, thiserror::Error)]
419#[error("{0}")]
420pub struct SetupError(pub String);