1pub 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};
37use systemprompt_identifiers::TenantId;
38
39pub use api_client::SyncApiClient;
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};
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
56pub enum SyncDirection {
57 Push,
58 Pull,
59}
60
61#[derive(Clone, Debug)]
62pub struct SyncConfig {
63 pub direction: SyncDirection,
64 pub dry_run: bool,
65 pub verbose: bool,
66 pub tenant_id: TenantId,
67 pub api_url: String,
68 pub api_token: String,
69 pub services_path: String,
70 pub hostname: Option<String>,
71 pub sync_token: Option<String>,
72 pub local_database_url: Option<String>,
73}
74
75#[derive(Debug)]
76pub struct SyncConfigBuilder {
77 direction: SyncDirection,
78 dry_run: bool,
79 verbose: bool,
80 tenant_id: TenantId,
81 api_url: String,
82 api_token: String,
83 services_path: String,
84 hostname: Option<String>,
85 sync_token: Option<String>,
86 local_database_url: Option<String>,
87}
88
89impl SyncConfigBuilder {
90 pub fn new(
91 tenant_id: impl Into<TenantId>,
92 api_url: impl Into<String>,
93 api_token: impl Into<String>,
94 services_path: impl Into<String>,
95 ) -> Self {
96 Self {
97 direction: SyncDirection::Push,
98 dry_run: false,
99 verbose: false,
100 tenant_id: tenant_id.into(),
101 api_url: api_url.into(),
102 api_token: api_token.into(),
103 services_path: services_path.into(),
104 hostname: None,
105 sync_token: None,
106 local_database_url: None,
107 }
108 }
109
110 pub const fn with_direction(mut self, direction: SyncDirection) -> Self {
111 self.direction = direction;
112 self
113 }
114
115 pub const fn with_dry_run(mut self, dry_run: bool) -> Self {
116 self.dry_run = dry_run;
117 self
118 }
119
120 pub const fn with_verbose(mut self, verbose: bool) -> Self {
121 self.verbose = verbose;
122 self
123 }
124
125 pub fn with_hostname(mut self, hostname: Option<String>) -> Self {
126 self.hostname = hostname;
127 self
128 }
129
130 pub fn with_sync_token(mut self, sync_token: Option<String>) -> Self {
131 self.sync_token = sync_token;
132 self
133 }
134
135 pub fn with_local_database_url(mut self, url: impl Into<String>) -> Self {
136 self.local_database_url = Some(url.into());
137 self
138 }
139
140 pub fn build(self) -> SyncConfig {
141 SyncConfig {
142 direction: self.direction,
143 dry_run: self.dry_run,
144 verbose: self.verbose,
145 tenant_id: self.tenant_id,
146 api_url: self.api_url,
147 api_token: self.api_token,
148 services_path: self.services_path,
149 hostname: self.hostname,
150 sync_token: self.sync_token,
151 local_database_url: self.local_database_url,
152 }
153 }
154}
155
156impl SyncConfig {
157 pub fn builder(
158 tenant_id: impl Into<TenantId>,
159 api_url: impl Into<String>,
160 api_token: impl Into<String>,
161 services_path: impl Into<String>,
162 ) -> SyncConfigBuilder {
163 SyncConfigBuilder::new(tenant_id, api_url, api_token, services_path)
164 }
165}
166
167#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
168#[serde(tag = "kind", rename_all = "snake_case")]
169pub enum SyncOpState {
170 NotStarted,
171 Partial {
172 completed: usize,
173 total: usize,
174 },
175 #[default]
176 Completed,
177 Failed,
178}
179
180#[derive(Clone, Debug, Serialize, Deserialize)]
181pub struct SyncOperationResult {
182 pub operation: String,
183 pub success: bool,
184 pub items_synced: usize,
185 pub items_skipped: usize,
186 pub errors: Vec<String>,
187 pub details: Option<serde_json::Value>,
188 #[serde(default)]
189 pub state: SyncOpState,
190}
191
192impl SyncOperationResult {
193 pub fn success(operation: &str, items_synced: usize) -> Self {
194 Self {
195 operation: operation.to_string(),
196 success: true,
197 items_synced,
198 items_skipped: 0,
199 errors: vec![],
200 details: None,
201 state: SyncOpState::Completed,
202 }
203 }
204
205 pub fn with_details(mut self, details: serde_json::Value) -> Self {
206 self.details = Some(details);
207 self
208 }
209
210 pub fn dry_run(operation: &str, items_skipped: usize, details: serde_json::Value) -> Self {
211 Self {
212 operation: operation.to_string(),
213 success: true,
214 items_synced: 0,
215 items_skipped,
216 errors: vec![],
217 details: Some(details),
218 state: SyncOpState::Completed,
219 }
220 }
221}
222
223#[derive(Debug)]
224pub struct SyncService {
225 config: SyncConfig,
226 api_client: SyncApiClient,
227}
228
229impl SyncService {
230 pub fn new(config: SyncConfig) -> SyncResult<Self> {
231 let api_client = SyncApiClient::new(&config.api_url, &config.api_token)?
232 .with_direct_sync(config.hostname.clone(), config.sync_token.clone());
233 Ok(Self { config, api_client })
234 }
235
236 pub async fn sync_files(&self) -> SyncResult<SyncOperationResult> {
237 let service = FileSyncService::new(self.config.clone(), self.api_client.clone());
238 service.sync().await
239 }
240
241 pub async fn sync_database(&self) -> SyncResult<SyncOperationResult> {
242 let local_db_url = self.config.local_database_url.as_ref().ok_or_else(|| {
243 SyncError::MissingConfig("local_database_url not configured".to_string())
244 })?;
245
246 let cloud_db_url = self
247 .api_client
248 .get_database_url(&self.config.tenant_id)
249 .await
250 .map_err(|e| SyncError::ApiError {
251 status: 500,
252 message: format!("Failed to get cloud database URL: {e}"),
253 })?;
254
255 let service = DatabaseSyncService::new(
256 self.config.direction,
257 self.config.dry_run,
258 local_db_url,
259 &cloud_db_url,
260 );
261
262 service.sync().await
263 }
264
265 pub async fn sync_all(&self) -> SyncResult<Vec<SyncOperationResult>> {
266 let mut results = Vec::new();
267
268 let files_result = self.sync_files().await?;
269 results.push(files_result);
270
271 match self.sync_database().await {
272 Ok(db_result) => results.push(db_result),
273 Err(e) => {
274 tracing::warn!(error = %e, "Database sync failed");
275 let (state, items_synced) = match &e {
276 SyncError::MissingConfig(_) => (SyncOpState::NotStarted, 0),
277 SyncError::PartialImport {
278 completed, total, ..
279 } => (
280 SyncOpState::Partial {
281 completed: *completed,
282 total: *total,
283 },
284 *completed,
285 ),
286 _ => (SyncOpState::Failed, 0),
287 };
288 results.push(SyncOperationResult {
289 operation: "database".to_string(),
290 success: false,
291 items_synced,
292 items_skipped: 0,
293 errors: vec![e.to_string()],
294 details: None,
295 state,
296 });
297 },
298 }
299
300 Ok(results)
301 }
302}