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