mod config;
mod result;
pub mod api_client;
pub mod crate_deploy;
pub mod database;
pub mod diff;
pub mod error;
pub mod export;
mod file_bundler;
pub mod files;
pub mod jobs;
pub mod local;
pub mod models;
use serde::{Deserialize, Serialize};
pub use api_client::SyncApiClient;
pub use config::{SyncConfig, SyncConfigBuilder};
pub use database::{ContextExport, DatabaseExport, DatabaseSyncService};
pub use diff::{ContentDiffCalculator, compute_content_hash};
pub use error::{SyncError, SyncResult};
pub use export::{escape_yaml, export_content_to_file, generate_content_markdown};
pub use files::{
FileBundle, FileDiffStatus, FileEntry, FileManifest, FileSyncService, PullDownload,
SyncDiffEntry, SyncDiffResult,
};
pub use jobs::{AccessControlSyncJob, ContentSyncJob};
pub use local::{AccessControlLocalSync, ContentDiffEntry, ContentLocalSync};
pub use models::{
ContentDiffItem, ContentDiffResult, DiffStatus, DiskContent, LocalSyncDirection,
LocalSyncResult,
};
pub use result::{SyncOpState, SyncOperationResult};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum SyncDirection {
Push,
Pull,
}
#[derive(Debug)]
pub struct SyncService {
config: SyncConfig,
api_client: SyncApiClient,
}
impl SyncService {
pub fn new(config: SyncConfig) -> SyncResult<Self> {
let api_client = SyncApiClient::new(&config.api_url, &config.api_token)?
.with_direct_sync(config.hostname.clone());
Ok(Self { config, api_client })
}
pub async fn sync_files(&self) -> SyncResult<SyncOperationResult> {
let service = FileSyncService::new(self.config.clone(), self.api_client.clone());
service.sync().await
}
pub async fn sync_database(&self) -> SyncResult<SyncOperationResult> {
let local_db_url = self.config.local_database_url.as_ref().ok_or_else(|| {
SyncError::MissingConfig("local_database_url not configured".to_string())
})?;
let cloud_db_url = self
.api_client
.get_database_url(&self.config.tenant_id)
.await
.map_err(|e| SyncError::ApiError {
status: 500,
message: format!("Failed to get cloud database URL: {e}"),
})?;
let service = DatabaseSyncService::new(
self.config.direction,
self.config.dry_run,
local_db_url,
&cloud_db_url,
);
service.sync().await
}
pub async fn sync_all(&self) -> SyncResult<Vec<SyncOperationResult>> {
let mut results = Vec::new();
let files_result = self.sync_files().await?;
results.push(files_result);
match self.sync_database().await {
Ok(db_result) => results.push(db_result),
Err(e) => results.push(database_failure_result(&e)),
}
Ok(results)
}
}
fn database_failure_result(error: &SyncError) -> SyncOperationResult {
tracing::warn!(error = %error, "Database sync failed");
let (state, items_synced) = match error {
SyncError::MissingConfig(_) => (SyncOpState::NotStarted, 0),
SyncError::PartialImport {
completed, total, ..
} => (
SyncOpState::Partial {
completed: *completed,
total: *total,
},
*completed,
),
_ => (SyncOpState::Failed, 0),
};
SyncOperationResult {
operation: "database".to_string(),
success: false,
items_synced,
items_skipped: 0,
errors: vec![error.to_string()],
details: None,
state,
}
}