1mod config;
25mod result;
26
27pub mod api_client;
28pub mod crate_deploy;
29pub mod database;
30pub mod deploy;
31pub mod diff;
32pub mod error;
33pub mod export;
34pub mod files;
35pub mod jobs;
36pub mod local;
37pub mod models;
38
39use serde::{Deserialize, Serialize};
40
41pub use api_client::SyncApiClient;
42pub use config::{SyncConfig, SyncConfigBuilder};
43pub use database::{ContextExport, DatabaseExport, DatabaseSyncService};
44pub use diff::{ContentDiffCalculator, compute_content_hash};
45pub use error::{SyncError, SyncResult};
46pub use export::{escape_yaml, export_content_to_file, generate_content_markdown};
47pub use files::{
48 FileBundle, FileDiffStatus, FileEntry, FileManifest, FileSyncService, PullDownload,
49 SyncDiffEntry, SyncDiffResult,
50};
51pub use jobs::{AccessControlSyncJob, ContentSyncJob};
52pub use local::{AccessControlLocalSync, ContentDiffEntry, ContentLocalSync};
53pub use models::{
54 ContentDiffItem, ContentDiffResult, DiffStatus, DiskContent, LocalSyncDirection,
55 LocalSyncResult,
56};
57pub use result::{SyncOpState, SyncOperationResult};
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
60pub enum SyncDirection {
61 Push,
62 Pull,
63}
64
65#[derive(Debug)]
66pub struct SyncService {
67 config: SyncConfig,
68 api_client: SyncApiClient,
69}
70
71impl SyncService {
72 pub fn new(config: SyncConfig) -> SyncResult<Self> {
73 let api_client = SyncApiClient::new(&config.api_url, &config.api_token)?
74 .with_direct_sync(config.hostname.clone());
75 Ok(Self { config, api_client })
76 }
77
78 pub async fn sync_files(&self) -> SyncResult<SyncOperationResult> {
79 let service = FileSyncService::new(self.config.clone(), self.api_client.clone());
80 service.sync().await
81 }
82
83 pub async fn sync_database(&self) -> SyncResult<SyncOperationResult> {
84 let local_db_url = self.config.local_database_url.as_ref().ok_or_else(|| {
85 SyncError::MissingConfig("local_database_url not configured".to_owned())
86 })?;
87
88 let cloud_db_url = self
89 .api_client
90 .get_database_url(&self.config.tenant_id)
91 .await
92 .map_err(|e| SyncError::ApiError {
93 status: 500,
94 message: format!("Failed to get cloud database URL: {e}"),
95 })?;
96
97 let service = DatabaseSyncService::new(
98 self.config.direction,
99 self.config.dry_run,
100 local_db_url,
101 &cloud_db_url,
102 );
103
104 service.sync().await
105 }
106
107 pub async fn sync_all(&self) -> SyncResult<Vec<SyncOperationResult>> {
108 let mut results = Vec::new();
109
110 let files_result = self.sync_files().await?;
111 results.push(files_result);
112
113 match self.sync_database().await {
114 Ok(db_result) => results.push(db_result),
115 Err(e) => results.push(database_failure_result(&e)),
116 }
117
118 Ok(results)
119 }
120}
121
122fn database_failure_result(error: &SyncError) -> SyncOperationResult {
123 tracing::warn!(error = %error, "Database sync failed");
124 let (state, items_synced) = match error {
125 SyncError::MissingConfig(_) => (SyncOpState::NotStarted, 0),
126 SyncError::PartialImport {
127 completed, total, ..
128 } => (
129 SyncOpState::Partial {
130 completed: *completed,
131 total: *total,
132 },
133 *completed,
134 ),
135 _ => (SyncOpState::Failed, 0),
136 };
137 SyncOperationResult {
138 operation: "database".to_owned(),
139 success: false,
140 items_synced,
141 items_skipped: 0,
142 errors: vec![error.to_string()],
143 details: None,
144 state,
145 }
146}