mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-07-06 13:07:49 +02:00
refactor: cleanup and decouple
This commit is contained in:
@@ -102,6 +102,24 @@ impl SyncEngine {
|
||||
// Generate local manifest
|
||||
let local_manifest = generate_manifest(&profile_id, &profile_dir, &mut hash_cache)?;
|
||||
|
||||
let total_size: u64 = local_manifest.files.iter().map(|f| f.size).sum();
|
||||
let has_cookies = local_manifest
|
||||
.files
|
||||
.iter()
|
||||
.any(|f| f.path.contains("Cookies") || f.path.contains("cookies"));
|
||||
let has_local_state = local_manifest
|
||||
.files
|
||||
.iter()
|
||||
.any(|f| f.path.contains("Local State"));
|
||||
log::info!(
|
||||
"Profile {} manifest: {} files, {} bytes total, cookies={}, local_state={}",
|
||||
profile_id,
|
||||
local_manifest.files.len(),
|
||||
total_size,
|
||||
has_cookies,
|
||||
has_local_state
|
||||
);
|
||||
|
||||
// Save the hash cache for future runs
|
||||
hash_cache.save(&cache_path)?;
|
||||
|
||||
@@ -174,13 +192,16 @@ impl SyncEngine {
|
||||
// Upload manifest.json last for atomicity
|
||||
self.upload_manifest(&profile_id, &local_manifest).await?;
|
||||
|
||||
// Sync associated proxy and group
|
||||
// Sync associated proxy, group, and VPN
|
||||
if let Some(proxy_id) = &profile.proxy_id {
|
||||
let _ = self.sync_proxy(proxy_id, Some(app_handle)).await;
|
||||
}
|
||||
if let Some(group_id) = &profile.group_id {
|
||||
let _ = self.sync_group(group_id, Some(app_handle)).await;
|
||||
}
|
||||
if let Some(vpn_id) = &profile.vpn_id {
|
||||
let _ = self.sync_vpn(vpn_id, Some(app_handle)).await;
|
||||
}
|
||||
|
||||
// Update profile last_sync
|
||||
let mut updated_profile = profile.clone();
|
||||
@@ -785,6 +806,145 @@ impl SyncEngine {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn sync_vpn(&self, vpn_id: &str, app_handle: Option<&tauri::AppHandle>) -> SyncResult<()> {
|
||||
let local_vpn = {
|
||||
let storage = crate::vpn::VPN_STORAGE.lock().unwrap();
|
||||
storage.load_config(vpn_id).ok()
|
||||
};
|
||||
|
||||
let remote_key = format!("vpns/{}.json", vpn_id);
|
||||
let stat = self.client.stat(&remote_key).await?;
|
||||
|
||||
match (local_vpn, stat.exists) {
|
||||
(Some(vpn), true) => {
|
||||
let local_updated = vpn.last_sync.unwrap_or(0);
|
||||
let remote_updated: DateTime<Utc> = stat
|
||||
.last_modified
|
||||
.as_ref()
|
||||
.and_then(|s| DateTime::parse_from_rfc3339(s).ok())
|
||||
.map(|dt| dt.with_timezone(&Utc))
|
||||
.unwrap_or_else(Utc::now);
|
||||
let remote_ts = remote_updated.timestamp() as u64;
|
||||
|
||||
if remote_ts > local_updated {
|
||||
self.download_vpn(vpn_id, app_handle).await?;
|
||||
} else if local_updated > remote_ts {
|
||||
self.upload_vpn(&vpn).await?;
|
||||
}
|
||||
}
|
||||
(Some(vpn), false) => {
|
||||
self.upload_vpn(&vpn).await?;
|
||||
}
|
||||
(None, true) => {
|
||||
self.download_vpn(vpn_id, app_handle).await?;
|
||||
}
|
||||
(None, false) => {
|
||||
log::debug!("VPN {} not found locally or remotely", vpn_id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn upload_vpn(&self, vpn: &crate::vpn::VpnConfig) -> SyncResult<()> {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
let mut updated_vpn = vpn.clone();
|
||||
updated_vpn.last_sync = Some(now);
|
||||
|
||||
let json = serde_json::to_string_pretty(&updated_vpn)
|
||||
.map_err(|e| SyncError::SerializationError(format!("Failed to serialize VPN: {e}")))?;
|
||||
|
||||
let remote_key = format!("vpns/{}.json", vpn.id);
|
||||
let presign = self
|
||||
.client
|
||||
.presign_upload(&remote_key, Some("application/json"))
|
||||
.await?;
|
||||
self
|
||||
.client
|
||||
.upload_bytes(&presign.url, json.as_bytes(), Some("application/json"))
|
||||
.await?;
|
||||
|
||||
// Update local VPN with new last_sync
|
||||
{
|
||||
let storage = crate::vpn::VPN_STORAGE.lock().unwrap();
|
||||
if let Err(e) = storage.update_sync_fields(&vpn.id, vpn.sync_enabled, Some(now)) {
|
||||
log::warn!("Failed to update VPN last_sync: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("VPN {} uploaded", vpn.id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn download_vpn(
|
||||
&self,
|
||||
vpn_id: &str,
|
||||
app_handle: Option<&tauri::AppHandle>,
|
||||
) -> SyncResult<()> {
|
||||
let remote_key = format!("vpns/{}.json", vpn_id);
|
||||
let presign = self.client.presign_download(&remote_key).await?;
|
||||
let data = self.client.download_bytes(&presign.url).await?;
|
||||
|
||||
let mut vpn: crate::vpn::VpnConfig = serde_json::from_slice(&data)
|
||||
.map_err(|e| SyncError::SerializationError(format!("Failed to parse VPN JSON: {e}")))?;
|
||||
|
||||
vpn.last_sync = Some(
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs(),
|
||||
);
|
||||
vpn.sync_enabled = true;
|
||||
|
||||
// Save via VPN storage (handles encryption)
|
||||
{
|
||||
let storage = crate::vpn::VPN_STORAGE.lock().unwrap();
|
||||
if let Err(e) = storage.save_config(&vpn) {
|
||||
log::warn!("Failed to save downloaded VPN: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Emit event for UI update
|
||||
if let Some(_handle) = app_handle {
|
||||
let _ = events::emit("vpn-configs-changed", ());
|
||||
let _ = events::emit(
|
||||
"vpn-sync-status",
|
||||
serde_json::json!({
|
||||
"id": vpn_id,
|
||||
"status": "synced"
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
log::info!("VPN {} downloaded", vpn_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn sync_vpn_by_id_with_handle(
|
||||
&self,
|
||||
vpn_id: &str,
|
||||
app_handle: &tauri::AppHandle,
|
||||
) -> SyncResult<()> {
|
||||
self.sync_vpn(vpn_id, Some(app_handle)).await
|
||||
}
|
||||
|
||||
pub async fn delete_vpn(&self, vpn_id: &str) -> SyncResult<()> {
|
||||
let remote_key = format!("vpns/{}.json", vpn_id);
|
||||
let tombstone_key = format!("tombstones/vpns/{}.json", vpn_id);
|
||||
|
||||
self
|
||||
.client
|
||||
.delete(&remote_key, Some(&tombstone_key))
|
||||
.await?;
|
||||
|
||||
log::info!("VPN {} deleted from sync", vpn_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Download a profile from S3 if it exists remotely but not locally
|
||||
pub async fn download_profile_if_missing(
|
||||
&self,
|
||||
@@ -901,6 +1061,21 @@ impl SyncEngine {
|
||||
})?;
|
||||
|
||||
// Download all files from manifest
|
||||
let total_size: u64 = manifest.files.iter().map(|f| f.size).sum();
|
||||
log::info!(
|
||||
"Profile {} recovery: downloading {} files ({} bytes total)",
|
||||
profile_id,
|
||||
manifest.files.len(),
|
||||
total_size
|
||||
);
|
||||
for file in &manifest.files {
|
||||
log::info!(
|
||||
" -> {} ({} bytes, hash: {})",
|
||||
file.path,
|
||||
file.size,
|
||||
file.hash
|
||||
);
|
||||
}
|
||||
if !manifest.files.is_empty() {
|
||||
self
|
||||
.download_profile_files(app_handle, profile_id, &profile_dir, &manifest.files)
|
||||
@@ -1100,6 +1275,43 @@ pub async fn enable_proxy_sync_if_needed(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if VPN is used by any synced profile
|
||||
pub fn is_vpn_used_by_synced_profile(vpn_id: &str) -> bool {
|
||||
let profile_manager = ProfileManager::instance();
|
||||
if let Ok(profiles) = profile_manager.list_profiles() {
|
||||
profiles
|
||||
.iter()
|
||||
.any(|p| p.sync_enabled && p.vpn_id.as_deref() == Some(vpn_id))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable sync for VPN if not already enabled
|
||||
pub async fn enable_vpn_sync_if_needed(
|
||||
vpn_id: &str,
|
||||
_app_handle: &tauri::AppHandle,
|
||||
) -> Result<(), String> {
|
||||
let vpn = {
|
||||
let storage = crate::vpn::VPN_STORAGE.lock().unwrap();
|
||||
storage
|
||||
.load_config(vpn_id)
|
||||
.map_err(|e| format!("VPN with ID '{vpn_id}' not found: {e}"))?
|
||||
};
|
||||
|
||||
if !vpn.sync_enabled {
|
||||
let storage = crate::vpn::VPN_STORAGE.lock().unwrap();
|
||||
storage
|
||||
.update_sync_fields(vpn_id, true, None)
|
||||
.map_err(|e| format!("Failed to enable VPN sync: {e}"))?;
|
||||
|
||||
let _ = events::emit("vpn-configs-changed", ());
|
||||
log::info!("Auto-enabled sync for VPN {}", vpn_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enable sync for group if not already enabled
|
||||
pub async fn enable_group_sync_if_needed(
|
||||
group_id: &str,
|
||||
@@ -1197,10 +1409,6 @@ pub async fn set_profile_sync_enabled(
|
||||
|
||||
profile.sync_enabled = enabled;
|
||||
|
||||
if !enabled {
|
||||
profile.last_sync = None;
|
||||
}
|
||||
|
||||
profile_manager
|
||||
.save_profile(&profile)
|
||||
.map_err(|e| format!("Failed to save profile: {e}"))?;
|
||||
@@ -1240,6 +1448,13 @@ pub async fn set_profile_sync_enabled(
|
||||
scheduler.queue_group_sync(group_id.clone()).await;
|
||||
}
|
||||
}
|
||||
if let Some(ref vpn_id) = profile.vpn_id {
|
||||
if let Err(e) = enable_vpn_sync_if_needed(vpn_id, &app_handle).await {
|
||||
log::warn!("Failed to enable sync for VPN {}: {}", vpn_id, e);
|
||||
} else {
|
||||
scheduler.queue_vpn_sync(vpn_id.clone()).await;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::warn!("Scheduler not initialized, sync will not start");
|
||||
}
|
||||
@@ -1526,3 +1741,85 @@ pub fn is_proxy_in_use_by_synced_profile(proxy_id: String) -> bool {
|
||||
pub fn is_group_in_use_by_synced_profile(group_id: String) -> bool {
|
||||
is_group_used_by_synced_profile(&group_id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_vpn_sync_enabled(
|
||||
app_handle: tauri::AppHandle,
|
||||
vpn_id: String,
|
||||
enabled: bool,
|
||||
) -> Result<(), String> {
|
||||
let vpn = {
|
||||
let storage = crate::vpn::VPN_STORAGE.lock().unwrap();
|
||||
storage
|
||||
.load_config(&vpn_id)
|
||||
.map_err(|e| format!("VPN with ID '{vpn_id}' not found: {e}"))?
|
||||
};
|
||||
|
||||
// If disabling, check if VPN is used by any synced profile
|
||||
if !enabled && is_vpn_used_by_synced_profile(&vpn_id) {
|
||||
return Err("Sync cannot be disabled while this VPN is used by synced profiles".to_string());
|
||||
}
|
||||
|
||||
// If enabling, check that sync settings are configured
|
||||
if enabled {
|
||||
let cloud_logged_in = crate::cloud_auth::CLOUD_AUTH.is_logged_in().await;
|
||||
|
||||
if !cloud_logged_in {
|
||||
let manager = SettingsManager::instance();
|
||||
let settings = manager
|
||||
.load_settings()
|
||||
.map_err(|e| format!("Failed to load settings: {e}"))?;
|
||||
|
||||
if settings.sync_server_url.is_none() {
|
||||
return Err(
|
||||
"Sync server not configured. Please configure sync settings first.".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let token = manager.get_sync_token(&app_handle).await.ok().flatten();
|
||||
if token.is_none() {
|
||||
return Err("Sync token not configured. Please configure sync settings first.".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let last_sync = if enabled { vpn.last_sync } else { None };
|
||||
|
||||
{
|
||||
let storage = crate::vpn::VPN_STORAGE.lock().unwrap();
|
||||
storage
|
||||
.update_sync_fields(&vpn_id, enabled, last_sync)
|
||||
.map_err(|e| format!("Failed to update VPN sync: {e}"))?;
|
||||
}
|
||||
|
||||
let _ = events::emit("vpn-configs-changed", ());
|
||||
|
||||
if enabled {
|
||||
let _ = events::emit(
|
||||
"vpn-sync-status",
|
||||
serde_json::json!({
|
||||
"id": vpn_id,
|
||||
"status": "syncing"
|
||||
}),
|
||||
);
|
||||
|
||||
if let Some(scheduler) = super::get_global_scheduler() {
|
||||
scheduler.queue_vpn_sync(vpn_id).await;
|
||||
}
|
||||
} else {
|
||||
let _ = events::emit(
|
||||
"vpn-sync-status",
|
||||
serde_json::json!({
|
||||
"id": vpn_id,
|
||||
"status": "disabled"
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn is_vpn_in_use_by_synced_profile(vpn_id: String) -> bool {
|
||||
is_vpn_used_by_synced_profile(&vpn_id)
|
||||
}
|
||||
|
||||
@@ -7,11 +7,12 @@ pub mod types;
|
||||
|
||||
pub use client::SyncClient;
|
||||
pub use engine::{
|
||||
enable_group_sync_if_needed, enable_proxy_sync_if_needed, is_group_in_use_by_synced_profile,
|
||||
is_group_used_by_synced_profile, is_proxy_in_use_by_synced_profile,
|
||||
is_proxy_used_by_synced_profile, request_profile_sync, set_group_sync_enabled,
|
||||
set_profile_sync_enabled, set_proxy_sync_enabled, sync_profile, trigger_sync_for_profile,
|
||||
SyncEngine,
|
||||
enable_group_sync_if_needed, enable_proxy_sync_if_needed, enable_vpn_sync_if_needed,
|
||||
is_group_in_use_by_synced_profile, is_group_used_by_synced_profile,
|
||||
is_proxy_in_use_by_synced_profile, is_proxy_used_by_synced_profile,
|
||||
is_vpn_in_use_by_synced_profile, is_vpn_used_by_synced_profile, request_profile_sync,
|
||||
set_group_sync_enabled, set_profile_sync_enabled, set_proxy_sync_enabled, set_vpn_sync_enabled,
|
||||
sync_profile, trigger_sync_for_profile, SyncEngine,
|
||||
};
|
||||
pub use manifest::{compute_diff, generate_manifest, HashCache, ManifestDiff, SyncManifest};
|
||||
pub use scheduler::{get_global_scheduler, set_global_scheduler, SyncScheduler};
|
||||
|
||||
@@ -34,6 +34,7 @@ pub struct SyncScheduler {
|
||||
pending_profiles: Arc<Mutex<HashMap<String, ProfileStopTime>>>,
|
||||
pending_proxies: Arc<Mutex<HashSet<String>>>,
|
||||
pending_groups: Arc<Mutex<HashSet<String>>>,
|
||||
pending_vpns: Arc<Mutex<HashSet<String>>>,
|
||||
pending_tombstones: Arc<Mutex<Vec<(String, String)>>>,
|
||||
running_profiles: Arc<Mutex<HashSet<String>>>,
|
||||
in_flight_profiles: Arc<Mutex<HashSet<String>>>,
|
||||
@@ -52,6 +53,7 @@ impl SyncScheduler {
|
||||
pending_profiles: Arc::new(Mutex::new(HashMap::new())),
|
||||
pending_proxies: Arc::new(Mutex::new(HashSet::new())),
|
||||
pending_groups: Arc::new(Mutex::new(HashSet::new())),
|
||||
pending_vpns: Arc::new(Mutex::new(HashSet::new())),
|
||||
pending_tombstones: Arc::new(Mutex::new(Vec::new())),
|
||||
running_profiles: Arc::new(Mutex::new(HashSet::new())),
|
||||
in_flight_profiles: Arc::new(Mutex::new(HashSet::new())),
|
||||
@@ -92,6 +94,12 @@ impl SyncScheduler {
|
||||
}
|
||||
drop(pending_groups);
|
||||
|
||||
let pending_vpns = self.pending_vpns.lock().await;
|
||||
if !pending_vpns.is_empty() {
|
||||
return true;
|
||||
}
|
||||
drop(pending_vpns);
|
||||
|
||||
let pending_tombstones = self.pending_tombstones.lock().await;
|
||||
if !pending_tombstones.is_empty() {
|
||||
return true;
|
||||
@@ -190,6 +198,11 @@ impl SyncScheduler {
|
||||
pending.insert(proxy_id);
|
||||
}
|
||||
|
||||
pub async fn queue_vpn_sync(&self, vpn_id: String) {
|
||||
let mut pending = self.pending_vpns.lock().await;
|
||||
pending.insert(vpn_id);
|
||||
}
|
||||
|
||||
pub async fn queue_group_sync(&self, group_id: String) {
|
||||
let mut pending = self.pending_groups.lock().await;
|
||||
pending.insert(group_id);
|
||||
@@ -269,6 +282,7 @@ impl SyncScheduler {
|
||||
SyncWorkItem::Profile(id) => scheduler.queue_profile_sync(id).await,
|
||||
SyncWorkItem::Proxy(id) => scheduler.queue_proxy_sync(id).await,
|
||||
SyncWorkItem::Group(id) => scheduler.queue_group_sync(id).await,
|
||||
SyncWorkItem::Vpn(id) => scheduler.queue_vpn_sync(id).await,
|
||||
SyncWorkItem::Tombstone(entity_type, entity_id) => {
|
||||
scheduler.queue_tombstone(entity_type, entity_id).await
|
||||
}
|
||||
@@ -288,6 +302,7 @@ impl SyncScheduler {
|
||||
self.process_pending_profiles(app_handle).await;
|
||||
self.process_pending_proxies(app_handle).await;
|
||||
self.process_pending_groups(app_handle).await;
|
||||
self.process_pending_vpns(app_handle).await;
|
||||
self.process_pending_tombstones(app_handle).await;
|
||||
}
|
||||
|
||||
@@ -366,6 +381,7 @@ impl SyncScheduler {
|
||||
&& self.pending_profiles.lock().await.is_empty()
|
||||
&& self.pending_proxies.lock().await.is_empty()
|
||||
&& self.pending_groups.lock().await.is_empty()
|
||||
&& self.pending_vpns.lock().await.is_empty()
|
||||
};
|
||||
|
||||
match result {
|
||||
@@ -537,6 +553,68 @@ impl SyncScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_pending_vpns(&self, app_handle: &tauri::AppHandle) {
|
||||
let vpns_to_sync: Vec<String> = {
|
||||
let mut pending = self.pending_vpns.lock().await;
|
||||
let list: Vec<String> = pending.drain().collect();
|
||||
list
|
||||
};
|
||||
|
||||
if vpns_to_sync.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
match SyncEngine::create_from_settings(app_handle).await {
|
||||
Ok(engine) => {
|
||||
for vpn_id in vpns_to_sync {
|
||||
log::info!("Syncing VPN {}", vpn_id);
|
||||
let _ = events::emit(
|
||||
"vpn-sync-status",
|
||||
serde_json::json!({
|
||||
"id": vpn_id,
|
||||
"status": "syncing"
|
||||
}),
|
||||
);
|
||||
match engine.sync_vpn_by_id_with_handle(&vpn_id, app_handle).await {
|
||||
Ok(()) => {
|
||||
let _ = events::emit(
|
||||
"vpn-sync-status",
|
||||
serde_json::json!({
|
||||
"id": vpn_id,
|
||||
"status": "synced"
|
||||
}),
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to sync VPN {}: {}", vpn_id, e);
|
||||
let _ = events::emit(
|
||||
"vpn-sync-status",
|
||||
serde_json::json!({
|
||||
"id": vpn_id,
|
||||
"status": "error"
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !self.is_sync_in_progress().await {
|
||||
log::debug!("All syncs completed after VPN sync, triggering cleanup");
|
||||
let registry =
|
||||
crate::downloaded_browsers_registry::DownloadedBrowsersRegistry::instance();
|
||||
if let Err(e) = registry.cleanup_unused_binaries() {
|
||||
log::warn!("Cleanup after sync failed: {e}");
|
||||
} else {
|
||||
log::debug!("Cleanup after sync completed successfully");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to create sync engine: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_pending_tombstones(&self, app_handle: &tauri::AppHandle) {
|
||||
let tombstones: Vec<(String, String)> = {
|
||||
let mut pending = self.pending_tombstones.lock().await;
|
||||
@@ -607,6 +685,12 @@ impl SyncScheduler {
|
||||
entity_id
|
||||
);
|
||||
}
|
||||
"vpn" => {
|
||||
log::debug!(
|
||||
"VPN tombstone for {} - local deletion not implemented",
|
||||
entity_id
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ pub enum SyncWorkItem {
|
||||
Profile(String),
|
||||
Proxy(String),
|
||||
Group(String),
|
||||
Vpn(String),
|
||||
Tombstone(String, String),
|
||||
}
|
||||
|
||||
@@ -229,6 +230,11 @@ impl SyncSubscription {
|
||||
.strip_prefix("groups/")
|
||||
.and_then(|s| s.strip_suffix(".json"))
|
||||
.map(|s| SyncWorkItem::Group(s.to_string()))
|
||||
} else if key.starts_with("vpns/") {
|
||||
key
|
||||
.strip_prefix("vpns/")
|
||||
.and_then(|s| s.strip_suffix(".json"))
|
||||
.map(|s| SyncWorkItem::Vpn(s.to_string()))
|
||||
} else if key.starts_with("tombstones/") {
|
||||
key.strip_prefix("tombstones/").and_then(|rest| {
|
||||
if rest.starts_with("profiles/") {
|
||||
@@ -246,6 +252,11 @@ impl SyncSubscription {
|
||||
.strip_prefix("groups/")
|
||||
.and_then(|s| s.strip_suffix(".json"))
|
||||
.map(|id| SyncWorkItem::Tombstone("group".to_string(), id.to_string()))
|
||||
} else if rest.starts_with("vpns/") {
|
||||
rest
|
||||
.strip_prefix("vpns/")
|
||||
.and_then(|s| s.strip_suffix(".json"))
|
||||
.map(|id| SyncWorkItem::Tombstone("vpn".to_string(), id.to_string()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user