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