refactor: make sync more robust

This commit is contained in:
zhom
2026-03-24 00:04:39 +04:00
parent 861d301451
commit 7092f2155b
7 changed files with 198 additions and 121 deletions
+6
View File
@@ -10,6 +10,7 @@
"appindicator",
"applescript",
"asyncio",
"autocheckpoint",
"autoconfig",
"autologin",
"biomejs",
@@ -95,6 +96,7 @@
"langpack",
"launchservices",
"letterboxing",
"leveldb",
"libappindicator",
"libatk",
"libayatana",
@@ -114,6 +116,7 @@
"macchiato",
"Matchalk",
"maxminddb",
"minidumps",
"minioadmin",
"mmdb",
"mountpoint",
@@ -155,6 +158,7 @@
"plasmohq",
"platformdirs",
"prefs",
"presign",
"PRIO",
"propertylist",
"psutil",
@@ -179,6 +183,7 @@
"rusqlite",
"rustc",
"rwxr",
"safebrowsing",
"SARIF",
"scipy",
"screeninfo",
@@ -222,6 +227,7 @@
"titlebar",
"tkinter",
"tmpfs",
"tombstoned",
"tqdm",
"trackingprotection",
"trailhead",
-45
View File
@@ -94,29 +94,6 @@ impl ProfileManager {
crate::camoufox_manager::CamoufoxConfig::default()
});
// Always ensure executable_path is set to the user's binary location
if config.executable_path.is_none() {
let mut browser_dir = self.get_binaries_dir();
browser_dir.push(browser);
browser_dir.push(version);
#[cfg(target_os = "macos")]
let binary_path = browser_dir
.join("Camoufox.app")
.join("Contents")
.join("MacOS")
.join("camoufox");
#[cfg(target_os = "windows")]
let binary_path = browser_dir.join("camoufox.exe");
#[cfg(target_os = "linux")]
let binary_path = browser_dir.join("camoufox");
config.executable_path = Some(binary_path.to_string_lossy().to_string());
log::info!("Set Camoufox executable path: {:?}", config.executable_path);
}
// Pass upstream proxy information to config for fingerprint generation
if let Some(proxy_id_ref) = &proxy_id {
if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) {
@@ -219,28 +196,6 @@ impl ProfileManager {
});
// Always ensure executable_path is set to the user's binary location
if config.executable_path.is_none() {
let mut browser_dir = self.get_binaries_dir();
browser_dir.push(browser);
browser_dir.push(version);
#[cfg(target_os = "macos")]
let binary_path = browser_dir
.join("Chromium.app")
.join("Contents")
.join("MacOS")
.join("Chromium");
#[cfg(target_os = "windows")]
let binary_path = browser_dir.join("chrome.exe");
#[cfg(target_os = "linux")]
let binary_path = browser_dir.join("chrome");
config.executable_path = Some(binary_path.to_string_lossy().to_string());
log::info!("Set Wayfern executable path: {:?}", config.executable_path);
}
// Pass upstream proxy information to config for fingerprint generation
if let Some(proxy_id_ref) = &proxy_id {
if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) {
+95 -2
View File
@@ -332,11 +332,11 @@ impl SyncEngine {
) -> SyncResult<()> {
if profile.is_cross_os() {
log::info!(
"Skipping file sync for cross-OS profile: {} ({})",
"Cross-OS profile: {} ({}) — syncing metadata only",
profile.name,
profile.id
);
return Ok(());
return self.sync_cross_os_metadata(app_handle, profile).await;
}
// Skip team profiles for self-hosted sync
@@ -727,6 +727,63 @@ impl SyncEngine {
Ok(profile)
}
/// Sync only metadata for cross-OS profiles (tags, notes, proxies, groups).
/// No browser files are synced.
async fn sync_cross_os_metadata(
&self,
app_handle: &tauri::AppHandle,
profile: &BrowserProfile,
) -> SyncResult<()> {
let profile_id = profile.id.to_string();
let key_prefix = Self::get_team_key_prefix(profile).await;
let profile_manager = ProfileManager::instance();
// Upload our metadata
self
.upload_profile_metadata(&profile_id, profile, &key_prefix)
.await?;
// Download remote metadata and merge if remote has changes
let remote_metadata_key = format!("{}profiles/{}/metadata.json", key_prefix, profile_id);
if let Ok(remote_meta) = self.download_profile_metadata(&remote_metadata_key).await {
let mut updated = profile.clone();
updated.name = remote_meta.name;
updated.tags = remote_meta.tags;
updated.note = remote_meta.note;
updated.proxy_id = remote_meta.proxy_id;
updated.vpn_id = remote_meta.vpn_id;
updated.group_id = remote_meta.group_id;
updated.last_sync = Some(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
);
let _ = profile_manager.save_profile(&updated);
}
// Sync associated entities
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;
}
let _ = events::emit("profiles-changed", ());
let _ = events::emit(
"profile-sync-status",
serde_json::json!({
"profile_id": profile_id,
"profile_name": profile.name,
"status": "synced"
}),
);
log::info!("Cross-OS profile {} metadata synced", profile_id);
Ok(())
}
async fn upload_profile_metadata(
&self,
profile_id: &str,
@@ -2284,6 +2341,42 @@ impl SyncEngine {
.await?;
}
// Verify critical files after download
let os_crypt_key_path = profile_dir.join("profile").join("os_crypt_key");
let cookies_path = profile_dir.join("profile").join("Default").join("Cookies");
if os_crypt_key_path.exists() {
let key_data = fs::read(&os_crypt_key_path).unwrap_or_default();
log::info!(
"Profile {} sync: os_crypt_key present ({} bytes, sha256: {:x})",
profile_id,
key_data.len(),
{
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
key_data.hash(&mut h);
h.finish()
}
);
} else {
log::warn!(
"Profile {} sync: os_crypt_key NOT FOUND after download",
profile_id
);
}
if cookies_path.exists() {
let cookies_meta = fs::metadata(&cookies_path).unwrap_or_else(|_| fs::metadata(".").unwrap());
log::info!(
"Profile {} sync: Cookies present ({} bytes)",
profile_id,
cookies_meta.len()
);
} else {
log::warn!(
"Profile {} sync: Cookies NOT FOUND after download",
profile_id
);
}
// Set sync mode and save profile
if profile.sync_mode == SyncMode::Disabled {
profile.sync_mode = if manifest.encrypted {
+8 -4
View File
@@ -13,7 +13,6 @@ use super::types::{SyncError, SyncResult};
/// Patterns use `**/` prefix to match at any directory depth, since the sync
/// engine scans from `profiles/{uuid}/` which contains `profile/Default/...`.
pub const DEFAULT_EXCLUDE_PATTERNS: &[&str] = &[
// Chromium caches (re-downloadable / re-generated)
"**/Cache/**",
"**/Code Cache/**",
"**/GPUCache/**",
@@ -23,7 +22,6 @@ pub const DEFAULT_EXCLUDE_PATTERNS: &[&str] = &[
"**/DawnGraphiteCache/**",
"**/Service Worker/CacheStorage/**",
"**/Service Worker/ScriptCache/**",
// Chromium transient / volatile data
"**/Session Storage/**",
"**/blob_storage/**",
"**/Crashpad/**",
@@ -32,14 +30,12 @@ pub const DEFAULT_EXCLUDE_PATTERNS: &[&str] = &[
"**/optimization_guide_model_store/**",
"**/Safe Browsing/**",
"**/component_crx_cache/**",
// Firefox/Camoufox caches (re-downloadable / re-generated)
"**/cache2/**",
"**/startupCache/**",
"**/safebrowsing/**",
"**/storage/temporary/**",
"**/crashes/**",
"**/minidumps/**",
// Common volatile files
"*.log",
"*.tmp",
"**/LOG",
@@ -47,6 +43,14 @@ pub const DEFAULT_EXCLUDE_PATTERNS: &[&str] = &[
"**/LOCK",
"**/*-journal",
"**/*-wal",
"**/SingletonLock",
"**/SingletonSocket",
"**/SingletonCookie",
"**/Secure Preferences",
"**/GraphiteDawnCache/**",
"**/DawnWebGPUCache/**",
"**/BrowserMetrics*",
"**/.DS_Store",
".donut-sync/**",
];
+2 -2
View File
@@ -264,7 +264,7 @@ impl SyncScheduler {
let sync_enabled_profiles: Vec<_> = profiles
.into_iter()
.filter(|p| p.is_sync_enabled() && !p.is_cross_os())
.filter(|p| p.is_sync_enabled())
.collect();
if sync_enabled_profiles.is_empty() {
@@ -418,7 +418,7 @@ impl SyncScheduler {
profile_manager.list_profiles().ok().and_then(|profiles| {
profiles
.into_iter()
.find(|p| p.id.to_string() == profile_id && p.is_sync_enabled() && !p.is_cross_os())
.find(|p| p.id.to_string() == profile_id && p.is_sync_enabled())
})
};
+84 -33
View File
@@ -37,8 +37,6 @@ pub struct WayfernConfig {
pub block_webrtc: Option<bool>,
#[serde(default)]
pub block_webgl: Option<bool>,
#[serde(default)]
pub executable_path: Option<String>,
#[serde(default, skip_serializing)]
pub proxy: Option<String>,
}
@@ -212,21 +210,9 @@ impl WayfernManager {
profile: &BrowserProfile,
config: &WayfernConfig,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let executable_path = if let Some(path) = &config.executable_path {
let p = PathBuf::from(path);
if p.exists() {
p
} else {
log::warn!("Stored Wayfern executable path does not exist: {path}, falling back to dynamic resolution");
BrowserRunner::instance()
.get_browser_executable_path(profile)
.map_err(|e| format!("Failed to get Wayfern executable path: {e}"))?
}
} else {
BrowserRunner::instance()
.get_browser_executable_path(profile)
.map_err(|e| format!("Failed to get Wayfern executable path: {e}"))?
};
let executable_path = BrowserRunner::instance()
.get_browser_executable_path(profile)
.map_err(|e| format!("Failed to get Wayfern executable path: {e}"))?;
let port = Self::find_free_port().await?;
log::info!("Launching headless Wayfern on port {port} for fingerprint generation");
@@ -456,21 +442,9 @@ impl WayfernManager {
extension_paths: &[String],
remote_debugging_port: Option<u16>,
) -> Result<WayfernLaunchResult, Box<dyn std::error::Error + Send + Sync>> {
let executable_path = if let Some(path) = &config.executable_path {
let p = PathBuf::from(path);
if p.exists() {
p
} else {
log::warn!("Stored Wayfern executable path does not exist: {path}, falling back to dynamic resolution");
BrowserRunner::instance()
.get_browser_executable_path(profile)
.map_err(|e| format!("Failed to get Wayfern executable path: {e}"))?
}
} else {
BrowserRunner::instance()
.get_browser_executable_path(profile)
.map_err(|e| format!("Failed to get Wayfern executable path: {e}"))?
};
let executable_path = BrowserRunner::instance()
.get_browser_executable_path(profile)
.map_err(|e| format!("Failed to get Wayfern executable path: {e}"))?;
let port = match remote_debugging_port {
Some(p) => p,
@@ -478,6 +452,84 @@ impl WayfernManager {
};
log::info!("Launching Wayfern on CDP port {port}");
// Diagnostic: verify critical profile files and test cookie decryption
{
let profile_path_buf = std::path::PathBuf::from(profile_path);
let key_path = profile_path_buf.join("os_crypt_key");
let cookies_path = profile_path_buf.join("Default").join("Cookies");
if key_path.exists() {
let key_text = std::fs::read_to_string(&key_path).unwrap_or_default();
log::info!(
"Pre-launch: os_crypt_key present ({} bytes, content: '{}')",
key_text.len(),
key_text.trim()
);
} else {
log::warn!("Pre-launch: os_crypt_key NOT FOUND");
}
if cookies_path.exists() {
// Try to open Cookies DB and check if encrypted cookies can be decrypted
if let Ok(conn) = rusqlite::Connection::open_with_flags(
&cookies_path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
) {
let cookie_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM cookies WHERE length(encrypted_value) > 0",
[],
|r| r.get(0),
)
.unwrap_or(0);
let total_count: i64 = conn
.query_row("SELECT COUNT(*) FROM cookies", [], |r| r.get(0))
.unwrap_or(0);
log::info!(
"Pre-launch: Cookies DB has {} total cookies, {} encrypted",
total_count,
cookie_count
);
// Try decrypting one cookie using the cookie_manager
if let Some(encryption_key) =
crate::cookie_manager::chrome_decrypt::get_encryption_key(&profile_path_buf)
{
if let Ok(mut stmt) = conn.prepare(
"SELECT name, host_key, encrypted_value FROM cookies WHERE length(encrypted_value) > 0 LIMIT 1",
) {
if let Ok(mut rows) = stmt.query([]) {
if let Ok(Some(row)) = rows.next() {
let name: String = row.get(0).unwrap_or_default();
let host: String = row.get(1).unwrap_or_default();
let encrypted: Vec<u8> = row.get(2).unwrap_or_default();
let decrypted =
crate::cookie_manager::chrome_decrypt::decrypt(
&encrypted,
&encryption_key,
);
match decrypted {
Some(val) => log::info!(
"Pre-launch: Cookie decryption SUCCEEDED for '{}' (host: {}, decrypted {} bytes)",
name, host, val.len()
),
None => log::error!(
"Pre-launch: Cookie decryption FAILED for '{}' (host: {}, encrypted {} bytes)",
name, host, encrypted.len()
),
}
}
}
}
} else {
log::error!("Pre-launch: Failed to derive encryption key from os_crypt_key");
}
}
} else {
log::warn!("Pre-launch: Cookies NOT FOUND");
}
}
let mut args = vec![
format!("--remote-debugging-port={port}"),
"--remote-debugging-address=127.0.0.1".to_string(),
@@ -492,7 +544,6 @@ impl WayfernManager {
"--disable-session-crashed-bubble".to_string(),
"--hide-crash-restore-bubble".to_string(),
"--disable-infobars".to_string(),
"--disable-quic".to_string(),
"--disable-features=DialMediaRouteProvider".to_string(),
"--use-mock-keychain".to_string(),
"--password-store=basic".to_string(),
+3 -35
View File
@@ -67,7 +67,9 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
const [isVerifying, setIsVerifying] = useState(false);
const [activeTab, setActiveTab] = useState<string>("cloud");
const [liveProxyUsage, setLiveProxyUsage] = useState<ProxyUsage | null>(null);
const [_liveProxyUsage, setLiveProxyUsage] = useState<ProxyUsage | null>(
null,
);
const [connectionStatus, setConnectionStatus] = useState<
"unknown" | "testing" | "connected" | "error"
@@ -300,40 +302,6 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
})}
</span>
</div>
{liveProxyUsage && (
<>
<div className="flex justify-between">
<span className="text-muted-foreground">
Recurring Proxy Bandwidth
</span>
<span>
{Math.max(
0,
liveProxyUsage.recurring_limit_mb -
liveProxyUsage.used_mb,
)}{" "}
/ {liveProxyUsage.recurring_limit_mb} MB remaining
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">
Extra Proxy Bandwidth
</span>
<span>
{Math.max(
0,
liveProxyUsage.remaining_mb -
Math.max(
0,
liveProxyUsage.recurring_limit_mb -
liveProxyUsage.used_mb,
),
)}{" "}
/ {liveProxyUsage.extra_limit_mb} MB remaining
</span>
</div>
</>
)}
{user.teamName && (
<>
<div className="flex justify-between">