1use anyhow::{anyhow, Result};
9use serde::{Deserialize, Serialize};
10use std::path::{Path, PathBuf};
11use std::time::{SystemTime, UNIX_EPOCH};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "lowercase")]
20pub enum CloudProvider {
21 Local,
23 S3,
25 AzureBlob,
27 Gcs,
29 Dropbox,
31 ICloud,
33 OneDrive,
35 WebDav,
37}
38
39impl CloudProvider {
40 pub fn display_name(&self) -> &'static str {
42 match self {
43 Self::Local => "Local Storage",
44 Self::S3 => "Amazon S3",
45 Self::AzureBlob => "Azure Blob Storage",
46 Self::Gcs => "Google Cloud Storage",
47 Self::Dropbox => "Dropbox",
48 Self::ICloud => "iCloud Drive",
49 Self::OneDrive => "OneDrive",
50 Self::WebDav => "WebDAV",
51 }
52 }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct CloudSyncConfig {
62 pub enabled: bool,
64 pub provider: CloudProvider,
66 pub provider_config: ProviderSpecificConfig,
68 pub sync_frequency_seconds: u64,
70 pub auto_sync: bool,
72 pub encrypt_before_upload: bool,
74 pub conflict_resolution: ConflictResolution,
76}
77
78impl Default for CloudSyncConfig {
79 fn default() -> Self {
80 Self {
81 enabled: false,
82 provider: CloudProvider::Local,
83 provider_config: ProviderSpecificConfig::Local(LocalConfig::default()),
84 sync_frequency_seconds: 300, auto_sync: true,
86 encrypt_before_upload: true,
87 conflict_resolution: ConflictResolution::LastWriteWins,
88 }
89 }
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
94#[serde(rename_all = "snake_case")]
95pub enum ConflictResolution {
96 LastWriteWins,
98 LocalWins,
100 RemoteWins,
102 KeepBoth,
104 Manual,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110#[serde(tag = "type", rename_all = "lowercase")]
111pub enum ProviderSpecificConfig {
112 Local(LocalConfig),
113 S3(S3Config),
114 AzureBlob(AzureBlobConfig),
115 Gcs(GcsConfig),
116 Dropbox(DropboxConfig),
117 ICloud(ICloudConfig),
118 OneDrive(OneDriveConfig),
119 WebDav(WebDavConfig),
120}
121
122#[derive(Debug, Clone, Default, Serialize, Deserialize)]
123pub struct LocalConfig {
124 pub sync_directory: Option<String>,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct S3Config {
129 pub bucket: String,
130 pub region: String,
131 pub prefix: Option<String>,
132 pub access_key_id: Option<String>,
133 pub secret_access_key: Option<String>,
134 pub endpoint: Option<String>, }
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct AzureBlobConfig {
139 pub container: String,
140 pub connection_string: Option<String>,
141 pub account_name: Option<String>,
142 pub account_key: Option<String>,
143 pub prefix: Option<String>,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct GcsConfig {
148 pub bucket: String,
149 pub project_id: String,
150 pub prefix: Option<String>,
151 pub credentials_file: Option<String>,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct DropboxConfig {
156 pub access_token: Option<String>,
157 pub refresh_token: Option<String>,
158 pub folder_path: Option<String>,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct ICloudConfig {
163 pub container_id: Option<String>,
164 pub folder_path: Option<String>,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct OneDriveConfig {
169 pub access_token: Option<String>,
170 pub refresh_token: Option<String>,
171 pub folder_path: Option<String>,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct WebDavConfig {
176 pub url: String,
177 pub username: Option<String>,
178 pub password: Option<String>,
179 pub folder_path: Option<String>,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct SessionSyncState {
189 pub session_id: String,
191 pub local_modified: i64,
193 pub remote_modified: Option<i64>,
195 pub local_hash: String,
197 pub remote_hash: Option<String>,
199 pub status: SyncStatus,
201 pub last_sync_attempt: Option<i64>,
203 pub last_sync_success: Option<i64>,
205 pub last_error: Option<String>,
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
211#[serde(rename_all = "snake_case")]
212pub enum SyncStatus {
213 Synced,
215 PendingUpload,
217 PendingDownload,
219 Conflict,
221 Syncing,
223 Error,
225 NeverSynced,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct SyncState {
232 pub last_full_sync: Option<i64>,
234 pub sessions: Vec<SessionSyncState>,
236 pub pending_uploads: u32,
238 pub pending_downloads: u32,
239 pub conflicts: u32,
240}
241
242impl SyncState {
243 pub fn new() -> Self {
244 Self {
245 last_full_sync: None,
246 sessions: Vec::new(),
247 pending_uploads: 0,
248 pending_downloads: 0,
249 conflicts: 0,
250 }
251 }
252}
253
254impl Default for SyncState {
255 fn default() -> Self {
256 Self::new()
257 }
258}
259
260#[async_trait::async_trait]
266pub trait CloudSyncService: Send + Sync {
267 fn provider(&self) -> CloudProvider;
269
270 async fn test_connection(&self) -> Result<bool>;
272
273 async fn list_remote_sessions(&self) -> Result<Vec<RemoteSessionInfo>>;
275
276 async fn upload_session(&self, session_id: &str, data: &[u8]) -> Result<UploadResult>;
278
279 async fn download_session(&self, session_id: &str) -> Result<Vec<u8>>;
281
282 async fn delete_remote_session(&self, session_id: &str) -> Result<()>;
284
285 async fn get_remote_metadata(&self, session_id: &str) -> Result<Option<RemoteSessionInfo>>;
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct RemoteSessionInfo {
292 pub session_id: String,
293 pub modified_at: i64,
294 pub size_bytes: u64,
295 pub content_hash: String,
296 pub metadata: Option<serde_json::Value>,
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct UploadResult {
302 pub success: bool,
303 pub remote_path: String,
304 pub content_hash: String,
305 pub uploaded_at: i64,
306}
307
308pub struct LocalSyncService {
314 sync_dir: PathBuf,
315}
316
317impl LocalSyncService {
318 pub fn new(sync_dir: PathBuf) -> Self {
319 Self { sync_dir }
320 }
321
322 fn session_path(&self, session_id: &str) -> PathBuf {
323 self.sync_dir.join(format!("{}.json", session_id))
324 }
325}
326
327#[async_trait::async_trait]
328impl CloudSyncService for LocalSyncService {
329 fn provider(&self) -> CloudProvider {
330 CloudProvider::Local
331 }
332
333 async fn test_connection(&self) -> Result<bool> {
334 Ok(self.sync_dir.exists() || std::fs::create_dir_all(&self.sync_dir).is_ok())
335 }
336
337 async fn list_remote_sessions(&self) -> Result<Vec<RemoteSessionInfo>> {
338 let mut sessions = Vec::new();
339
340 if !self.sync_dir.exists() {
341 return Ok(sessions);
342 }
343
344 for entry in std::fs::read_dir(&self.sync_dir)? {
345 let entry = entry?;
346 let path = entry.path();
347
348 if path.extension().and_then(|s| s.to_str()) == Some("json") {
349 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
350 let metadata = entry.metadata()?;
351 let modified = metadata
352 .modified()?
353 .duration_since(UNIX_EPOCH)
354 .unwrap_or_default()
355 .as_secs() as i64;
356
357 let hash = format!("{}-{}", metadata.len(), modified);
359
360 sessions.push(RemoteSessionInfo {
361 session_id: stem.to_string(),
362 modified_at: modified,
363 size_bytes: metadata.len(),
364 content_hash: hash,
365 metadata: None,
366 });
367 }
368 }
369 }
370
371 Ok(sessions)
372 }
373
374 async fn upload_session(&self, session_id: &str, data: &[u8]) -> Result<UploadResult> {
375 std::fs::create_dir_all(&self.sync_dir)?;
376
377 let path = self.session_path(session_id);
378 std::fs::write(&path, data)?;
379
380 let now = SystemTime::now()
381 .duration_since(UNIX_EPOCH)
382 .unwrap_or_default()
383 .as_secs() as i64;
384
385 let hash = format!("{}-{}", data.len(), now);
387
388 Ok(UploadResult {
389 success: true,
390 remote_path: path.to_string_lossy().to_string(),
391 content_hash: hash,
392 uploaded_at: now,
393 })
394 }
395
396 async fn download_session(&self, session_id: &str) -> Result<Vec<u8>> {
397 let path = self.session_path(session_id);
398 std::fs::read(&path).map_err(|e| anyhow!("Failed to read session: {}", e))
399 }
400
401 async fn delete_remote_session(&self, session_id: &str) -> Result<()> {
402 let path = self.session_path(session_id);
403 if path.exists() {
404 std::fs::remove_file(&path)?;
405 }
406 Ok(())
407 }
408
409 async fn get_remote_metadata(&self, session_id: &str) -> Result<Option<RemoteSessionInfo>> {
410 let path = self.session_path(session_id);
411
412 if !path.exists() {
413 return Ok(None);
414 }
415
416 let metadata = std::fs::metadata(&path)?;
417 let modified = metadata
418 .modified()?
419 .duration_since(UNIX_EPOCH)
420 .unwrap_or_default()
421 .as_secs() as i64;
422
423 let hash = format!("{}-{}", metadata.len(), modified);
424
425 Ok(Some(RemoteSessionInfo {
426 session_id: session_id.to_string(),
427 modified_at: modified,
428 size_bytes: metadata.len(),
429 content_hash: hash,
430 metadata: None,
431 }))
432 }
433}
434
435pub struct SyncManager {
441 config: CloudSyncConfig,
442 state: SyncState,
443 service: Option<Box<dyn CloudSyncService>>,
444}
445
446impl SyncManager {
447 pub fn new(config: CloudSyncConfig) -> Self {
448 Self {
449 config,
450 state: SyncState::new(),
451 service: None,
452 }
453 }
454
455 pub fn initialize(&mut self) -> Result<()> {
457 if !self.config.enabled {
458 return Ok(());
459 }
460
461 match &self.config.provider_config {
462 ProviderSpecificConfig::Local(local_config) => {
463 let sync_dir = local_config
464 .sync_directory
465 .as_ref()
466 .map(PathBuf::from)
467 .unwrap_or_else(|| {
468 dirs::data_local_dir()
469 .unwrap_or_else(|| PathBuf::from("."))
470 .join("csm")
471 .join("sync")
472 });
473 self.service = Some(Box::new(LocalSyncService::new(sync_dir)));
474 }
475 _ => {
476 return Err(anyhow!(
477 "Cloud provider {:?} not yet implemented",
478 self.config.provider
479 ));
480 }
481 }
482
483 Ok(())
484 }
485
486 pub async fn test_connection(&self) -> Result<bool> {
488 match &self.service {
489 Some(service) => service.test_connection().await,
490 None => Err(anyhow!("Sync service not initialized")),
491 }
492 }
493
494 pub fn get_state(&self) -> &SyncState {
496 &self.state
497 }
498
499 pub async fn sync_all(&mut self) -> Result<SyncResult> {
501 let service = self
502 .service
503 .as_ref()
504 .ok_or_else(|| anyhow!("Sync service not initialized"))?;
505
506 let mut result = SyncResult {
507 uploaded: 0,
508 downloaded: 0,
509 conflicts: 0,
510 errors: Vec::new(),
511 };
512
513 let remote_sessions = service.list_remote_sessions().await?;
515
516 self.state.last_full_sync = Some(
518 SystemTime::now()
519 .duration_since(UNIX_EPOCH)
520 .unwrap_or_default()
521 .as_secs() as i64,
522 );
523
524 Ok(result)
527 }
528
529 pub async fn upload_session(&mut self, session_id: &str, data: &[u8]) -> Result<UploadResult> {
531 let service = self
532 .service
533 .as_ref()
534 .ok_or_else(|| anyhow!("Sync service not initialized"))?;
535
536 service.upload_session(session_id, data).await
537 }
538
539 pub async fn download_session(&self, session_id: &str) -> Result<Vec<u8>> {
541 let service = self
542 .service
543 .as_ref()
544 .ok_or_else(|| anyhow!("Sync service not initialized"))?;
545
546 service.download_session(session_id).await
547 }
548}
549
550#[derive(Debug, Clone, Serialize, Deserialize)]
552pub struct SyncResult {
553 pub uploaded: u32,
554 pub downloaded: u32,
555 pub conflicts: u32,
556 pub errors: Vec<String>,
557}
558
559#[cfg(test)]
564mod tests {
565 use super::*;
566 use tempfile::tempdir;
567
568 #[tokio::test]
569 async fn test_local_sync_service() {
570 let temp_dir = tempdir().unwrap();
571 let sync_dir = temp_dir.path().join("sync");
572
573 let service = LocalSyncService::new(sync_dir.clone());
574
575 assert!(service.test_connection().await.unwrap());
577
578 let data = b"test session data";
580 let result = service.upload_session("test-session", data).await.unwrap();
581 assert!(result.success);
582
583 let sessions = service.list_remote_sessions().await.unwrap();
585 assert_eq!(sessions.len(), 1);
586 assert_eq!(sessions[0].session_id, "test-session");
587
588 let downloaded = service.download_session("test-session").await.unwrap();
590 assert_eq!(downloaded, data);
591
592 service.delete_remote_session("test-session").await.unwrap();
594 let sessions = service.list_remote_sessions().await.unwrap();
595 assert!(sessions.is_empty());
596 }
597}