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