use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use tracing::{debug, warn};
use crate::config::{Config, RuntimeMode};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyncStatus {
Enabled,
Disabled,
Syncing,
Failed,
}
pub struct SyncManager {
api_url: Option<String>,
tenant_id: Option<String>,
auto_sync: bool,
runtime_mode: RuntimeMode,
}
#[derive(Debug, Serialize, Deserialize)]
struct SyncRequest {
tenant_id: String,
config: Config,
timestamp: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SyncResponse {
pub config: Config,
pub timestamp: i64,
pub conflicts: Vec<String>,
}
impl SyncManager {
pub fn new(
api_url: Option<String>,
tenant_id: Option<String>,
auto_sync: bool,
runtime_mode: RuntimeMode,
) -> Self {
Self {
api_url,
tenant_id,
auto_sync,
runtime_mode,
}
}
pub fn is_enabled(&self) -> bool {
if !self.auto_sync {
return false;
}
if matches!(self.runtime_mode, RuntimeMode::AirGapped) {
debug!("Sync disabled: air-gapped mode");
return false;
}
if self.api_url.is_none() || self.tenant_id.is_none() {
debug!("Sync disabled: missing API URL or tenant ID");
return false;
}
true
}
pub fn status(&self) -> SyncStatus {
if self.is_enabled() {
SyncStatus::Enabled
} else {
SyncStatus::Disabled
}
}
pub async fn sync_to_cloud(&self, config: &Config) -> Result<Option<SyncResponse>> {
if !self.is_enabled() {
return Ok(None);
}
let api_url = self.api_url.as_ref().unwrap();
let tenant_id = self.tenant_id.as_ref().unwrap();
let sync_request = SyncRequest {
tenant_id: tenant_id.clone(),
config: config.clone(),
timestamp: chrono::Utc::now().timestamp(),
};
let client = reqwest::Client::new();
let url = format!("{}/api/v1/config/sync", api_url);
debug!("Syncing configuration to cloud: {}", url);
let response = client
.post(&url)
.json(&sync_request)
.send()
.await
.context("Failed to send sync request")?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return Err(anyhow::anyhow!(
"Sync failed with status {}: {}",
status,
error_text
));
}
let sync_response: SyncResponse = response
.json()
.await
.context("Failed to parse sync response")?;
if !sync_response.conflicts.is_empty() {
warn!(
"Configuration conflicts detected: {:?}",
sync_response.conflicts
);
}
debug!("Configuration synced successfully");
Ok(Some(sync_response))
}
pub async fn sync_from_cloud(&self) -> Result<Option<Config>> {
if !self.is_enabled() {
return Ok(None);
}
let api_url = self.api_url.as_ref().unwrap();
let tenant_id = self.tenant_id.as_ref().unwrap();
let client = reqwest::Client::new();
let url = format!("{}/api/v1/config/sync?tenant_id={}", api_url, tenant_id);
debug!("Syncing configuration from cloud: {}", url);
let response = client
.get(&url)
.send()
.await
.context("Failed to send sync request")?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return Err(anyhow::anyhow!(
"Sync failed with status {}: {}",
status,
error_text
));
}
let sync_response: SyncResponse = response
.json()
.await
.context("Failed to parse sync response")?;
debug!("Configuration synced from cloud successfully");
Ok(Some(sync_response.config))
}
pub fn resolve_conflicts(
&self,
_local_config: &Config,
cloud_config: &Config,
conflicts: &[String],
) -> Config {
warn!(
"Resolving {} conflicts using last-write-wins (cloud wins)",
conflicts.len()
);
cloud_config.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sync_enabled() {
let manager = SyncManager::new(
Some("https://api.example.com".to_string()),
Some("tenant-123".to_string()),
true,
RuntimeMode::Local,
);
assert!(manager.is_enabled());
assert_eq!(manager.status(), SyncStatus::Enabled);
}
#[test]
fn test_sync_disabled_airgapped() {
let manager = SyncManager::new(
Some("https://api.example.com".to_string()),
Some("tenant-123".to_string()),
true,
RuntimeMode::AirGapped,
);
assert!(!manager.is_enabled());
assert_eq!(manager.status(), SyncStatus::Disabled);
}
#[test]
fn test_sync_disabled_no_auth() {
let manager = SyncManager::new(None, None, true, RuntimeMode::Local);
assert!(!manager.is_enabled());
assert_eq!(manager.status(), SyncStatus::Disabled);
}
}