mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-05-12 20:52:21 +02:00
refactor: make sync more robust
This commit is contained in:
Vendored
+6
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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/**",
|
||||
];
|
||||
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user