diff --git a/.vscode/settings.json b/.vscode/settings.json index 4225b89..c0ee74e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "cSpell.words": [ + "ABORTIFHUNG", "adwaita", "ahooks", "akhilmhdh", @@ -83,6 +84,7 @@ "libwebkit", "libxdo", "localtime", + "lpdw", "lxml", "lzma", "Matchalk", @@ -114,6 +116,7 @@ "peerconnection", "pids", "pixbuf", + "pkexec", "pkill", "plasmohq", "platformdirs", @@ -136,6 +139,7 @@ "screeninfo", "selectables", "serde", + "SETTINGCHANGE", "setuptools", "shadcn", "showcursor", @@ -143,6 +147,7 @@ "signon", "signum", "sklearn", + "SMTO", "sonner", "splitn", "sspi", diff --git a/src-tauri/src/api_server.rs b/src-tauri/src/api_server.rs index db70447..2a35e1d 100644 --- a/src-tauri/src/api_server.rs +++ b/src-tauri/src/api_server.rs @@ -1,3 +1,4 @@ +use crate::camoufox_manager::CamoufoxConfig; use crate::group_manager::GROUP_MANAGER; use crate::profile::manager::ProfileManager; use crate::proxy_manager::PROXY_MANAGER; @@ -509,8 +510,7 @@ async fn update_profile( } if let Some(camoufox_config) = request.camoufox_config { - let config: Result = - serde_json::from_value(camoufox_config); + let config: Result = serde_json::from_value(camoufox_config); match config { Ok(config) => { if profile_manager @@ -830,15 +830,12 @@ async fn download_browser_api( State(state): State, Json(request): Json, ) -> Result, StatusCode> { - let browser_runner = crate::browser_runner::BrowserRunner::instance(); - - match browser_runner - .download_browser_impl( - state.app_handle.clone(), - request.browser.clone(), - request.version.clone(), - ) - .await + match crate::downloader::download_browser( + state.app_handle.clone(), + request.browser.clone(), + request.version.clone(), + ) + .await { Ok(_) => Ok(Json(DownloadBrowserResponse { browser: request.browser, @@ -870,7 +867,6 @@ async fn check_browser_downloaded( Path((browser, version)): Path<(String, String)>, State(_state): State, ) -> Result, StatusCode> { - let browser_runner = crate::browser_runner::BrowserRunner::instance(); - let is_downloaded = browser_runner.is_browser_downloaded(&browser, &version); + let is_downloaded = crate::downloaded_browsers_registry::is_browser_downloaded(browser, version); Ok(Json(is_downloaded)) } diff --git a/src-tauri/src/app_auto_updater.rs b/src-tauri/src/app_auto_updater.rs index d954f11..f09de5d 100644 --- a/src-tauri/src/app_auto_updater.rs +++ b/src-tauri/src/app_auto_updater.rs @@ -120,12 +120,14 @@ pub struct AppUpdateProgress { pub struct AppAutoUpdater { client: Client, + extractor: &'static crate::extraction::Extractor, } impl AppAutoUpdater { fn new() -> Self { Self { client: Client::new(), + extractor: crate::extraction::Extractor::instance(), } } @@ -829,8 +831,6 @@ impl AppAutoUpdater { archive_path: &Path, dest_dir: &Path, ) -> Result> { - let extractor = crate::extraction::Extractor::instance(); - let file_name = archive_path .file_name() .and_then(|name| name.to_str()) @@ -838,7 +838,7 @@ impl AppAutoUpdater { // Handle compound extensions like .tar.gz if file_name.ends_with(".tar.gz") { - return extractor.extract_tar_gz(archive_path, dest_dir).await; + return self.extractor.extract_tar_gz(archive_path, dest_dir).await; } let extension = archive_path @@ -850,7 +850,7 @@ impl AppAutoUpdater { "dmg" => { #[cfg(target_os = "macos")] { - extractor.extract_dmg(archive_path, dest_dir).await + self.extractor.extract_dmg(archive_path, dest_dir).await } #[cfg(not(target_os = "macos"))] { @@ -914,7 +914,7 @@ impl AppAutoUpdater { Err("AppImage installation is only supported on Linux".into()) } } - "zip" => extractor.extract_zip(archive_path, dest_dir).await, + "zip" => self.extractor.extract_zip(archive_path, dest_dir).await, _ => Err(format!("Unsupported archive format: {extension}").into()), } } @@ -1083,8 +1083,8 @@ impl AppAutoUpdater { fs::create_dir_all(&temp_extract_dir)?; // Extract ZIP file - let extractor = crate::extraction::Extractor::instance(); - let extracted_path = extractor + let extracted_path = self + .extractor .extract_zip(installer_path, &temp_extract_dir) .await?; @@ -1314,8 +1314,8 @@ impl AppAutoUpdater { fs::create_dir_all(&temp_extract_dir)?; // Extract tarball - let extractor = crate::extraction::Extractor::instance(); - let extracted_path = extractor + let extracted_path = self + .extractor .extract_tar_gz(tarball_path, &temp_extract_dir) .await?; diff --git a/src-tauri/src/auto_updater.rs b/src-tauri/src/auto_updater.rs index 264a146..99b1a42 100644 --- a/src-tauri/src/auto_updater.rs +++ b/src-tauri/src/auto_updater.rs @@ -1,6 +1,5 @@ -use crate::api_client::is_browser_version_nightly; use crate::browser_version_manager::{BrowserVersionInfo, BrowserVersionManager}; -use crate::profile::BrowserProfile; +use crate::profile::{BrowserProfile, ProfileManager}; use crate::settings_manager::SettingsManager; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; @@ -29,15 +28,17 @@ pub struct AutoUpdateState { } pub struct AutoUpdater { - version_service: &'static BrowserVersionManager, + browser_version_manager: &'static BrowserVersionManager, settings_manager: &'static SettingsManager, + profile_manager: &'static ProfileManager, } impl AutoUpdater { fn new() -> Self { Self { - version_service: BrowserVersionManager::instance(), + browser_version_manager: BrowserVersionManager::instance(), settings_manager: SettingsManager::instance(), + profile_manager: ProfileManager::instance(), } } @@ -53,8 +54,8 @@ impl AutoUpdater { let mut browser_versions: HashMap> = HashMap::new(); // Group profiles by browser - let profile_manager = crate::profile::ProfileManager::instance(); - let profiles = profile_manager + let profiles = self + .profile_manager .list_profiles() .map_err(|e| format!("Failed to list profiles: {e}"))?; let mut browser_profiles: HashMap> = HashMap::new(); @@ -62,7 +63,7 @@ impl AutoUpdater { for profile in profiles { // Only check supported browsers if !self - .version_service + .browser_version_manager .is_browser_supported(&profile.browser) .unwrap_or(false) { @@ -78,14 +79,14 @@ impl AutoUpdater { for (browser, profiles) in browser_profiles { // Get cached versions first, then try to fetch if needed let versions = if let Some(cached) = self - .version_service + .browser_version_manager .get_cached_browser_versions_detailed(&browser) { cached - } else if self.version_service.should_update_cache(&browser) { + } else if self.browser_version_manager.should_update_cache(&browser) { // Try to fetch fresh versions match self - .version_service + .browser_version_manager .fetch_browser_versions_detailed(&browser, false) .await { @@ -156,8 +157,9 @@ impl AutoUpdater { // Spawn async task to handle the download and auto-update tokio::spawn(async move { + // TODO: update the logic to use the downloaded browsers registry instance instead of the static method // First, check if browser already exists - match crate::browser_runner::is_browser_downloaded( + match crate::downloaded_browsers_registry::is_browser_downloaded( browser.clone(), new_version.clone(), ) { @@ -165,12 +167,13 @@ impl AutoUpdater { println!("Browser {browser} {new_version} already downloaded, proceeding to auto-update profiles"); // Browser already exists, go straight to profile update - match crate::auto_updater::complete_browser_update_with_auto_update( - app_handle_clone, - browser.clone(), - new_version.clone(), - ) - .await + match AutoUpdater::instance() + .complete_browser_update_with_auto_update( + &app_handle_clone, + &browser.clone(), + &new_version.clone(), + ) + .await { Ok(updated_profiles) => { println!( @@ -223,7 +226,8 @@ impl AutoUpdater { available_versions: &[BrowserVersionInfo], ) -> Result, Box> { let current_version = &profile.version; - let is_current_nightly = is_browser_version_nightly(&profile.browser, current_version, None); + let is_current_nightly = + crate::api_client::is_browser_version_nightly(&profile.browser, current_version, None); // Find the best available update let best_update = available_versions @@ -231,7 +235,8 @@ impl AutoUpdater { .filter(|v| { // Only consider versions newer than current self.is_version_newer(&v.version, current_version) - && is_browser_version_nightly(&profile.browser, &v.version, None) == is_current_nightly + && crate::api_client::is_browser_version_nightly(&profile.browser, &v.version, None) + == is_current_nightly }) .max_by(|a, b| self.compare_versions(&a.version, &b.version)); @@ -298,8 +303,8 @@ impl AutoUpdater { browser: &str, new_version: &str, ) -> Result, Box> { - let profile_manager = crate::profile::ProfileManager::instance(); - let profiles = profile_manager + let profiles = self + .profile_manager .list_profiles() .map_err(|e| format!("Failed to list profiles: {e}"))?; @@ -316,7 +321,10 @@ impl AutoUpdater { // Check if this is an update (newer version) if self.is_version_newer(new_version, &profile.version) { // Update the profile version - match profile_manager.update_profile_version(app_handle, &profile.name, new_version) { + match self + .profile_manager + .update_profile_version(app_handle, &profile.name, new_version) + { Ok(_) => { updated_profiles.push(profile.name); } @@ -350,46 +358,9 @@ impl AutoUpdater { state.auto_update_downloads.remove(&download_key); self.save_auto_update_state(&state)?; - // Always perform cleanup after auto-update - don't fail the update if cleanup fails - if let Err(e) = self.cleanup_unused_binaries_internal() { - eprintln!("Warning: Failed to cleanup unused binaries after auto-update: {e}"); - } - Ok(updated_profiles) } - /// Internal method to cleanup unused binaries (used by auto-cleanup) - fn cleanup_unused_binaries_internal( - &self, - ) -> Result, Box> { - // Load current profiles - let profile_manager = crate::profile::ProfileManager::instance(); - let profiles = profile_manager - .list_profiles() - .map_err(|e| format!("Failed to load profiles: {e}"))?; - - // Get registry instance - let registry = crate::downloaded_browsers::DownloadedBrowsersRegistry::instance(); - - // Get active browser versions (all profiles) - let active_versions = registry.get_active_browser_versions(&profiles); - - // Get running browser versions (only running profiles) - let running_versions = registry.get_running_browser_versions(&profiles); - - // Cleanup unused binaries (but keep running ones) - let cleaned_up = registry - .cleanup_unused_binaries(&active_versions, &running_versions) - .map_err(|e| format!("Failed to cleanup unused binaries: {e}"))?; - - // Save updated registry - registry - .save() - .map_err(|e| format!("Failed to save registry: {e}"))?; - - Ok(cleaned_up) - } - /// Check if browser is disabled due to ongoing update pub fn is_browser_disabled( &self, diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index 5daaa47..c1e6891 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -1,37 +1,31 @@ +use crate::browser::{create_browser, BrowserType, ProxySettings}; +use crate::camoufox_manager::{CamoufoxConfig, CamoufoxManager}; +use crate::downloaded_browsers_registry::DownloadedBrowsersRegistry; use crate::platform_browser; use crate::profile::{BrowserProfile, ProfileManager}; use crate::proxy_manager::PROXY_MANAGER; use directories::BaseDirs; use serde::Serialize; -use std::collections::HashSet; -use std::fs::create_dir_all; -use std::path::{Path, PathBuf}; -use std::sync::{Arc, Mutex}; +use std::path::PathBuf; use std::time::{SystemTime, UNIX_EPOCH}; use sysinfo::System; use tauri::Emitter; - -use crate::browser::{create_browser, BrowserType, ProxySettings}; -use crate::browser_version_manager::{ - BrowserVersionInfo, BrowserVersionManager, BrowserVersionsResult, -}; -use crate::camoufox::CamoufoxConfig; -use crate::download::DownloadProgress; -use crate::downloaded_browsers::DownloadedBrowsersRegistry; - -// Global state to track currently downloading browser-version pairs -lazy_static::lazy_static! { - static ref DOWNLOADING_BROWSERS: Arc>> = Arc::new(Mutex::new(HashSet::new())); -} - pub struct BrowserRunner { base_dirs: BaseDirs, + pub profile_manager: &'static ProfileManager, + pub downloaded_browsers_registry: &'static DownloadedBrowsersRegistry, + auto_updater: &'static crate::auto_updater::AutoUpdater, + camoufox_manager: &'static CamoufoxManager, } impl BrowserRunner { fn new() -> Self { Self { base_dirs: BaseDirs::new().expect("Failed to get base directories"), + profile_manager: ProfileManager::instance(), + downloaded_browsers_registry: DownloadedBrowsersRegistry::instance(), + auto_updater: crate::auto_updater::AutoUpdater::instance(), + camoufox_manager: CamoufoxManager::instance(), } } @@ -73,11 +67,6 @@ impl BrowserRunner { path } - pub fn get_profiles_dir(&self) -> PathBuf { - let profile_manager = ProfileManager::instance(); - profile_manager.get_profiles_dir() - } - /// Get the executable path for a browser profile /// This is a common helper to eliminate code duplication across the codebase pub fn get_browser_executable_path( @@ -100,61 +89,6 @@ impl BrowserRunner { .map_err(|e| format!("Failed to get executable path for {}: {e}", profile.browser).into()) } - /// Internal method to cleanup unused binaries (used by auto-cleanup) - pub fn cleanup_unused_binaries_internal( - &self, - ) -> Result, Box> { - // Load current profiles - let profiles = self - .list_profiles() - .map_err(|e| format!("Failed to list profiles: {e}"))?; - - // Get registry instance - let registry = crate::downloaded_browsers::DownloadedBrowsersRegistry::instance(); - - // Get active browser versions (all profiles) - let active_versions = registry.get_active_browser_versions(&profiles); - - // Get running browser versions (only running profiles) - let running_versions = registry.get_running_browser_versions(&profiles); - - // Get binaries directory - let binaries_dir = self.get_binaries_dir(); - - // Use comprehensive cleanup that syncs registry with disk and removes unused binaries - let cleaned_up = - registry.comprehensive_cleanup(&binaries_dir, &active_versions, &running_versions)?; - - // Registry is already saved by comprehensive_cleanup - Ok(cleaned_up) - } - - fn apply_proxy_settings_to_profile( - &self, - profile_data_path: &Path, - proxy: &ProxySettings, - internal_proxy: Option<&ProxySettings>, - ) -> Result<(), Box> { - let profile_manager = ProfileManager::instance(); - profile_manager.apply_proxy_settings_to_profile(profile_data_path, proxy, internal_proxy) - } - - pub fn save_profile(&self, profile: &BrowserProfile) -> Result<(), Box> { - let profile_manager = ProfileManager::instance(); - let result = profile_manager.save_profile(profile); - // Update tag suggestions after any save - let _ = crate::tag_manager::TAG_MANAGER.lock().map(|tm| { - let _ = tm.rebuild_from_profiles(&self.list_profiles().unwrap_or_default()); - }); - result - } - - pub fn list_profiles(&self) -> Result, Box> { - let profile_manager = ProfileManager::instance(); - - profile_manager.list_profiles() - } - pub async fn launch_browser( &self, app_handle: tauri::AppHandle, @@ -177,8 +111,7 @@ impl BrowserRunner { headless: bool, ) -> Result> { // Check if browser is disabled due to ongoing update - let auto_updater = crate::auto_updater::AutoUpdater::instance(); - if auto_updater.is_browser_disabled(&profile.browser)? { + if self.auto_updater.is_browser_disabled(&profile.browser)? { return Err( format!( "{} is currently being updated. Please wait for the update to complete.", @@ -196,7 +129,7 @@ impl BrowserRunner { "No camoufox config found for profile {}, using default", profile.name ); - crate::camoufox::CamoufoxConfig::default() + CamoufoxConfig::default() }); // Always start a local proxy for Camoufox (for traffic monitoring and geoip support) @@ -246,8 +179,8 @@ impl BrowserRunner { "Launching Camoufox via nodecar for profile: {}", profile.name ); - let camoufox_launcher = crate::camoufox::CamoufoxNodecarLauncher::instance(); - let camoufox_result = camoufox_launcher + let camoufox_result = self + .camoufox_manager .launch_camoufox_profile_nodecar(app_handle.clone(), profile.clone(), camoufox_config, url) .await .map_err(|e| -> Box { @@ -274,7 +207,7 @@ impl BrowserRunner { self.save_process_info(&updated_profile)?; // Ensure tag suggestions include any tags from this profile let _ = crate::tag_manager::TAG_MANAGER.lock().map(|tm| { - let _ = tm.rebuild_from_profiles(&self.list_profiles().unwrap_or_default()); + let _ = tm.rebuild_from_profiles(&self.profile_manager.list_profiles().unwrap_or_default()); }); println!( "Updated profile with process info: {}", @@ -343,7 +276,7 @@ impl BrowserRunner { let proxy_for_launch_args: Option<&ProxySettings> = local_proxy_settings; // Get profile data path and launch arguments - let profiles_dir = self.get_profiles_dir(); + let profiles_dir = self.profile_manager.get_profiles_dir(); let profile_data_path = profile.get_profile_data_path(&profiles_dir); let browser_args = browser .create_launch_args( @@ -427,7 +360,7 @@ impl BrowserRunner { tokio::time::sleep(tokio::time::Duration::from_millis(1500)).await; let system = System::new_all(); - let profiles_dir = self.get_profiles_dir(); + let profiles_dir = self.profile_manager.get_profiles_dir(); let profile_data_path = profile.get_profile_data_path(&profiles_dir); let profile_data_path_str = profile_data_path.to_string_lossy(); @@ -533,7 +466,7 @@ impl BrowserRunner { self.save_process_info(&updated_profile)?; let _ = crate::tag_manager::TAG_MANAGER.lock().map(|tm| { - let _ = tm.rebuild_from_profiles(&self.list_profiles().unwrap_or_default()); + let _ = tm.rebuild_from_profiles(&self.profile_manager.list_profiles().unwrap_or_default()); }); // Apply proxy settings if needed (for Firefox-based browsers) @@ -593,15 +526,14 @@ impl BrowserRunner { ) -> Result<(), Box> { // Handle camoufox profiles using nodecar launcher if profile.browser == "camoufox" { - let camoufox_launcher = crate::camoufox::CamoufoxNodecarLauncher::instance(); - // Get the profile path based on the UUID - let profiles_dir = self.get_profiles_dir(); + let profiles_dir = self.profile_manager.get_profiles_dir(); let profile_data_path = profile.get_profile_data_path(&profiles_dir); let profile_path_str = profile_data_path.to_string_lossy(); // Check if the process is running - match camoufox_launcher + match self + .camoufox_manager .find_camoufox_by_profile(&profile_path_str) .await { @@ -634,7 +566,10 @@ impl BrowserRunner { } // Get the updated profile with current PID - let profiles = self.list_profiles().expect("Failed to list profiles"); + let profiles = self + .profile_manager + .list_profiles() + .expect("Failed to list profiles"); let updated_profile = profiles .into_iter() .find(|p| p.id == profile.id) @@ -657,7 +592,7 @@ impl BrowserRunner { BrowserType::Firefox | BrowserType::FirefoxDeveloper | BrowserType::Zen => { #[cfg(target_os = "macos")] { - let profiles_dir = self.get_profiles_dir(); + let profiles_dir = self.profile_manager.get_profiles_dir(); return platform_browser::macos::open_url_in_existing_browser_firefox_like( &updated_profile, url, @@ -670,7 +605,7 @@ impl BrowserRunner { #[cfg(target_os = "windows")] { - let profiles_dir = self.get_profiles_dir(); + let profiles_dir = self.profile_manager.get_profiles_dir(); return platform_browser::windows::open_url_in_existing_browser_firefox_like( &updated_profile, url, @@ -683,7 +618,7 @@ impl BrowserRunner { #[cfg(target_os = "linux")] { - let profiles_dir = self.get_profiles_dir(); + let profiles_dir = self.profile_manager.get_profiles_dir(); return platform_browser::linux::open_url_in_existing_browser_firefox_like( &updated_profile, url, @@ -700,7 +635,7 @@ impl BrowserRunner { BrowserType::MullvadBrowser | BrowserType::TorBrowser => { #[cfg(target_os = "macos")] { - let profiles_dir = self.get_profiles_dir(); + let profiles_dir = self.profile_manager.get_profiles_dir(); return platform_browser::macos::open_url_in_existing_browser_tor_mullvad( &updated_profile, url, @@ -713,7 +648,7 @@ impl BrowserRunner { #[cfg(target_os = "windows")] { - let profiles_dir = self.get_profiles_dir(); + let profiles_dir = self.profile_manager.get_profiles_dir(); return platform_browser::windows::open_url_in_existing_browser_tor_mullvad( &updated_profile, url, @@ -726,7 +661,7 @@ impl BrowserRunner { #[cfg(target_os = "linux")] { - let profiles_dir = self.get_profiles_dir(); + let profiles_dir = self.profile_manager.get_profiles_dir(); return platform_browser::linux::open_url_in_existing_browser_tor_mullvad( &updated_profile, url, @@ -743,7 +678,7 @@ impl BrowserRunner { BrowserType::Chromium | BrowserType::Brave => { #[cfg(target_os = "macos")] { - let profiles_dir = self.get_profiles_dir(); + let profiles_dir = self.profile_manager.get_profiles_dir(); return platform_browser::macos::open_url_in_existing_browser_chromium( &updated_profile, url, @@ -756,7 +691,7 @@ impl BrowserRunner { #[cfg(target_os = "windows")] { - let profiles_dir = self.get_profiles_dir(); + let profiles_dir = self.profile_manager.get_profiles_dir(); return platform_browser::windows::open_url_in_existing_browser_chromium( &updated_profile, url, @@ -769,7 +704,7 @@ impl BrowserRunner { #[cfg(target_os = "linux")] { - let profiles_dir = self.get_profiles_dir(); + let profiles_dir = self.profile_manager.get_profiles_dir(); return platform_browser::linux::open_url_in_existing_browser_chromium( &updated_profile, url, @@ -827,7 +762,7 @@ impl BrowserRunner { profile.browser.as_str(), "firefox" | "firefox-developer" | "zen" | "tor-browser" | "mullvad-browser" ) { - let profiles_dir = self.get_profiles_dir(); + let profiles_dir = self.profile_manager.get_profiles_dir(); let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); // Provide a dummy upstream (ignored when internal proxy is provided) @@ -840,6 +775,7 @@ impl BrowserRunner { }; self + .profile_manager .apply_proxy_settings_to_profile(&profile_path, &dummy_upstream, Some(&internal_proxy)) .map_err(|e| format!("Failed to update profile proxy: {e}"))?; } @@ -884,6 +820,7 @@ impl BrowserRunner { // Get the most up-to-date profile data let profiles = self + .profile_manager .list_profiles() .map_err(|e| format!("Failed to list profiles in launch_or_open_url: {e}"))?; let updated_profile = profiles @@ -904,6 +841,7 @@ impl BrowserRunner { // Get the updated profile again after status check (PID might have been updated) let profiles = self + .profile_manager .list_profiles() .map_err(|e| format!("Failed to list profiles after status check: {e}"))?; let final_profile = profiles @@ -1001,53 +939,19 @@ impl BrowserRunner { profile: &BrowserProfile, ) -> Result<(), Box> { // Use the regular save_profile method which handles the UUID structure - self.save_profile(profile).map_err(|e| { + self.profile_manager.save_profile(profile).map_err(|e| { let error_string = e.to_string(); Box::new(std::io::Error::other(error_string)) as Box }) } - /// Update a profile's browser version - pub fn update_profile_version( - &self, - app_handle: &tauri::AppHandle, - profile_id: &str, - version: &str, - ) -> Result> { - let profile_manager = ProfileManager::instance(); - profile_manager - .update_profile_version(app_handle, profile_id, version) - .map_err(|e| format!("Failed to update profile version: {e}").into()) - } - - pub fn delete_profile( - &self, - app_handle: tauri::AppHandle, - profile_id: &str, - ) -> Result<(), Box> { - let profile_manager = ProfileManager::instance(); - profile_manager.delete_profile(&app_handle, profile_id)?; - - // Always perform cleanup after profile deletion to remove unused binaries - if let Err(e) = self.cleanup_unused_binaries_internal() { - println!("Warning: Failed to cleanup unused binaries: {e}"); - } - - // Rebuild tags after deletion - let _ = crate::tag_manager::TAG_MANAGER.lock().map(|tm| { - let _ = tm.rebuild_from_profiles(&self.list_profiles().unwrap_or_default()); - }); - - Ok(()) - } - pub async fn check_browser_status( &self, app_handle: tauri::AppHandle, profile: &BrowserProfile, ) -> Result> { - let profile_manager = ProfileManager::instance(); - profile_manager + self + .profile_manager .check_browser_status(app_handle, profile) .await } @@ -1059,10 +963,8 @@ impl BrowserRunner { ) -> Result<(), Box> { // Handle camoufox profiles using nodecar launcher if profile.browser == "camoufox" { - let camoufox_launcher = crate::camoufox::CamoufoxNodecarLauncher::instance(); - // Search by profile path to find the running Camoufox instance - let profiles_dir = self.get_profiles_dir(); + let profiles_dir = self.profile_manager.get_profiles_dir(); let profile_data_path = profile.get_profile_data_path(&profiles_dir); let profile_path_str = profile_data_path.to_string_lossy(); @@ -1071,7 +973,8 @@ impl BrowserRunner { profile.name, profile.id ); - match camoufox_launcher + match self + .camoufox_manager .find_camoufox_by_profile(&profile_path_str) .await { @@ -1081,7 +984,8 @@ impl BrowserRunner { camoufox_process.id, camoufox_process.processId ); - match camoufox_launcher + match self + .camoufox_manager .stop_camoufox(&app_handle, &camoufox_process.id) .await { @@ -1125,9 +1029,9 @@ impl BrowserRunner { updated_profile.process_id = None; // Check for pending updates and apply them for Camoufox profiles too - let auto_updater = crate::auto_updater::AutoUpdater::instance(); - if let Ok(Some(pending_update)) = - auto_updater.get_pending_update(&profile.browser, &profile.version) + if let Ok(Some(pending_update)) = self + .auto_updater + .get_pending_update(&profile.browser, &profile.version) { println!( "Found pending update for Camoufox profile {}: {} -> {}", @@ -1135,7 +1039,7 @@ impl BrowserRunner { ); // Update the profile to the new version - match self.update_profile_version( + match self.profile_manager.update_profile_version( &app_handle, &profile.id.to_string(), &pending_update.new_version, @@ -1148,7 +1052,10 @@ impl BrowserRunner { updated_profile = updated_profile_after_update; // Remove the pending update from the auto updater state - if let Err(e) = auto_updater.dismiss_update_notification(&pending_update.id) { + if let Err(e) = self + .auto_updater + .dismiss_update_notification(&pending_update.id) + { eprintln!("Warning: Failed to dismiss pending update notification: {e}"); } } @@ -1202,8 +1109,10 @@ impl BrowserRunner { ); // Consolidate browser versions after stopping a browser - let registry = DownloadedBrowsersRegistry::instance(); - if let Ok(consolidated) = registry.consolidate_browser_versions(&app_handle, self) { + if let Ok(consolidated) = self + .downloaded_browsers_registry + .consolidate_browser_versions(&app_handle) + { if !consolidated.is_empty() { println!("Post-stop version consolidation results:"); for action in &consolidated { @@ -1255,7 +1164,7 @@ impl BrowserRunner { if is_correct_browser { // Verify profile path match - let profiles_dir = self.get_profiles_dir(); + let profiles_dir = self.profile_manager.get_profiles_dir(); let profile_data_path = profile.get_profile_data_path(&profiles_dir); let profile_data_path_str = profile_data_path.to_string_lossy(); @@ -1354,9 +1263,9 @@ impl BrowserRunner { updated_profile.process_id = None; // Check for pending updates and apply them - let auto_updater = crate::auto_updater::AutoUpdater::instance(); - if let Ok(Some(pending_update)) = - auto_updater.get_pending_update(&profile.browser, &profile.version) + if let Ok(Some(pending_update)) = self + .auto_updater + .get_pending_update(&profile.browser, &profile.version) { println!( "Found pending update for profile {}: {} -> {}", @@ -1364,7 +1273,7 @@ impl BrowserRunner { ); // Update the profile to the new version - match self.update_profile_version( + match self.profile_manager.update_profile_version( &app_handle, &profile.id.to_string(), &pending_update.new_version, @@ -1377,7 +1286,10 @@ impl BrowserRunner { updated_profile = updated_profile_after_update; // Remove the pending update from the auto updater state - if let Err(e) = auto_updater.dismiss_update_notification(&pending_update.id) { + if let Err(e) = self + .auto_updater + .dismiss_update_notification(&pending_update.id) + { eprintln!("Warning: Failed to dismiss pending update notification: {e}"); } } @@ -1426,8 +1338,10 @@ impl BrowserRunner { } // Consolidate browser versions after stopping a browser - let registry = DownloadedBrowsersRegistry::instance(); - if let Ok(consolidated) = registry.consolidate_browser_versions(&app_handle, self) { + if let Ok(consolidated) = self + .downloaded_browsers_registry + .consolidate_browser_versions(&app_handle) + { if !consolidated.is_empty() { println!("Post-stop version consolidation results:"); for action in &consolidated { @@ -1445,7 +1359,7 @@ impl BrowserRunner { profile: &BrowserProfile, ) -> Result> { let system = System::new_all(); - let profiles_dir = self.get_profiles_dir(); + let profiles_dir = self.profile_manager.get_profiles_dir(); let profile_data_path = profile.get_profile_data_path(&profiles_dir); let profile_data_path_str = profile_data_path.to_string_lossy(); @@ -1556,484 +1470,36 @@ impl BrowserRunner { ) } - /// Check if browser binaries exist for all profiles and return missing binaries - pub async fn check_missing_binaries( - &self, - ) -> Result, Box> { - // Get all profiles - let profiles = self - .list_profiles() - .map_err(|e| format!("Failed to list profiles: {e}"))?; - let mut missing_binaries = Vec::new(); - - for profile in profiles { - let browser_type = match BrowserType::from_str(&profile.browser) { - Ok(bt) => bt, - Err(_) => { - println!( - "Warning: Invalid browser type '{}' for profile '{}'", - profile.browser, profile.name - ); - continue; - } - }; - - let browser = create_browser(browser_type.clone()); - let binaries_dir = self.get_binaries_dir(); - println!( - "binaries_dir: {binaries_dir:?} for profile: {}", - profile.name - ); - - // Check if the version is downloaded - if !browser.is_version_downloaded(&profile.version, &binaries_dir) { - missing_binaries.push((profile.name, profile.browser, profile.version)); - } - } - - Ok(missing_binaries) - } - - /// Check if GeoIP database is missing for Camoufox profiles - pub fn check_missing_geoip_database( - &self, - ) -> Result> { - // Get all profiles - let profiles = self - .list_profiles() - .map_err(|e| format!("Failed to list profiles: {e}"))?; - - // Check if there are any Camoufox profiles - let has_camoufox_profiles = profiles.iter().any(|profile| profile.browser == "camoufox"); - - if has_camoufox_profiles { - // Check if GeoIP database is available - use crate::geoip_downloader::GeoIPDownloader; - return Ok(!GeoIPDownloader::is_geoip_database_available()); - } - - Ok(false) - } - - /// Automatically download missing binaries for all profiles - pub async fn ensure_all_binaries_exist( - &self, - app_handle: &tauri::AppHandle, - ) -> Result, Box> { - // First, clean up any stale registry entries - let registry = DownloadedBrowsersRegistry::instance(); - if let Ok(cleaned_up) = registry.verify_and_cleanup_stale_entries(self) { - if !cleaned_up.is_empty() { - println!( - "Cleaned up {} stale registry entries: {}", - cleaned_up.len(), - cleaned_up.join(", ") - ); - } - } - - // Consolidate browser versions - keep only latest version per browser - if let Ok(consolidated) = registry.consolidate_browser_versions(app_handle, self) { - if !consolidated.is_empty() { - println!("Version consolidation results:"); - for action in &consolidated { - println!(" {action}"); - } - } - } - - let missing_binaries = self.check_missing_binaries().await?; - let mut downloaded = Vec::new(); - - for (profile_name, browser, version) in missing_binaries { - println!("Downloading missing binary for profile '{profile_name}': {browser} {version}"); - - match self - .download_browser_impl(app_handle.clone(), browser.clone(), version.clone()) - .await - { - Ok(_) => { - downloaded.push(format!( - "{browser} {version} (for profile '{profile_name}')" - )); - } - Err(e) => { - eprintln!("Failed to download {browser} {version} for profile '{profile_name}': {e}"); - } - } - } - - // Check if GeoIP database is missing for Camoufox profiles - if self.check_missing_geoip_database()? { - println!("GeoIP database is missing for Camoufox profiles, downloading..."); - - use crate::geoip_downloader::GeoIPDownloader; - - let geoip_downloader = GeoIPDownloader::instance(); - - match geoip_downloader.download_geoip_database(app_handle).await { - Ok(_) => { - downloaded.push("GeoIP database for Camoufox".to_string()); - println!("GeoIP database downloaded successfully"); - } - Err(e) => { - eprintln!("Failed to download GeoIP database: {e}"); - // Don't fail the entire operation if GeoIP download fails - } - } - } - - Ok(downloaded) - } - - pub async fn download_browser_impl( + pub async fn open_url_with_profile( &self, app_handle: tauri::AppHandle, - browser_str: String, - version: String, - ) -> Result> { - // Check if this browser-version pair is already being downloaded - let download_key = format!("{browser_str}-{version}"); - { - let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap(); - if downloading.contains(&download_key) { - return Err(format!("Browser '{browser_str}' version '{version}' is already being downloaded. Please wait for the current download to complete.").into()); - } - // Mark this browser-version pair as being downloaded - downloading.insert(download_key.clone()); - } + profile_id: String, + url: String, + ) -> Result<(), String> { + // Get the profile by name + let profiles = self + .profile_manager + .list_profiles() + .map_err(|e| format!("Failed to list profiles: {e}"))?; + let profile = profiles + .into_iter() + .find(|p| p.id.to_string() == profile_id) + .ok_or_else(|| format!("Profile '{profile_id}' not found"))?; - let browser_type = - BrowserType::from_str(&browser_str).map_err(|e| format!("Invalid browser type: {e}"))?; - let browser = create_browser(browser_type.clone()); + println!("Opening URL '{url}' with profile '{profile_id}'"); - // Get registry instance and check if already downloaded - let registry = DownloadedBrowsersRegistry::instance(); - - // Check if registry thinks it's downloaded, but also verify files actually exist - if registry.is_browser_downloaded(&browser_str, &version) { - let binaries_dir = self.get_binaries_dir(); - let actually_exists = browser.is_version_downloaded(&version, &binaries_dir); - - if actually_exists { - return Ok(version); - } else { - // Registry says it's downloaded but files don't exist - clean up registry - println!("Registry indicates {browser_str} {version} is downloaded, but files are missing. Cleaning up registry entry."); - registry.remove_browser(&browser_str, &version); - registry - .save() - .map_err(|e| format!("Failed to save cleaned registry: {e}"))?; - } - } - - // Check if browser is supported on current platform before attempting download - let version_service = BrowserVersionManager::instance(); - - if !version_service - .is_browser_supported(&browser_str) - .unwrap_or(false) - { - return Err( - format!( - "Browser '{}' is not supported on your platform ({} {}). Supported browsers: {}", - browser_str, - std::env::consts::OS, - std::env::consts::ARCH, - version_service.get_supported_browsers().join(", ") - ) - .into(), - ); - } - - let download_info = version_service - .get_download_info(&browser_str, &version) - .map_err(|e| format!("Failed to get download info: {e}"))?; - - // Create browser directory - let mut browser_dir = self.get_binaries_dir(); - browser_dir.push(&browser_str); - browser_dir.push(&version); - - create_dir_all(&browser_dir).map_err(|e| format!("Failed to create browser directory: {e}"))?; - - // Mark download as started (but don't add to registry yet) - registry.mark_download_started(&browser_str, &version, browser_dir.clone()); - - // Use the download module - let downloader = crate::download::Downloader::instance(); - // Attempt to download the archive. If the download fails but an archive with the - // expected filename already exists (manual download), continue using that file. - let download_path: PathBuf = match downloader - .download_browser( - &app_handle, - browser_type.clone(), - &version, - &download_info, - &browser_dir, - ) + // Use launch_or_open_url which handles both launching new instances and opening in existing ones + self + .launch_or_open_url(app_handle, &profile, Some(url.clone()), None) .await - { - Ok(path) => path, - Err(e) => { - // Do NOT continue with extraction on failed downloads. Partial files may exist but are invalid. - // Clean registry entry and stop here so the UI can show a single, clear error. - let _ = registry.remove_browser(&browser_str, &version); - let _ = registry.save(); - let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap(); - downloading.remove(&download_key); - return Err(format!("Failed to download browser: {e}").into()); - } - }; + .map_err(|e| { + println!("Failed to open URL with profile '{profile_id}': {e}"); + format!("Failed to open URL with profile: {e}") + })?; - // Use the extraction module - if download_info.is_archive { - let extractor = crate::extraction::Extractor::instance(); - match extractor - .extract_browser( - &app_handle, - browser_type.clone(), - &version, - &download_path, - &browser_dir, - ) - .await - { - Ok(_) => { - // Do not remove the archive here. We keep it until verification succeeds. - } - Err(e) => { - // Do not remove the archive or extracted files. Just drop the registry entry - // so it won't be reported as downloaded. - let _ = registry.remove_browser(&browser_str, &version); - let _ = registry.save(); - // Remove browser-version pair from downloading set on error - { - let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap(); - downloading.remove(&download_key); - } - return Err(format!("Failed to extract browser: {e}").into()); - } - } - - // Give filesystem a moment to settle after extraction - tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; - } - - // Emit verification progress - let progress = DownloadProgress { - browser: browser_str.clone(), - version: version.clone(), - downloaded_bytes: 0, - total_bytes: None, - percentage: 100.0, - speed_bytes_per_sec: 0.0, - eta_seconds: None, - stage: "verifying".to_string(), - }; - let _ = app_handle.emit("download-progress", &progress); - - // Verify the browser was downloaded correctly - println!("Verifying download for browser: {browser_str}, version: {version}"); - - // Use the browser's own verification method - let binaries_dir = self.get_binaries_dir(); - if !browser.is_version_downloaded(&version, &binaries_dir) { - // Provide detailed error information for debugging - let browser_dir = binaries_dir.join(&browser_str).join(&version); - let mut error_details = format!( - "Browser download completed but verification failed for {} {}. Expected directory: {}", - browser_str, - version, - browser_dir.display() - ); - - // List what files actually exist - if browser_dir.exists() { - error_details.push_str("\nFiles found in directory:"); - if let Ok(entries) = std::fs::read_dir(&browser_dir) { - for entry in entries.flatten() { - let path = entry.path(); - let file_type = if path.is_dir() { "DIR" } else { "FILE" }; - error_details.push_str(&format!("\n {} {}", file_type, path.display())); - } - } else { - error_details.push_str("\n (Could not read directory contents)"); - } - } else { - error_details.push_str("\nDirectory does not exist!"); - } - - // For Camoufox on Linux, provide specific expected files - if browser_str == "camoufox" && cfg!(target_os = "linux") { - let camoufox_subdir = browser_dir.join("camoufox"); - error_details.push_str("\nExpected Camoufox executable locations:"); - error_details.push_str(&format!("\n {}/camoufox-bin", camoufox_subdir.display())); - error_details.push_str(&format!("\n {}/camoufox", camoufox_subdir.display())); - - if camoufox_subdir.exists() { - error_details.push_str(&format!( - "\nCamoufox subdirectory exists: {}", - camoufox_subdir.display() - )); - if let Ok(entries) = std::fs::read_dir(&camoufox_subdir) { - error_details.push_str("\nFiles in camoufox subdirectory:"); - for entry in entries.flatten() { - let path = entry.path(); - let file_type = if path.is_dir() { "DIR" } else { "FILE" }; - error_details.push_str(&format!("\n {} {}", file_type, path.display())); - } - } - } else { - error_details.push_str(&format!( - "\nCamoufox subdirectory does not exist: {}", - camoufox_subdir.display() - )); - } - } - - // Do not delete files on verification failure; keep archive for manual retry. - let _ = registry.remove_browser(&browser_str, &version); - let _ = registry.save(); - // Remove browser-version pair from downloading set on verification failure - { - let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap(); - downloading.remove(&download_key); - } - return Err(error_details.into()); - } - - // Mark completion in registry - only now add to registry after verification - if let Err(e) = registry.mark_download_completed(&browser_str, &version, browser_dir.clone()) { - eprintln!("Warning: Could not mark {browser_str} {version} as completed in registry: {e}"); - } - registry - .save() - .map_err(|e| format!("Failed to save registry: {e}"))?; - - // Now that verification succeeded, remove the archive file if it exists - if download_info.is_archive { - let archive_path = browser_dir.join(&download_info.filename); - if archive_path.exists() { - if let Err(e) = std::fs::remove_file(&archive_path) { - println!("Warning: Could not delete archive file after verification: {e}"); - } - } - } - - // If this is Camoufox, automatically download GeoIP database - if browser_str == "camoufox" { - use crate::geoip_downloader::GeoIPDownloader; - - // Check if GeoIP database is already available - if !GeoIPDownloader::is_geoip_database_available() { - println!("Downloading GeoIP database for Camoufox..."); - - let geoip_downloader = GeoIPDownloader::instance(); - - match geoip_downloader.download_geoip_database(&app_handle).await { - Ok(_) => { - println!("GeoIP database downloaded successfully"); - } - Err(e) => { - eprintln!("Failed to download GeoIP database: {e}"); - // Don't fail the browser download if GeoIP download fails - } - } - } else { - println!("GeoIP database already available"); - } - } - - // Emit completion - let progress = DownloadProgress { - browser: browser_str.clone(), - version: version.clone(), - downloaded_bytes: 0, - total_bytes: None, - percentage: 100.0, - speed_bytes_per_sec: 0.0, - eta_seconds: Some(0.0), - stage: "completed".to_string(), - }; - let _ = app_handle.emit("download-progress", &progress); - - // Remove browser-version pair from downloading set - { - let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap(); - downloading.remove(&download_key); - } - - Ok(version) + println!("Successfully opened URL '{url}' with profile '{profile_id}'"); + Ok(()) } - - /// Check if a browser version is downloaded - pub fn is_browser_downloaded(&self, browser_str: &str, version: &str) -> bool { - // Always check if files actually exist on disk - let browser_type = match BrowserType::from_str(browser_str) { - Ok(bt) => bt, - Err(_) => { - println!("Invalid browser type: {browser_str}"); - return false; - } - }; - let browser = create_browser(browser_type.clone()); - let binaries_dir = self.get_binaries_dir(); - let files_exist = browser.is_version_downloaded(version, &binaries_dir); - - // If files don't exist but registry thinks they do, clean up the registry - if !files_exist { - let registry = DownloadedBrowsersRegistry::instance(); - if registry.is_browser_downloaded(browser_str, version) { - println!("Cleaning up stale registry entry for {browser_str} {version}"); - registry.remove_browser(browser_str, version); - let _ = registry.save(); // Don't fail if save fails, just log - } - } - - files_exist - } - - pub fn get_all_tags(&self) -> Result, Box> { - let tag_manager = crate::tag_manager::TAG_MANAGER.lock().unwrap(); - tag_manager.get_all_tags() - } -} - -#[allow(clippy::too_many_arguments)] -#[tauri::command] -pub async fn create_browser_profile_with_group( - app_handle: tauri::AppHandle, - name: String, - browser: String, - version: String, - release_type: String, - proxy_id: Option, - camoufox_config: Option, - group_id: Option, -) -> Result { - let profile_manager = ProfileManager::instance(); - profile_manager - .create_profile_with_group( - &app_handle, - &name, - &browser, - &version, - &release_type, - proxy_id, - camoufox_config, - group_id, - ) - .await - .map_err(|e| format!("Failed to create profile: {e}")) -} - -#[tauri::command] -pub fn list_browser_profiles() -> Result, String> { - let profile_manager = ProfileManager::instance(); - profile_manager - .list_profiles() - .map_err(|e| format!("Failed to list profiles: {e}")) } #[tauri::command] @@ -2054,6 +1520,7 @@ pub async fn launch_browser_profile( // Resolve the most up-to-date profile from disk by ID to avoid using stale proxy_id/browser state let profile_for_launch = match browser_runner + .profile_manager .list_profiles() .map_err(|e| format!("Failed to list profiles: {e}")) { @@ -2100,7 +1567,7 @@ pub async fn launch_browser_profile( profile_for_launch.browser.as_str(), "firefox" | "firefox-developer" | "zen" | "tor-browser" | "mullvad-browser" ) { - let profiles_dir = browser_runner.get_profiles_dir(); + let profiles_dir = browser_runner.profile_manager.get_profiles_dir(); let profile_path = profiles_dir .join(profile_for_launch.id.to_string()) .join("profile"); @@ -2115,6 +1582,7 @@ pub async fn launch_browser_profile( }; browser_runner + .profile_manager .apply_proxy_settings_to_profile(&profile_path, &dummy_upstream, Some(&internal_proxy)) .map_err(|e| format!("Failed to update profile proxy: {e}"))?; } @@ -2182,181 +1650,13 @@ pub async fn launch_browser_profile( Ok(updated_profile) } -#[tauri::command] -pub async fn update_profile_proxy( - app_handle: tauri::AppHandle, - profile_id: String, - proxy_id: Option, -) -> Result { - let profile_manager = ProfileManager::instance(); - profile_manager - .update_profile_proxy(app_handle, &profile_id, proxy_id) - .await - .map_err(|e| format!("Failed to update profile: {e}")) -} - -#[tauri::command] -pub fn update_profile_tags( - app_handle: tauri::AppHandle, - profile_id: String, - tags: Vec, -) -> Result { - let profile_manager = ProfileManager::instance(); - profile_manager - .update_profile_tags(&app_handle, &profile_id, tags) - .map_err(|e| format!("Failed to update profile tags: {e}")) -} - -#[tauri::command] -pub async fn check_browser_status( - app_handle: tauri::AppHandle, - profile: BrowserProfile, -) -> Result { - let profile_manager = ProfileManager::instance(); - profile_manager - .check_browser_status(app_handle, &profile) - .await - .map_err(|e| format!("Failed to check browser status: {e}")) -} - -#[tauri::command] -pub fn rename_profile( - app_handle: tauri::AppHandle, - profile_id: String, - new_name: String, -) -> Result { - let profile_manager = ProfileManager::instance(); - profile_manager - .rename_profile(&app_handle, &profile_id, &new_name) - .map_err(|e| format!("Failed to rename profile: {e}")) -} - -#[tauri::command] -pub async fn delete_profile( - app_handle: tauri::AppHandle, - profile_id: String, -) -> Result<(), String> { - let browser_runner = BrowserRunner::instance(); - browser_runner - .delete_profile(app_handle, &profile_id) - .map_err(|e| format!("Failed to delete profile: {e}")) -} - -#[tauri::command] -pub fn get_supported_browsers() -> Result, String> { - let service = BrowserVersionManager::instance(); - Ok(service.get_supported_browsers()) -} - -#[tauri::command] -pub fn is_browser_supported_on_platform(browser_str: String) -> Result { - let service = BrowserVersionManager::instance(); - service - .is_browser_supported(&browser_str) - .map_err(|e| format!("Failed to check browser support: {e}")) -} - -#[tauri::command] -pub async fn fetch_browser_versions_cached_first( - browser_str: String, -) -> Result, String> { - let service = BrowserVersionManager::instance(); - - // Get cached versions immediately if available - if let Some(cached_versions) = service.get_cached_browser_versions_detailed(&browser_str) { - // Check if we should update cache in background - if service.should_update_cache(&browser_str) { - // Start background update but return cached data immediately - let service_clone = BrowserVersionManager::instance(); - let browser_str_clone = browser_str.clone(); - tokio::spawn(async move { - if let Err(e) = service_clone - .fetch_browser_versions_detailed(&browser_str_clone, false) - .await - { - eprintln!("Background version update failed for {browser_str_clone}: {e}"); - } - }); - } - Ok(cached_versions) - } else { - // No cache available, fetch fresh - service - .fetch_browser_versions_detailed(&browser_str, false) - .await - .map_err(|e| format!("Failed to fetch detailed browser versions: {e}")) - } -} - -#[tauri::command] -pub async fn fetch_browser_versions_with_count_cached_first( - browser_str: String, -) -> Result { - let service = BrowserVersionManager::instance(); - - // Get cached versions immediately if available - if let Some(cached_versions) = service.get_cached_browser_versions(&browser_str) { - // Check if we should update cache in background - if service.should_update_cache(&browser_str) { - // Start background update but return cached data immediately - let service_clone = BrowserVersionManager::instance(); - let browser_str_clone = browser_str.clone(); - tokio::spawn(async move { - if let Err(e) = service_clone - .fetch_browser_versions_with_count(&browser_str_clone, false) - .await - { - eprintln!("Background version update failed for {browser_str_clone}: {e}"); - } - }); - } - - // Return cached data in the expected format - Ok(BrowserVersionsResult { - versions: cached_versions.clone(), - new_versions_count: None, // No new versions when returning cached data - total_versions_count: cached_versions.len(), - }) - } else { - // No cache available, fetch fresh - service - .fetch_browser_versions_with_count(&browser_str, false) - .await - .map_err(|e| format!("Failed to fetch browser versions: {e}")) - } -} - -#[tauri::command] -pub async fn download_browser( - app_handle: tauri::AppHandle, - browser_str: String, - version: String, -) -> Result { - let browser_runner = BrowserRunner::instance(); - browser_runner - .download_browser_impl(app_handle, browser_str, version) - .await - .map_err(|e| format!("Failed to download browser: {e}")) -} - -#[tauri::command] -pub fn is_browser_downloaded(browser_str: String, version: String) -> bool { - let browser_runner = BrowserRunner::instance(); - browser_runner.is_browser_downloaded(&browser_str, &version) -} - -#[tauri::command] -pub fn get_all_tags() -> Result, String> { - let browser_runner = BrowserRunner::instance(); - browser_runner - .get_all_tags() - .map_err(|e| format!("Failed to get tags: {e}")) -} - #[tauri::command] pub fn check_browser_exists(browser_str: String, version: String) -> bool { // This is an alias for is_browser_downloaded to provide clearer semantics for auto-updates - is_browser_downloaded(browser_str, version) + let runner = BrowserRunner::instance(); + runner + .downloaded_browsers_registry + .is_browser_downloaded(&browser_str, &version) } #[tauri::command] @@ -2406,91 +1706,6 @@ pub async fn kill_browser_profile( } } -#[allow(clippy::too_many_arguments)] -#[tauri::command] -pub async fn create_browser_profile_new( - app_handle: tauri::AppHandle, - name: String, - browser_str: String, - version: String, - release_type: String, - proxy_id: Option, - camoufox_config: Option, - group_id: Option, -) -> Result { - let browser_type = - BrowserType::from_str(&browser_str).map_err(|e| format!("Invalid browser type: {e}"))?; - create_browser_profile_with_group( - app_handle, - name, - browser_type.as_str().to_string(), - version, - release_type, - proxy_id, - camoufox_config, - group_id, - ) - .await -} - -#[tauri::command] -pub async fn update_camoufox_config( - app_handle: tauri::AppHandle, - profile_id: String, - config: CamoufoxConfig, -) -> Result<(), String> { - let profile_manager = ProfileManager::instance(); - profile_manager - .update_camoufox_config(app_handle, &profile_id, config) - .await - .map_err(|e| format!("Failed to update Camoufox config: {e}")) -} - -#[tauri::command] -pub async fn fetch_browser_versions_with_count( - browser_str: String, -) -> Result { - let service = BrowserVersionManager::instance(); - service - .fetch_browser_versions_with_count(&browser_str, false) - .await - .map_err(|e| format!("Failed to fetch browser versions: {e}")) -} - -#[tauri::command] -pub fn get_downloaded_browser_versions(browser_str: String) -> Result, String> { - let registry = DownloadedBrowsersRegistry::instance(); - Ok(registry.get_downloaded_versions(&browser_str)) -} - -#[tauri::command] -pub async fn check_missing_binaries() -> Result, String> { - let browser_runner = BrowserRunner::instance(); - browser_runner - .check_missing_binaries() - .await - .map_err(|e| format!("Failed to check missing binaries: {e}")) -} - -#[tauri::command] -pub async fn check_missing_geoip_database() -> Result { - let browser_runner = BrowserRunner::instance(); - browser_runner - .check_missing_geoip_database() - .map_err(|e| format!("Failed to check missing GeoIP database: {e}")) -} - -#[tauri::command] -pub async fn ensure_all_binaries_exist( - app_handle: tauri::AppHandle, -) -> Result, String> { - let browser_runner = BrowserRunner::instance(); - browser_runner - .ensure_all_binaries_exist(&app_handle) - .await - .map_err(|e| format!("Failed to ensure all binaries exist: {e}")) -} - pub async fn launch_browser_profile_with_debugging( app_handle: tauri::AppHandle, profile: BrowserProfile, @@ -2505,38 +1720,16 @@ pub async fn launch_browser_profile_with_debugging( .map_err(|e| format!("Failed to launch browser with debugging: {e}")) } -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - - fn create_test_browser_runner() -> (&'static BrowserRunner, TempDir) { - let temp_dir = TempDir::new().unwrap(); - - // Mock the base directories by setting environment variables - std::env::set_var("HOME", temp_dir.path()); - - let browser_runner = BrowserRunner::instance(); - (browser_runner, temp_dir) - } - - #[test] - fn test_get_binaries_dir() { - let (runner, _temp_dir) = create_test_browser_runner(); - let binaries_dir = runner.get_binaries_dir(); - - assert!(binaries_dir.to_string_lossy().contains("DonutBrowser")); - assert!(binaries_dir.to_string_lossy().contains("binaries")); - } - - #[test] - fn test_get_profiles_dir() { - let (runner, _temp_dir) = create_test_browser_runner(); - let profiles_dir = runner.get_profiles_dir(); - - assert!(profiles_dir.to_string_lossy().contains("DonutBrowser")); - assert!(profiles_dir.to_string_lossy().contains("profiles")); - } +#[tauri::command] +pub async fn open_url_with_profile( + app_handle: tauri::AppHandle, + profile_id: String, + url: String, +) -> Result<(), String> { + let browser_runner = BrowserRunner::instance(); + browser_runner + .open_url_with_profile(app_handle, profile_id, url) + .await } // Global singleton instance diff --git a/src-tauri/src/browser_version_manager.rs b/src-tauri/src/browser_version_manager.rs index cd46f56..3a912f3 100644 --- a/src-tauri/src/browser_version_manager.rs +++ b/src-tauri/src/browser_version_manager.rs @@ -117,7 +117,8 @@ impl BrowserVersionManager { /// Get cached browser versions immediately (returns None if no cache exists) pub fn get_cached_browser_versions(&self, browser: &str) -> Option> { if browser == "brave" { - return ApiClient::instance() + return self + .api_client .get_cached_github_releases("brave") .map(|releases| releases.into_iter().map(|r| r.tag_name).collect()); } @@ -134,7 +135,7 @@ impl BrowserVersionManager { browser: &str, ) -> Option> { if browser == "brave" { - if let Some(releases) = ApiClient::instance().get_cached_github_releases("brave") { + if let Some(releases) = self.api_client.get_cached_github_releases("brave") { let detailed_info: Vec = releases .into_iter() .map(|r| BrowserVersionInfo { @@ -1274,6 +1275,101 @@ mod tests { } } +#[tauri::command] +pub fn get_supported_browsers() -> Result, String> { + let service = BrowserVersionManager::instance(); + Ok(service.get_supported_browsers()) +} + +#[tauri::command] +pub fn is_browser_supported_on_platform(browser_str: String) -> Result { + let service = BrowserVersionManager::instance(); + service + .is_browser_supported(&browser_str) + .map_err(|e| format!("Failed to check browser support: {e}")) +} + +#[tauri::command] +pub async fn fetch_browser_versions_cached_first( + browser_str: String, +) -> Result, String> { + let service = BrowserVersionManager::instance(); + + // Get cached versions immediately if available + if let Some(cached_versions) = service.get_cached_browser_versions_detailed(&browser_str) { + // Check if we should update cache in background + if service.should_update_cache(&browser_str) { + // Start background update but return cached data immediately + let service_clone = BrowserVersionManager::instance(); + let browser_str_clone = browser_str.clone(); + tokio::spawn(async move { + if let Err(e) = service_clone + .fetch_browser_versions_detailed(&browser_str_clone, false) + .await + { + eprintln!("Background version update failed for {browser_str_clone}: {e}"); + } + }); + } + Ok(cached_versions) + } else { + // No cache available, fetch fresh + service + .fetch_browser_versions_detailed(&browser_str, false) + .await + .map_err(|e| format!("Failed to fetch detailed browser versions: {e}")) + } +} + +#[tauri::command] +pub async fn fetch_browser_versions_with_count_cached_first( + browser_str: String, +) -> Result { + let service = BrowserVersionManager::instance(); + + // Get cached versions immediately if available + if let Some(cached_versions) = service.get_cached_browser_versions(&browser_str) { + // Check if we should update cache in background + if service.should_update_cache(&browser_str) { + // Start background update but return cached data immediately + let service_clone = BrowserVersionManager::instance(); + let browser_str_clone = browser_str.clone(); + tokio::spawn(async move { + if let Err(e) = service_clone + .fetch_browser_versions_with_count(&browser_str_clone, false) + .await + { + eprintln!("Background version update failed for {browser_str_clone}: {e}"); + } + }); + } + + // Return cached data in the expected format + Ok(BrowserVersionsResult { + versions: cached_versions.clone(), + new_versions_count: None, // No new versions when returning cached data + total_versions_count: cached_versions.len(), + }) + } else { + // No cache available, fetch fresh + service + .fetch_browser_versions_with_count(&browser_str, false) + .await + .map_err(|e| format!("Failed to fetch browser versions: {e}")) + } +} + +#[tauri::command] +pub async fn fetch_browser_versions_with_count( + browser_str: String, +) -> Result { + let service = BrowserVersionManager::instance(); + service + .fetch_browser_versions_with_count(&browser_str, false) + .await + .map_err(|e| format!("Failed to fetch browser versions: {e}")) +} + // Global singleton instance lazy_static::lazy_static! { static ref BROWSER_VERSION_SERVICE: BrowserVersionManager = BrowserVersionManager::new(); diff --git a/src-tauri/src/camoufox.rs b/src-tauri/src/camoufox_manager.rs similarity index 93% rename from src-tauri/src/camoufox.rs rename to src-tauri/src/camoufox_manager.rs index 98fa908..16bd6e2 100644 --- a/src-tauri/src/camoufox.rs +++ b/src-tauri/src/camoufox_manager.rs @@ -1,8 +1,10 @@ +use crate::browser_runner::BrowserRunner; use crate::profile::BrowserProfile; +use directories::BaseDirs; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::path::PathBuf; use std::sync::Arc; - use tauri::AppHandle; use tauri_plugin_shell::ShellExt; use tokio::sync::Mutex as AsyncMutex; @@ -60,27 +62,40 @@ struct CamoufoxInstance { url: Option, } -struct CamoufoxNodecarLauncherInner { +struct CamoufoxManagerInner { instances: HashMap, } -pub struct CamoufoxNodecarLauncher { - inner: Arc>, +pub struct CamoufoxManager { + inner: Arc>, + base_dirs: BaseDirs, } -impl CamoufoxNodecarLauncher { +impl CamoufoxManager { fn new() -> Self { Self { - inner: Arc::new(AsyncMutex::new(CamoufoxNodecarLauncherInner { + inner: Arc::new(AsyncMutex::new(CamoufoxManagerInner { instances: HashMap::new(), })), + base_dirs: BaseDirs::new().expect("Failed to get base directories"), } } - pub fn instance() -> &'static CamoufoxNodecarLauncher { + pub fn instance() -> &'static CamoufoxManager { &CAMOUFOX_NODECAR_LAUNCHER } + pub fn get_profiles_dir(&self) -> PathBuf { + let mut path = self.base_dirs.data_local_dir().to_path_buf(); + path.push(if cfg!(debug_assertions) { + "DonutBrowserDev" + } else { + "DonutBrowser" + }); + path.push("profiles"); + path + } + /// Generate Camoufox fingerprint configuration during profile creation pub async fn generate_fingerprint_config( &self, @@ -95,8 +110,8 @@ impl CamoufoxNodecarLauncher { path.clone() } else { // Use the browser runner helper with the real profile - let browser_runner = crate::browser_runner::BrowserRunner::instance(); - browser_runner + // Use self.browser_runner instead of instance() + BrowserRunner::instance() .get_browser_executable_path(profile) .map_err(|e| format!("Failed to get Camoufox executable path: {e}"))? .to_string_lossy() @@ -202,8 +217,8 @@ impl CamoufoxNodecarLauncher { path.clone() } else { // Use the browser runner helper with the real profile - let browser_runner = crate::browser_runner::BrowserRunner::instance(); - browser_runner + // Use self.browser_runner instead of instance() + BrowserRunner::instance() .get_browser_executable_path(profile) .map_err(|e| format!("Failed to get Camoufox executable path: {e}"))? .to_string_lossy() @@ -431,7 +446,7 @@ impl CamoufoxNodecarLauncher { } } -impl CamoufoxNodecarLauncher { +impl CamoufoxManager { pub async fn launch_camoufox_profile_nodecar( &self, app_handle: AppHandle, @@ -440,8 +455,7 @@ impl CamoufoxNodecarLauncher { url: Option, ) -> Result { // Get profile path - let browser_runner = crate::browser_runner::BrowserRunner::instance(); - let profiles_dir = browser_runner.get_profiles_dir(); + let profiles_dir = self.get_profiles_dir(); let profile_path = profile.get_profile_data_path(&profiles_dir); let profile_path_str = profile_path.to_string_lossy(); @@ -484,5 +498,5 @@ mod tests { // Global singleton instance lazy_static::lazy_static! { - static ref CAMOUFOX_NODECAR_LAUNCHER: CamoufoxNodecarLauncher = CamoufoxNodecarLauncher::new(); + static ref CAMOUFOX_NODECAR_LAUNCHER: CamoufoxManager = CamoufoxManager::new(); } diff --git a/src-tauri/src/default_browser.rs b/src-tauri/src/default_browser.rs index 1f6d9a2..89bf31a 100644 --- a/src-tauri/src/default_browser.rs +++ b/src-tauri/src/default_browser.rs @@ -1,10 +1,10 @@ use tauri::command; -pub struct DefaultBrowser; +pub struct DefaultBrowser {} impl DefaultBrowser { fn new() -> Self { - Self + Self {} } pub fn instance() -> &'static DefaultBrowser { @@ -38,38 +38,6 @@ impl DefaultBrowser { #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] Err("Unsupported platform".to_string()) } - - pub async fn open_url_with_profile( - &self, - app_handle: tauri::AppHandle, - profile_id: String, - url: String, - ) -> Result<(), String> { - let runner = crate::browser_runner::BrowserRunner::instance(); - - // Get the profile by name - let profiles = runner - .list_profiles() - .map_err(|e| format!("Failed to list profiles: {e}"))?; - let profile = profiles - .into_iter() - .find(|p| p.id.to_string() == profile_id) - .ok_or_else(|| format!("Profile '{profile_id}' not found"))?; - - println!("Opening URL '{url}' with profile '{profile_id}'"); - - // Use launch_or_open_url which handles both launching new instances and opening in existing ones - runner - .launch_or_open_url(app_handle, &profile, Some(url.clone()), None) - .await - .map_err(|e| { - println!("Failed to open URL with profile '{profile_id}': {e}"); - format!("Failed to open URL with profile: {e}") - })?; - - println!("Successfully opened URL '{url}' with profile '{profile_id}'"); - Ok(()) - } } #[cfg(target_os = "macos")] @@ -570,15 +538,3 @@ pub async fn set_as_default_browser() -> Result<(), String> { let default_browser = DefaultBrowser::instance(); default_browser.set_as_default_browser().await } - -#[tauri::command] -pub async fn open_url_with_profile( - app_handle: tauri::AppHandle, - profile_id: String, - url: String, -) -> Result<(), String> { - let default_browser = DefaultBrowser::instance(); - default_browser - .open_url_with_profile(app_handle, profile_id, url) - .await -} diff --git a/src-tauri/src/downloaded_browsers.rs b/src-tauri/src/downloaded_browsers_registry.rs similarity index 70% rename from src-tauri/src/downloaded_browsers.rs rename to src-tauri/src/downloaded_browsers_registry.rs index 08fd5cf..99abd13 100644 --- a/src-tauri/src/downloaded_browsers.rs +++ b/src-tauri/src/downloaded_browsers_registry.rs @@ -5,6 +5,9 @@ use std::fs; use std::path::PathBuf; use std::sync::Mutex; +use crate::geoip_downloader::GeoIPDownloader; +use crate::profile::{BrowserProfile, ProfileManager}; + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct DownloadedBrowserInfo { pub browser: String, @@ -19,12 +22,18 @@ struct RegistryData { pub struct DownloadedBrowsersRegistry { data: Mutex, + profile_manager: &'static ProfileManager, + auto_updater: &'static crate::auto_updater::AutoUpdater, + geoip_downloader: &'static GeoIPDownloader, } impl DownloadedBrowsersRegistry { fn new() -> Self { Self { data: Mutex::new(RegistryData::default()), + profile_manager: ProfileManager::instance(), + auto_updater: crate::auto_updater::AutoUpdater::instance(), + geoip_downloader: GeoIPDownloader::instance(), } } @@ -88,7 +97,9 @@ impl DownloadedBrowsersRegistry { data.browsers.get_mut(browser)?.remove(version) } - pub fn is_browser_downloaded(&self, browser: &str, version: &str) -> bool { + /// Check if browser is registered in the registry (without disk validation) + /// This method only checks the in-memory registry and does not validate file existence + pub fn is_browser_registered(&self, browser: &str, version: &str) -> bool { let data = self.data.lock().unwrap(); data .browsers @@ -97,6 +108,52 @@ impl DownloadedBrowsersRegistry { .is_some() } + /// Check if browser is downloaded and files exist on disk + /// This method validates both registry entry and actual file existence + pub fn is_browser_downloaded(&self, browser: &str, version: &str) -> bool { + use crate::browser::{create_browser, BrowserType}; + + // First check if browser is registered + if !self.is_browser_registered(browser, version) { + return false; + } + + // Always check if files actually exist on disk + let browser_type = match BrowserType::from_str(browser) { + Ok(bt) => bt, + Err(_) => { + println!("Invalid browser type: {browser}"); + return false; + } + }; + let browser_instance = create_browser(browser_type.clone()); + + // Get binaries directory + let binaries_dir = if let Some(base_dirs) = directories::BaseDirs::new() { + let mut path = base_dirs.data_local_dir().to_path_buf(); + path.push(if cfg!(debug_assertions) { + "DonutBrowserDev" + } else { + "DonutBrowser" + }); + path.push("binaries"); + path + } else { + return false; + }; + + let files_exist = browser_instance.is_version_downloaded(version, &binaries_dir); + + // If files don't exist but registry thinks they do, clean up the registry + if !files_exist { + println!("Cleaning up stale registry entry for {browser} {version}"); + self.remove_browser(browser, version); + let _ = self.save(); // Don't fail if save fails, just log + } + + files_exist + } + pub fn get_downloaded_versions(&self, browser: &str) -> Vec { let data = self.data.lock().unwrap(); data @@ -196,7 +253,7 @@ impl DownloadedBrowsersRegistry { } /// Find and remove unused browser binaries that are not referenced by any active profiles - pub fn cleanup_unused_binaries( + fn cleanup_unused_binaries_internal( &self, active_profiles: &[(String, String)], // (browser, version) pairs running_profiles: &[(String, String)], // (browser, version) pairs for running profiles @@ -208,14 +265,13 @@ impl DownloadedBrowsersRegistry { let mut cleaned_up = Vec::new(); // Get pending update versions from auto updater - let pending_updates = - match crate::auto_updater::AutoUpdater::instance().get_pending_update_versions() { - Ok(updates) => updates, - Err(e) => { - eprintln!("Warning: Failed to get pending updates for cleanup: {e}"); - std::collections::HashSet::new() - } - }; + let pending_updates = match self.auto_updater.get_pending_update_versions() { + Ok(updates) => updates, + Err(e) => { + eprintln!("Warning: Failed to get pending updates for cleanup: {e}"); + std::collections::HashSet::new() + } + }; // Collect all downloaded browsers that are not in active profiles let mut to_remove = Vec::new(); @@ -293,11 +349,10 @@ impl DownloadedBrowsersRegistry { /// Verify that all registered browsers actually exist on disk and clean up stale entries pub fn verify_and_cleanup_stale_entries( &self, - browser_runner: &crate::browser_runner::BrowserRunner, ) -> Result, Box> { use crate::browser::{create_browser, BrowserType}; let mut cleaned_up = Vec::new(); - let binaries_dir = browser_runner.get_binaries_dir(); + let binaries_dir = self.profile_manager.get_binaries_dir(); let browsers_to_check: Vec<(String, String)> = { let data = self.data.lock().unwrap(); @@ -418,7 +473,7 @@ impl DownloadedBrowsersRegistry { } /// Comprehensive cleanup that removes unused binaries and syncs registry - pub fn comprehensive_cleanup( + fn comprehensive_cleanup( &self, binaries_dir: &std::path::Path, active_profiles: &[(String, String)], @@ -431,11 +486,12 @@ impl DownloadedBrowsersRegistry { cleanup_results.extend(sync_results); // Then perform the regular cleanup - let regular_cleanup = self.cleanup_unused_binaries(active_profiles, running_profiles)?; + let regular_cleanup = + self.cleanup_unused_binaries_internal(active_profiles, running_profiles)?; cleanup_results.extend(regular_cleanup); // Verify and cleanup stale entries - let stale_cleanup = self.verify_and_cleanup_stale_entries_simple(binaries_dir)?; + let stale_cleanup = self.verify_and_cleanup_stale_entries()?; cleanup_results.extend(stale_cleanup); // Clean up any remaining empty folders @@ -599,19 +655,19 @@ impl DownloadedBrowsersRegistry { pub fn consolidate_browser_versions( &self, app_handle: &tauri::AppHandle, - browser_runner: &crate::browser_runner::BrowserRunner, ) -> Result, Box> { println!("Starting browser version consolidation..."); - let profiles = browser_runner + let profiles = self + .profile_manager .list_profiles() .map_err(|e| format!("Failed to list profiles: {e}"))?; - let binaries_dir = browser_runner.get_binaries_dir(); + let binaries_dir = self.profile_manager.get_binaries_dir(); let mut consolidated = Vec::new(); // Group profiles by browser - let mut browser_profiles: std::collections::HashMap> = + let mut browser_profiles: std::collections::HashMap> = std::collections::HashMap::new(); for profile in &profiles { browser_profiles @@ -620,7 +676,7 @@ impl DownloadedBrowsersRegistry { .push(profile); } - for (browser_name, browser_profiles) in browser_profiles { + for (browser_name, browser_profiles) in browser_profiles.iter() { // Find the latest version among all profiles for this browser let mut versions: Vec<&str> = browser_profiles .iter() @@ -636,9 +692,9 @@ impl DownloadedBrowsersRegistry { // Check which profiles need to be updated to the latest version let mut profiles_to_update = Vec::new(); - let mut older_versions_to_remove = std::collections::HashSet::new(); + let mut older_versions_to_remove = std::collections::HashSet::::new(); - for profile in &browser_profiles { + for profile in browser_profiles { if profile.version != latest_version { // Only update if profile is not currently running if profile.process_id.is_none() { @@ -655,7 +711,7 @@ impl DownloadedBrowsersRegistry { // Update profiles to latest version for profile in profiles_to_update { - match browser_runner.update_profile_version( + match self.profile_manager.update_profile_version( app_handle, &profile.id.to_string(), latest_version, @@ -673,14 +729,14 @@ impl DownloadedBrowsersRegistry { } // Remove older version binaries that are no longer needed - for old_version in older_versions_to_remove { - let old_version_dir = binaries_dir.join(&browser_name).join(&old_version); + for old_version in &older_versions_to_remove { + let old_version_dir = binaries_dir.join(browser_name).join(old_version); if old_version_dir.exists() { match std::fs::remove_dir_all(&old_version_dir) { Ok(_) => { consolidated.push(format!("Removed old version: {browser_name} {old_version}")); // Also remove from registry - self.remove_browser(&browser_name, &old_version); + self.remove_browser(browser_name, old_version); } Err(e) => { eprintln!( @@ -707,36 +763,157 @@ impl DownloadedBrowsersRegistry { Ok(consolidated) } - /// Simplified version of verify_and_cleanup_stale_entries that doesn't need BrowserRunner - pub fn verify_and_cleanup_stale_entries_simple( + /// Check if browser binaries exist for all profiles and return missing binaries + pub async fn check_missing_binaries( &self, - binaries_dir: &std::path::Path, - ) -> Result, Box> { - let mut cleaned_up = Vec::new(); - let mut browsers_to_remove = Vec::new(); + ) -> Result, Box> { + use crate::browser::{create_browser, BrowserType}; + // Get all profiles + let profiles = self + .profile_manager + .list_profiles() + .map_err(|e| format!("Failed to list profiles: {e}"))?; + let mut missing_binaries = Vec::new(); - { - let data = self.data.lock().unwrap(); - for (browser_str, versions) in &data.browsers { - for version in versions.keys() { - // Check if the browser directory actually exists - let browser_dir = binaries_dir.join(browser_str).join(version); - if !browser_dir.exists() { - browsers_to_remove.push((browser_str.clone(), version.clone())); - } + for profile in profiles { + let browser_type = match BrowserType::from_str(&profile.browser) { + Ok(bt) => bt, + Err(_) => { + println!( + "Warning: Invalid browser type '{}' for profile '{}'", + profile.browser, profile.name + ); + continue; + } + }; + + let browser = create_browser(browser_type.clone()); + + // Get binaries directory + let binaries_dir = if let Some(base_dirs) = directories::BaseDirs::new() { + let mut path = base_dirs.data_local_dir().to_path_buf(); + path.push(if cfg!(debug_assertions) { + "DonutBrowserDev" + } else { + "DonutBrowser" + }); + path.push("binaries"); + path + } else { + return Err("Failed to get base directories".into()); + }; + + println!( + "binaries_dir: {binaries_dir:?} for profile: {}", + profile.name + ); + + // Check if the version is downloaded + if !browser.is_version_downloaded(&profile.version, &binaries_dir) { + missing_binaries.push((profile.name, profile.browser, profile.version)); + } + } + + Ok(missing_binaries) + } + + /// Automatically download missing binaries for all profiles + pub async fn ensure_all_binaries_exist( + &self, + app_handle: &tauri::AppHandle, + ) -> Result, Box> { + // First, clean up any stale registry entries + if let Ok(cleaned_up) = self.verify_and_cleanup_stale_entries() { + if !cleaned_up.is_empty() { + println!( + "Cleaned up {} stale registry entries: {}", + cleaned_up.len(), + cleaned_up.join(", ") + ); + } + } + + // Consolidate browser versions - keep only latest version per browser + if let Ok(consolidated) = self.consolidate_browser_versions(app_handle) { + if !consolidated.is_empty() { + println!("Version consolidation results:"); + for action in &consolidated { + println!(" {action}"); } } } - // Remove stale entries - for (browser_str, version) in browsers_to_remove { - if let Some(_removed) = self.remove_browser(&browser_str, &version) { - cleaned_up.push(format!( - "Removed stale registry entry for {browser_str} {version}" - )); + let missing_binaries = self.check_missing_binaries().await?; + let mut downloaded = Vec::new(); + + for (profile_name, browser, version) in missing_binaries { + println!("Downloading missing binary for profile '{profile_name}': {browser} {version}"); + + match crate::downloader::download_browser( + app_handle.clone(), + browser.clone(), + version.clone(), + ) + .await + { + Ok(_) => { + downloaded.push(format!( + "{browser} {version} (for profile '{profile_name}')" + )); + } + Err(e) => { + eprintln!("Failed to download {browser} {version} for profile '{profile_name}': {e}"); + } } } + // Check if GeoIP database is missing for Camoufox profiles + if self.geoip_downloader.check_missing_geoip_database()? { + println!("GeoIP database is missing for Camoufox profiles, downloading..."); + + match self + .geoip_downloader + .download_geoip_database(app_handle) + .await + { + Ok(_) => { + downloaded.push("GeoIP database for Camoufox".to_string()); + println!("GeoIP database downloaded successfully"); + } + Err(e) => { + eprintln!("Failed to download GeoIP database: {e}"); + // Don't fail the entire operation if GeoIP download fails + } + } + } + + Ok(downloaded) + } + + /// Cleanup unused binaries based on active and running profiles + pub fn cleanup_unused_binaries( + &self, + ) -> Result, Box> { + // Load current profiles using injected ProfileManager + let profiles = self + .profile_manager + .list_profiles() + .map_err(|e| format!("Failed to list profiles: {e}"))?; + + // Get active browser versions (all profiles) + let active_versions = self.get_active_browser_versions(&profiles); + + // Get running browser versions (only running profiles) + let running_versions = self.get_running_browser_versions(&profiles); + + // Get binaries directory from profile manager + let binaries_dir = self.profile_manager.get_binaries_dir(); + + // Use comprehensive cleanup that syncs registry with disk and removes unused binaries + let cleaned_up = + self.comprehensive_cleanup(&binaries_dir, &active_versions, &running_versions)?; + + // Registry is already saved by comprehensive_cleanup Ok(cleaned_up) } } @@ -758,6 +935,7 @@ mod tests { #[test] fn test_registry_creation() { + // Create a mock profile manager for testing let registry = DownloadedBrowsersRegistry::new(); let data = registry.data.lock().unwrap(); assert!(data.browsers.is_empty()); @@ -774,9 +952,9 @@ mod tests { registry.add_browser(info.clone()); - assert!(registry.is_browser_downloaded("firefox", "139.0")); - assert!(!registry.is_browser_downloaded("firefox", "140.0")); - assert!(!registry.is_browser_downloaded("chrome", "139.0")); + assert!(registry.is_browser_registered("firefox", "139.0")); + assert!(!registry.is_browser_registered("firefox", "140.0")); + assert!(!registry.is_browser_registered("chrome", "139.0")); } #[test] @@ -819,10 +997,10 @@ mod tests { // Mark download started registry.mark_download_started("firefox", "139.0", PathBuf::from("/test/path")); - // Should NOT be considered downloaded until verification completes + // Should NOT be registered until verification completes assert!( - !registry.is_browser_downloaded("firefox", "139.0"), - "Browser should NOT be considered downloaded after marking as started (only after verification)" + !registry.is_browser_registered("firefox", "139.0"), + "Browser should NOT be registered after marking as started (only after verification)" ); // Mark as completed (after verification) @@ -830,10 +1008,10 @@ mod tests { .mark_download_completed("firefox", "139.0", PathBuf::from("/test/path")) .expect("Failed to mark download as completed"); - // Should now be considered downloaded + // Should now be registered assert!( - registry.is_browser_downloaded("firefox", "139.0"), - "Browser should be considered downloaded after verification completes" + registry.is_browser_registered("firefox", "139.0"), + "Browser should be registered after verification completes" ); } @@ -848,8 +1026,8 @@ mod tests { registry.add_browser(info); assert!( - registry.is_browser_downloaded("firefox", "139.0"), - "Browser should be downloaded after adding" + registry.is_browser_registered("firefox", "139.0"), + "Browser should be registered after adding" ); let removed = registry.remove_browser("firefox", "139.0"); @@ -858,8 +1036,8 @@ mod tests { "Remove operation should return the removed browser info" ); assert!( - !registry.is_browser_downloaded("firefox", "139.0"), - "Browser should not be downloaded after removal" + !registry.is_browser_registered("firefox", "139.0"), + "Browser should not be registered after removal" ); } @@ -872,18 +1050,77 @@ mod tests { // Should NOT be registered until verification completes assert!( - !registry.is_browser_downloaded("zen", "twilight"), + !registry.is_browser_registered("zen", "twilight"), "Zen twilight version should NOT be registered until verification completes" ); // Mark as completed (after verification) - registry.mark_download_completed("zen", "twilight", PathBuf::from("/test/zen-twilight")) + registry + .mark_download_completed("zen", "twilight", PathBuf::from("/test/zen-twilight")) .expect("Failed to mark twilight download as completed"); // Now it should be registered assert!( - registry.is_browser_downloaded("zen", "twilight"), + registry.is_browser_registered("zen", "twilight"), "Zen twilight version should be registered after verification completes" ); } + + #[test] + fn test_is_browser_registered_vs_downloaded() { + let registry = DownloadedBrowsersRegistry::new(); + let info = DownloadedBrowserInfo { + browser: "firefox".to_string(), + version: "139.0".to_string(), + file_path: PathBuf::from("/test/path"), + }; + + // Add browser to registry + registry.add_browser(info); + + // Should be registered (in-memory check) + assert!( + registry.is_browser_registered("firefox", "139.0"), + "Browser should be registered after adding to registry" + ); + + // is_browser_downloaded should return false in test environment because files don't exist + // This tests the difference between registered (in registry) vs downloaded (files exist) + assert!( + !registry.is_browser_downloaded("firefox", "139.0"), + "Browser should not be considered downloaded when files don't exist on disk" + ); + } +} + +#[tauri::command] +pub fn get_downloaded_browser_versions(browser_str: String) -> Result, String> { + let registry = DownloadedBrowsersRegistry::instance(); + Ok(registry.get_downloaded_versions(&browser_str)) +} + +#[tauri::command] +pub fn is_browser_downloaded(browser_str: String, version: String) -> bool { + let registry = DownloadedBrowsersRegistry::instance(); + registry.is_browser_downloaded(&browser_str, &version) +} + +#[tauri::command] +pub async fn check_missing_binaries() -> Result, String> { + let registry = DownloadedBrowsersRegistry::instance(); + registry + .check_missing_binaries() + .await + .map_err(|e| format!("Failed to check missing binaries: {e}")) +} + +#[tauri::command] +pub async fn ensure_all_binaries_exist( + app_handle: tauri::AppHandle, +) -> Result, String> { + let registry = DownloadedBrowsersRegistry::instance(); + registry + .ensure_all_binaries_exist(&app_handle) + .await + .map_err(|e| format!("Failed to ensure all binaries exist: {e}")) } diff --git a/src-tauri/src/download.rs b/src-tauri/src/downloader.rs similarity index 66% rename from src-tauri/src/download.rs rename to src-tauri/src/downloader.rs index 8c4ec9e..fe87c30 100644 --- a/src-tauri/src/download.rs +++ b/src-tauri/src/downloader.rs @@ -2,12 +2,19 @@ use reqwest::Client; use serde::{Deserialize, Serialize}; use std::io; use std::path::{Path, PathBuf}; +use std::sync::Mutex; use tauri::Emitter; use crate::api_client::ApiClient; -use crate::browser::BrowserType; +use crate::browser::{create_browser, BrowserType}; use crate::browser_version_manager::DownloadInfo; +// Global state to track currently downloading browser-version pairs +lazy_static::lazy_static! { + static ref DOWNLOADING_BROWSERS: std::sync::Arc>> = + std::sync::Arc::new(Mutex::new(std::collections::HashSet::new())); +} + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct DownloadProgress { pub browser: String, @@ -23,6 +30,10 @@ pub struct DownloadProgress { pub struct Downloader { client: Client, api_client: &'static ApiClient, + registry: &'static crate::downloaded_browsers_registry::DownloadedBrowsersRegistry, + version_service: &'static crate::browser_version_manager::BrowserVersionManager, + extractor: &'static crate::extraction::Extractor, + geoip_downloader: &'static crate::geoip_downloader::GeoIPDownloader, } impl Downloader { @@ -30,6 +41,10 @@ impl Downloader { Self { client: Client::new(), api_client: ApiClient::instance(), + registry: crate::downloaded_browsers_registry::DownloadedBrowsersRegistry::instance(), + version_service: crate::browser_version_manager::BrowserVersionManager::instance(), + extractor: crate::extraction::Extractor::instance(), + geoip_downloader: crate::geoip_downloader::GeoIPDownloader::instance(), } } @@ -42,6 +57,10 @@ impl Downloader { Self { client: Client::new(), api_client: ApiClient::instance(), + registry: crate::downloaded_browsers_registry::DownloadedBrowsersRegistry::instance(), + version_service: crate::browser_version_manager::BrowserVersionManager::instance(), + extractor: crate::extraction::Extractor::instance(), + geoip_downloader: crate::geoip_downloader::GeoIPDownloader::instance(), } } @@ -573,6 +592,327 @@ impl Downloader { Ok(file_path) } + + /// Download a browser binary, verify it, and register it in the downloaded browsers registry + pub async fn download_browser_full( + &self, + app_handle: &tauri::AppHandle, + browser_str: String, + version: String, + ) -> Result> { + // Check if this browser-version pair is already being downloaded + let download_key = format!("{browser_str}-{version}"); + { + let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap(); + if downloading.contains(&download_key) { + return Err(format!("Browser '{browser_str}' version '{version}' is already being downloaded. Please wait for the current download to complete.").into()); + } + // Mark this browser-version pair as being downloaded + downloading.insert(download_key.clone()); + } + + let browser_type = + BrowserType::from_str(&browser_str).map_err(|e| format!("Invalid browser type: {e}"))?; + let browser = create_browser(browser_type.clone()); + + // Use injected registry instance + + // Get binaries directory - we need to get it from somewhere + // This is a bit tricky since we don't have access to BrowserRunner's get_binaries_dir + // We'll need to replicate this logic + let binaries_dir = if let Some(base_dirs) = directories::BaseDirs::new() { + let mut path = base_dirs.data_local_dir().to_path_buf(); + path.push(if cfg!(debug_assertions) { + "DonutBrowserDev" + } else { + "DonutBrowser" + }); + path.push("binaries"); + path + } else { + return Err("Failed to get base directories".into()); + }; + + // Check if registry thinks it's downloaded, but also verify files actually exist + if self.registry.is_browser_downloaded(&browser_str, &version) { + let actually_exists = browser.is_version_downloaded(&version, &binaries_dir); + + if actually_exists { + // Remove from downloading set since it's already downloaded + let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap(); + downloading.remove(&download_key); + return Ok(version); + } else { + // Registry says it's downloaded but files don't exist - clean up registry + println!("Registry indicates {browser_str} {version} is downloaded, but files are missing. Cleaning up registry entry."); + self.registry.remove_browser(&browser_str, &version); + self + .registry + .save() + .map_err(|e| format!("Failed to save cleaned registry: {e}"))?; + } + } + + // Check if browser is supported on current platform before attempting download + if !self + .version_service + .is_browser_supported(&browser_str) + .unwrap_or(false) + { + // Remove from downloading set on error + let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap(); + downloading.remove(&download_key); + return Err( + format!( + "Browser '{}' is not supported on your platform ({} {}). Supported browsers: {}", + browser_str, + std::env::consts::OS, + std::env::consts::ARCH, + self.version_service.get_supported_browsers().join(", ") + ) + .into(), + ); + } + + let download_info = self + .version_service + .get_download_info(&browser_str, &version) + .map_err(|e| format!("Failed to get download info: {e}"))?; + + // Create browser directory + let mut browser_dir = binaries_dir.clone(); + browser_dir.push(&browser_str); + browser_dir.push(&version); + + std::fs::create_dir_all(&browser_dir) + .map_err(|e| format!("Failed to create browser directory: {e}"))?; + + // Mark download as started (but don't add to registry yet) + self + .registry + .mark_download_started(&browser_str, &version, browser_dir.clone()); + + // Attempt to download the archive. If the download fails but an archive with the + // expected filename already exists (manual download), continue using that file. + let download_path: PathBuf = match self + .download_browser( + app_handle, + browser_type.clone(), + &version, + &download_info, + &browser_dir, + ) + .await + { + Ok(path) => path, + Err(e) => { + // Do NOT continue with extraction on failed downloads. Partial files may exist but are invalid. + // Clean registry entry and stop here so the UI can show a single, clear error. + let _ = self.registry.remove_browser(&browser_str, &version); + let _ = self.registry.save(); + let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap(); + downloading.remove(&download_key); + return Err(format!("Failed to download browser: {e}").into()); + } + }; + + // Use the extraction module + if download_info.is_archive { + match self + .extractor + .extract_browser( + app_handle, + browser_type.clone(), + &version, + &download_path, + &browser_dir, + ) + .await + { + Ok(_) => { + // Do not remove the archive here. We keep it until verification succeeds. + } + Err(e) => { + // Do not remove the archive or extracted files. Just drop the registry entry + // so it won't be reported as downloaded. + let _ = self.registry.remove_browser(&browser_str, &version); + let _ = self.registry.save(); + // Remove browser-version pair from downloading set on error + { + let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap(); + downloading.remove(&download_key); + } + return Err(format!("Failed to extract browser: {e}").into()); + } + } + + // Give filesystem a moment to settle after extraction + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + } + + // Emit verification progress + let progress = DownloadProgress { + browser: browser_str.clone(), + version: version.clone(), + downloaded_bytes: 0, + total_bytes: None, + percentage: 100.0, + speed_bytes_per_sec: 0.0, + eta_seconds: None, + stage: "verifying".to_string(), + }; + let _ = app_handle.emit("download-progress", &progress); + + // Verify the browser was downloaded correctly + println!("Verifying download for browser: {browser_str}, version: {version}"); + + // Use the browser's own verification method + if !browser.is_version_downloaded(&version, &binaries_dir) { + // Provide detailed error information for debugging + let browser_dir = binaries_dir.join(&browser_str).join(&version); + let mut error_details = format!( + "Browser download completed but verification failed for {} {}. Expected directory: {}", + browser_str, + version, + browser_dir.display() + ); + + // List what files actually exist + if browser_dir.exists() { + error_details.push_str("\nFiles found in directory:"); + if let Ok(entries) = std::fs::read_dir(&browser_dir) { + for entry in entries.flatten() { + let path = entry.path(); + let file_type = if path.is_dir() { "DIR" } else { "FILE" }; + error_details.push_str(&format!("\n {} {}", file_type, path.display())); + } + } else { + error_details.push_str("\n (Could not read directory contents)"); + } + } else { + error_details.push_str("\nDirectory does not exist!"); + } + + // For Camoufox on Linux, provide specific expected files + if browser_str == "camoufox" && cfg!(target_os = "linux") { + let camoufox_subdir = browser_dir.join("camoufox"); + error_details.push_str("\nExpected Camoufox executable locations:"); + error_details.push_str(&format!("\n {}/camoufox-bin", camoufox_subdir.display())); + error_details.push_str(&format!("\n {}/camoufox", camoufox_subdir.display())); + + if camoufox_subdir.exists() { + error_details.push_str(&format!( + "\nCamoufox subdirectory exists: {}", + camoufox_subdir.display() + )); + if let Ok(entries) = std::fs::read_dir(&camoufox_subdir) { + error_details.push_str("\nFiles in camoufox subdirectory:"); + for entry in entries.flatten() { + let path = entry.path(); + let file_type = if path.is_dir() { "DIR" } else { "FILE" }; + error_details.push_str(&format!("\n {} {}", file_type, path.display())); + } + } + } else { + error_details.push_str(&format!( + "\nCamoufox subdirectory does not exist: {}", + camoufox_subdir.display() + )); + } + } + + // Do not delete files on verification failure; keep archive for manual retry. + let _ = self.registry.remove_browser(&browser_str, &version); + let _ = self.registry.save(); + // Remove browser-version pair from downloading set on verification failure + { + let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap(); + downloading.remove(&download_key); + } + return Err(error_details.into()); + } + + // Mark completion in registry - only now add to registry after verification + if let Err(e) = + self + .registry + .mark_download_completed(&browser_str, &version, browser_dir.clone()) + { + eprintln!("Warning: Could not mark {browser_str} {version} as completed in registry: {e}"); + } + self + .registry + .save() + .map_err(|e| format!("Failed to save registry: {e}"))?; + + // Now that verification succeeded, remove the archive file if it exists + if download_info.is_archive { + let archive_path = browser_dir.join(&download_info.filename); + if archive_path.exists() { + if let Err(e) = std::fs::remove_file(&archive_path) { + println!("Warning: Could not delete archive file after verification: {e}"); + } + } + } + + // If this is Camoufox, automatically download GeoIP database + if browser_str == "camoufox" { + // Check if GeoIP database is already available + if !crate::geoip_downloader::GeoIPDownloader::is_geoip_database_available() { + println!("Downloading GeoIP database for Camoufox..."); + + match self + .geoip_downloader + .download_geoip_database(app_handle) + .await + { + Ok(_) => { + println!("GeoIP database downloaded successfully"); + } + Err(e) => { + eprintln!("Failed to download GeoIP database: {e}"); + // Don't fail the browser download if GeoIP download fails + } + } + } else { + println!("GeoIP database already available"); + } + } + + // Emit completion + let progress = DownloadProgress { + browser: browser_str.clone(), + version: version.clone(), + downloaded_bytes: 0, + total_bytes: None, + percentage: 100.0, + speed_bytes_per_sec: 0.0, + eta_seconds: Some(0.0), + stage: "completed".to_string(), + }; + let _ = app_handle.emit("download-progress", &progress); + + // Remove browser-version pair from downloading set + { + let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap(); + downloading.remove(&download_key); + } + + Ok(version) + } +} + +#[tauri::command] +pub async fn download_browser( + app_handle: tauri::AppHandle, + browser_str: String, + version: String, +) -> Result { + let downloader = Downloader::instance(); + downloader + .download_browser_full(&app_handle, browser_str, version) + .await + .map_err(|e| format!("Failed to download browser: {e}")) } #[cfg(test)] diff --git a/src-tauri/src/extraction.rs b/src-tauri/src/extraction.rs index 3989dc6..af3436f 100644 --- a/src-tauri/src/extraction.rs +++ b/src-tauri/src/extraction.rs @@ -4,7 +4,7 @@ use std::path::{Path, PathBuf}; use tauri::Emitter; use crate::browser::BrowserType; -use crate::download::DownloadProgress; +use crate::downloader::DownloadProgress; #[cfg(any(target_os = "macos", target_os = "windows"))] use std::process::Command; diff --git a/src-tauri/src/geoip_downloader.rs b/src-tauri/src/geoip_downloader.rs index c92866b..5d2ed39 100644 --- a/src-tauri/src/geoip_downloader.rs +++ b/src-tauri/src/geoip_downloader.rs @@ -1,4 +1,5 @@ use crate::browser::GithubRelease; +use crate::profile::manager::ProfileManager; use directories::BaseDirs; use reqwest::Client; use serde::{Deserialize, Serialize}; @@ -75,6 +76,25 @@ impl GeoIPDownloader { false } } + /// Check if GeoIP database is missing for Camoufox profiles + pub fn check_missing_geoip_database( + &self, + ) -> Result> { + // Get all profiles + let profiles = ProfileManager::instance() + .list_profiles() + .map_err(|e| format!("Failed to list profiles: {e}"))?; + + // Check if there are any Camoufox profiles + let has_camoufox_profiles = profiles.iter().any(|profile| profile.browser == "camoufox"); + + if has_camoufox_profiles { + // Check if GeoIP database is available + return Ok(!Self::is_geoip_database_available()); + } + + Ok(false) + } fn find_city_mmdb_asset(&self, release: &GithubRelease) -> Option { for asset in &release.assets { @@ -218,6 +238,19 @@ impl GeoIPDownloader { } } +#[tauri::command] +pub fn check_missing_geoip_database() -> Result { + let geoip_downloader = GeoIPDownloader::instance(); + geoip_downloader + .check_missing_geoip_database() + .map_err(|e| format!("Failed to check missing GeoIP database: {e}")) +} + +// Global singleton instance +lazy_static::lazy_static! { + static ref GEOIP_DOWNLOADER: GeoIPDownloader = GeoIPDownloader::new(); +} + #[cfg(test)] mod tests { use super::*; @@ -353,8 +386,3 @@ mod tests { ); } } - -// Global singleton instance -lazy_static::lazy_static! { - static ref GEOIP_DOWNLOADER: GeoIPDownloader = GeoIPDownloader::new(); -} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0231d10..24fff61 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -14,10 +14,10 @@ mod auto_updater; mod browser; mod browser_runner; mod browser_version_manager; -mod camoufox; +mod camoufox_manager; mod default_browser; -mod download; -mod downloaded_browsers; +mod downloaded_browsers_registry; +mod downloader; mod extraction; mod geoip_downloader; mod group_manager; @@ -31,24 +31,38 @@ mod tag_manager; mod version_updater; use browser_runner::{ - check_browser_exists, check_browser_status, check_missing_binaries, check_missing_geoip_database, - create_browser_profile_new, delete_profile, download_browser, ensure_all_binaries_exist, - fetch_browser_versions_cached_first, fetch_browser_versions_with_count, - fetch_browser_versions_with_count_cached_first, get_all_tags, get_downloaded_browser_versions, - get_supported_browsers, is_browser_supported_on_platform, kill_browser_profile, - launch_browser_profile, list_browser_profiles, rename_profile, update_camoufox_config, - update_profile_proxy, update_profile_tags, + check_browser_exists, kill_browser_profile, launch_browser_profile, open_url_with_profile, }; +use profile::manager::{ + check_browser_status, create_browser_profile_new, delete_profile, list_browser_profiles, + rename_profile, update_camoufox_config, update_profile_proxy, update_profile_tags, +}; + +use browser_version_manager::{ + fetch_browser_versions_cached_first, fetch_browser_versions_with_count, + fetch_browser_versions_with_count_cached_first, get_supported_browsers, + is_browser_supported_on_platform, +}; + +use downloaded_browsers_registry::{ + check_missing_binaries, ensure_all_binaries_exist, get_downloaded_browser_versions, +}; + +use downloader::download_browser; + use settings_manager::{ - clear_all_version_cache_and_refetch, get_app_settings, get_table_sorting_settings, - save_app_settings, save_table_sorting_settings, should_show_settings_on_startup, + get_app_settings, get_table_sorting_settings, save_app_settings, save_table_sorting_settings, + should_show_settings_on_startup, }; -use default_browser::{is_default_browser, open_url_with_profile, set_as_default_browser}; +use tag_manager::get_all_tags; + +use default_browser::{is_default_browser, set_as_default_browser}; use version_updater::{ - get_version_update_status, get_version_updater, trigger_manual_version_update, + clear_all_version_cache_and_refetch, get_version_update_status, get_version_updater, + trigger_manual_version_update, }; use auto_updater::{ @@ -66,7 +80,7 @@ use group_manager::{ get_groups_with_profile_counts, get_profile_groups, update_profile_group, }; -use geoip_downloader::GeoIPDownloader; +use geoip_downloader::{check_missing_geoip_database, GeoIPDownloader}; use browser_version_manager::get_browser_release_types; @@ -379,8 +393,9 @@ pub fn run() { loop { interval.tick().await; - let browser_runner = crate::browser_runner::BrowserRunner::instance(); - if let Err(e) = browser_runner.cleanup_unused_binaries_internal() { + let registry = + crate::downloaded_browsers_registry::DownloadedBrowsersRegistry::instance(); + if let Err(e) = registry.cleanup_unused_binaries() { eprintln!("Periodic cleanup failed: {e}"); } else { println!("Periodic cleanup completed successfully"); @@ -417,14 +432,14 @@ pub fn run() { // Start Camoufox cleanup task let _app_handle_cleanup = app.handle().clone(); tauri::async_runtime::spawn(async move { - let launcher = crate::camoufox::CamoufoxNodecarLauncher::instance(); + let camoufox_manager = crate::camoufox_manager::CamoufoxManager::instance(); let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5)); loop { interval.tick().await; - match launcher.cleanup_dead_instances().await { - Ok(_dead_instances) => { + match camoufox_manager.cleanup_dead_instances().await { + Ok(_) => { // Cleanup completed silently } Err(e) => { @@ -440,8 +455,8 @@ pub fn run() { // Wait a bit for the app to fully initialize tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; - let browser_runner = crate::browser_runner::BrowserRunner::instance(); - match browser_runner.check_missing_geoip_database() { + let geoip_downloader = crate::geoip_downloader::GeoIPDownloader::instance(); + match geoip_downloader.check_missing_geoip_database() { Ok(true) => { println!("GeoIP database is missing for Camoufox profiles, downloading at startup..."); let geoip_downloader = GeoIPDownloader::instance(); @@ -502,7 +517,7 @@ pub fn run() { let runner = crate::browser_runner::BrowserRunner::instance(); // If listing profiles fails, skip this tick - let profiles = match runner.list_profiles() { + let profiles = match runner.profile_manager.list_profiles() { Ok(p) => p, Err(e) => { println!("Warning: Failed to list profiles in status checker: {e}"); @@ -658,7 +673,6 @@ pub fn run() { check_for_app_updates, check_for_app_updates_manual, download_and_install_app_update, - // get_system_theme, // removed detect_existing_profiles, import_browser_profile, check_missing_binaries, diff --git a/src-tauri/src/profile/manager.rs b/src-tauri/src/profile/manager.rs index eb0f128..071ebfc 100644 --- a/src-tauri/src/profile/manager.rs +++ b/src-tauri/src/profile/manager.rs @@ -1,5 +1,7 @@ +use crate::api_client::is_browser_version_nightly; use crate::browser::{create_browser, BrowserType, ProxySettings}; -use crate::camoufox::CamoufoxConfig; +use crate::camoufox_manager::CamoufoxConfig; +use crate::downloaded_browsers_registry::DownloadedBrowsersRegistry; use crate::profile::types::BrowserProfile; use crate::proxy_manager::PROXY_MANAGER; use directories::BaseDirs; @@ -10,12 +12,14 @@ use tauri::Emitter; pub struct ProfileManager { base_dirs: BaseDirs, + camoufox_manager: &'static crate::camoufox_manager::CamoufoxManager, } impl ProfileManager { fn new() -> Self { Self { base_dirs: BaseDirs::new().expect("Failed to get base directories"), + camoufox_manager: crate::camoufox_manager::CamoufoxManager::instance(), } } @@ -34,6 +38,17 @@ impl ProfileManager { path } + pub fn get_binaries_dir(&self) -> PathBuf { + let mut path = self.base_dirs.data_local_dir().to_path_buf(); + path.push(if cfg!(debug_assertions) { + "DonutBrowserDev" + } else { + "DonutBrowser" + }); + path.push("binaries"); + path + } + #[allow(clippy::too_many_arguments)] pub async fn create_profile_with_group( &self, @@ -72,13 +87,12 @@ impl ProfileManager { let final_camoufox_config = if browser == "camoufox" { let mut config = camoufox_config.unwrap_or_else(|| { println!("Creating default Camoufox config for profile: {name}"); - crate::camoufox::CamoufoxConfig::default() + crate::camoufox_manager::CamoufoxConfig::default() }); // Always ensure executable_path is set to the user's binary location if config.executable_path.is_none() { - let browser_runner = crate::browser_runner::BrowserRunner::instance(); - let mut browser_dir = browser_runner.get_binaries_dir(); + let mut browser_dir = self.get_binaries_dir(); browser_dir.push(browser); browser_dir.push(version); @@ -137,7 +151,6 @@ impl ProfileManager { println!("Generating fingerprint for Camoufox profile: {name}"); // Use the camoufox launcher to generate the config - let camoufox_launcher = crate::camoufox::CamoufoxNodecarLauncher::instance(); // Create a temporary profile for fingerprint generation let temp_profile = BrowserProfile { @@ -154,7 +167,8 @@ impl ProfileManager { tags: Vec::new(), }; - match camoufox_launcher + match self + .camoufox_manager .generate_fingerprint_config(app_handle, &temp_profile, &config) .await { @@ -237,6 +251,11 @@ impl ProfileManager { let json = serde_json::to_string_pretty(profile)?; fs::write(profile_file, json)?; + // Update tag suggestions after any save + let _ = crate::tag_manager::TAG_MANAGER.lock().map(|tm| { + let _ = tm.rebuild_from_profiles(&self.list_profiles().unwrap_or_default()); + }); + Ok(()) } @@ -355,6 +374,11 @@ impl ProfileManager { let _ = tm.rebuild_from_profiles(&self.list_profiles().unwrap_or_default()); }); + // Always perform cleanup after profile deletion to remove unused binaries + if let Err(e) = DownloadedBrowsersRegistry::instance().cleanup_unused_binaries() { + println!("Warning: Failed to cleanup unused binaries after profile deletion: {e}"); + } + // Emit profile deletion event if let Err(e) = app_handle.emit("profiles-changed", ()) { println!("Warning: Failed to emit profiles-changed event: {e}"); @@ -399,12 +423,11 @@ impl ProfileManager { profile.version = version.to_string(); // Update the release_type based on the version and browser - profile.release_type = - if crate::api_client::is_browser_version_nightly(&profile.browser, version, None) { - "nightly".to_string() - } else { - "stable".to_string() - }; + profile.release_type = if is_browser_version_nightly(&profile.browser, version, None) { + "nightly".to_string() + } else { + "stable".to_string() + }; // Save the updated profile self.save_profile(&profile)?; @@ -866,9 +889,7 @@ impl ProfileManager { app_handle: &tauri::AppHandle, profile: &BrowserProfile, ) -> Result> { - use crate::camoufox::CamoufoxNodecarLauncher; - - let launcher = CamoufoxNodecarLauncher::instance(); + let launcher = self.camoufox_manager; let profiles_dir = self.get_profiles_dir(); let profile_data_path = profile.get_profile_data_path(&profiles_dir); let profile_path_str = profile_data_path.to_string_lossy(); @@ -1012,17 +1033,6 @@ impl ProfileManager { } } - fn get_binaries_dir(&self) -> PathBuf { - let mut path = self.base_dirs.data_local_dir().to_path_buf(); - path.push(if cfg!(debug_assertions) { - "DonutBrowserDev" - } else { - "DonutBrowser" - }); - path.push("binaries"); - path - } - fn get_common_firefox_preferences(&self) -> Vec { vec![ // Disable default browser updates @@ -1202,23 +1212,6 @@ mod tests { ); } - #[test] - fn test_list_profiles_empty() { - let (manager, _temp_dir) = create_test_profile_manager(); - - let result = manager.list_profiles(); - assert!( - result.is_ok(), - "Should successfully list profiles even when empty" - ); - - let profiles = result.unwrap(); - assert!( - profiles.is_empty(), - "Should return empty vector when no profiles exist" - ); - } - #[test] fn test_get_common_firefox_preferences() { let (manager, _temp_dir) = create_test_profile_manager(); @@ -1324,7 +1317,139 @@ mod tests { } } +#[allow(clippy::too_many_arguments)] +#[tauri::command] +pub async fn create_browser_profile_with_group( + app_handle: tauri::AppHandle, + name: String, + browser: String, + version: String, + release_type: String, + proxy_id: Option, + camoufox_config: Option, + group_id: Option, +) -> Result { + let profile_manager = ProfileManager::instance(); + profile_manager + .create_profile_with_group( + &app_handle, + &name, + &browser, + &version, + &release_type, + proxy_id, + camoufox_config, + group_id, + ) + .await + .map_err(|e| format!("Failed to create profile: {e}")) +} + +#[tauri::command] +pub fn list_browser_profiles() -> Result, String> { + let profile_manager = ProfileManager::instance(); + profile_manager + .list_profiles() + .map_err(|e| format!("Failed to list profiles: {e}")) +} + +#[tauri::command] +pub async fn update_profile_proxy( + app_handle: tauri::AppHandle, + profile_id: String, + proxy_id: Option, +) -> Result { + let profile_manager = ProfileManager::instance(); + profile_manager + .update_profile_proxy(app_handle, &profile_id, proxy_id) + .await + .map_err(|e| format!("Failed to update profile: {e}")) +} + +#[tauri::command] +pub fn update_profile_tags( + app_handle: tauri::AppHandle, + profile_id: String, + tags: Vec, +) -> Result { + let profile_manager = ProfileManager::instance(); + profile_manager + .update_profile_tags(&app_handle, &profile_id, tags) + .map_err(|e| format!("Failed to update profile tags: {e}")) +} + +#[tauri::command] +pub async fn check_browser_status( + app_handle: tauri::AppHandle, + profile: BrowserProfile, +) -> Result { + let profile_manager = ProfileManager::instance(); + profile_manager + .check_browser_status(app_handle, &profile) + .await + .map_err(|e| format!("Failed to check browser status: {e}")) +} + +#[tauri::command] +pub fn rename_profile( + app_handle: tauri::AppHandle, + profile_id: String, + new_name: String, +) -> Result { + let profile_manager = ProfileManager::instance(); + profile_manager + .rename_profile(&app_handle, &profile_id, &new_name) + .map_err(|e| format!("Failed to rename profile: {e}")) +} + +#[allow(clippy::too_many_arguments)] +#[tauri::command] +pub async fn create_browser_profile_new( + app_handle: tauri::AppHandle, + name: String, + browser_str: String, + version: String, + release_type: String, + proxy_id: Option, + camoufox_config: Option, + group_id: Option, +) -> Result { + let browser_type = + BrowserType::from_str(&browser_str).map_err(|e| format!("Invalid browser type: {e}"))?; + create_browser_profile_with_group( + app_handle, + name, + browser_type.as_str().to_string(), + version, + release_type, + proxy_id, + camoufox_config, + group_id, + ) + .await +} + +#[tauri::command] +pub async fn update_camoufox_config( + app_handle: tauri::AppHandle, + profile_id: String, + config: CamoufoxConfig, +) -> Result<(), String> { + let profile_manager = ProfileManager::instance(); + profile_manager + .update_camoufox_config(app_handle, &profile_id, config) + .await + .map_err(|e| format!("Failed to update Camoufox config: {e}")) +} + // Global singleton instance +#[tauri::command] +pub fn delete_profile(app_handle: tauri::AppHandle, profile_id: String) -> Result<(), String> { + ProfileManager::instance() + .delete_profile(&app_handle, &profile_id) + .map_err(|e| format!("Failed to delete profile: {e}")) +} + lazy_static::lazy_static! { static ref PROFILE_MANAGER: ProfileManager = ProfileManager::new(); } diff --git a/src-tauri/src/profile/types.rs b/src-tauri/src/profile/types.rs index 41c0550..c991d36 100644 --- a/src-tauri/src/profile/types.rs +++ b/src-tauri/src/profile/types.rs @@ -1,4 +1,4 @@ -use crate::camoufox::CamoufoxConfig; +use crate::camoufox_manager::CamoufoxConfig; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; diff --git a/src-tauri/src/profile_importer.rs b/src-tauri/src/profile_importer.rs index ce72a6b..32686ce 100644 --- a/src-tauri/src/profile_importer.rs +++ b/src-tauri/src/profile_importer.rs @@ -5,7 +5,8 @@ use std::fs::{self, create_dir_all}; use std::path::{Path, PathBuf}; use crate::browser::BrowserType; -use crate::browser_runner::BrowserRunner; +use crate::downloaded_browsers_registry::DownloadedBrowsersRegistry; +use crate::profile::ProfileManager; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct DetectedProfile { @@ -17,12 +18,16 @@ pub struct DetectedProfile { pub struct ProfileImporter { base_dirs: BaseDirs, + downloaded_browsers_registry: &'static DownloadedBrowsersRegistry, + profile_manager: &'static ProfileManager, } impl ProfileImporter { fn new() -> Self { Self { base_dirs: BaseDirs::new().expect("Failed to get base directories"), + downloaded_browsers_registry: DownloadedBrowsersRegistry::instance(), + profile_manager: ProfileManager::instance(), } } @@ -520,7 +525,7 @@ impl ProfileImporter { .map_err(|_| format!("Invalid browser type: {browser_type}"))?; // Check if a profile with this name already exists - let existing_profiles = BrowserRunner::instance().list_profiles()?; + let existing_profiles = self.profile_manager.list_profiles()?; if existing_profiles .iter() .any(|p| p.name.to_lowercase() == new_profile_name.to_lowercase()) @@ -530,7 +535,7 @@ impl ProfileImporter { // Generate UUID for new profile and create the directory structure let profile_id = uuid::Uuid::new_v4(); - let profiles_dir = BrowserRunner::instance().get_profiles_dir(); + let profiles_dir = self.profile_manager.get_profiles_dir(); let new_profile_uuid_dir = profiles_dir.join(profile_id.to_string()); let new_profile_data_dir = new_profile_uuid_dir.join("profile"); @@ -559,7 +564,7 @@ impl ProfileImporter { }; // Save the profile metadata - BrowserRunner::instance().save_profile(&profile)?; + self.profile_manager.save_profile(&profile)?; println!( "Successfully imported profile '{}' from '{}'", @@ -576,8 +581,9 @@ impl ProfileImporter { browser_type: &str, ) -> Result> { // Check if any version of the browser is downloaded - let registry = crate::downloaded_browsers::DownloadedBrowsersRegistry::instance(); - let downloaded_versions = registry.get_downloaded_versions(browser_type); + let downloaded_versions = self + .downloaded_browsers_registry + .get_downloaded_versions(browser_type); if let Some(version) = downloaded_versions.first() { return Ok(version.clone()); diff --git a/src-tauri/src/settings_manager.rs b/src-tauri/src/settings_manager.rs index 6e4ba28..3b25a8f 100644 --- a/src-tauri/src/settings_manager.rs +++ b/src-tauri/src/settings_manager.rs @@ -3,9 +3,6 @@ use serde::{Deserialize, Serialize}; use std::fs::{self, create_dir_all}; use std::path::PathBuf; -use crate::api_client::ApiClient; -use crate::version_updater; - use aes_gcm::{ aead::{Aead, AeadCore, KeyInit, OsRng}, Aes256Gcm, Key, Nonce, @@ -442,54 +439,6 @@ pub async fn save_table_sorting_settings(sorting: TableSortingSettings) -> Resul .map_err(|e| format!("Failed to save table sorting settings: {e}")) } -#[tauri::command] -pub async fn clear_all_version_cache_and_refetch( - app_handle: tauri::AppHandle, -) -> Result<(), String> { - let api_client = ApiClient::instance(); - - // Clear all cache first - api_client - .clear_all_cache() - .map_err(|e| format!("Failed to clear version cache: {e}"))?; - - // Disable all browsers during the update process - let auto_updater = crate::auto_updater::AutoUpdater::instance(); - let supported_browsers = - crate::browser_version_manager::BrowserVersionManager::instance().get_supported_browsers(); - - // Load current state and disable all browsers - let mut state = auto_updater - .load_auto_update_state() - .map_err(|e| format!("Failed to load auto update state: {e}"))?; - for browser in &supported_browsers { - state.disabled_browsers.insert(browser.clone()); - } - auto_updater - .save_auto_update_state(&state) - .map_err(|e| format!("Failed to save auto update state: {e}"))?; - - let updater = version_updater::get_version_updater(); - let updater_guard = updater.lock().await; - - let result = updater_guard - .trigger_manual_update(&app_handle) - .await - .map_err(|e| format!("Failed to trigger version update: {e}")); - - // Re-enable all browsers after the update completes (regardless of success/failure) - let mut final_state = auto_updater.load_auto_update_state().unwrap_or_default(); - for browser in &supported_browsers { - final_state.disabled_browsers.remove(browser); - } - if let Err(e) = auto_updater.save_auto_update_state(&final_state) { - eprintln!("Warning: Failed to re-enable browsers after cache clear: {e}"); - } - - result?; - Ok(()) -} - // Global singleton instance lazy_static::lazy_static! { static ref SETTINGS_MANAGER: SettingsManager = SettingsManager::new(); diff --git a/src-tauri/src/tag_manager.rs b/src-tauri/src/tag_manager.rs index 4edc4ce..7577d6e 100644 --- a/src-tauri/src/tag_manager.rs +++ b/src-tauri/src/tag_manager.rs @@ -101,6 +101,14 @@ impl TagManager { } } +#[tauri::command] +pub fn get_all_tags() -> Result, String> { + let tag_manager = crate::tag_manager::TAG_MANAGER.lock().unwrap(); + tag_manager + .get_all_tags() + .map_err(|e| format!("Failed to get tags: {e}")) +} + lazy_static::lazy_static! { pub static ref TAG_MANAGER: std::sync::Mutex = std::sync::Mutex::new(TagManager::new()); } diff --git a/src-tauri/src/version_updater.rs b/src-tauri/src/version_updater.rs index 93f18f7..90a514c 100644 --- a/src-tauri/src/version_updater.rs +++ b/src-tauri/src/version_updater.rs @@ -46,8 +46,9 @@ impl Default for BackgroundUpdateState { } } +/// Extension of auto_updater.rs for background updates pub struct VersionUpdater { - version_service: &'static BrowserVersionManager, + browser_version_manager: &'static BrowserVersionManager, auto_updater: &'static AutoUpdater, app_handle: Option, } @@ -55,7 +56,7 @@ pub struct VersionUpdater { impl VersionUpdater { pub fn new() -> Self { Self { - version_service: BrowserVersionManager::instance(), + browser_version_manager: BrowserVersionManager::instance(), auto_updater: AutoUpdater::instance(), app_handle: None, } @@ -263,7 +264,7 @@ impl VersionUpdater { &self, app_handle: &tauri::AppHandle, ) -> Result, Box> { - let supported_browsers = self.version_service.get_supported_browsers(); + let supported_browsers = self.browser_version_manager.get_supported_browsers(); let total_browsers = supported_browsers.len(); let mut results = Vec::new(); let mut total_new_versions = 0; @@ -374,7 +375,7 @@ impl VersionUpdater { browser: &str, ) -> Result> { self - .version_service + .browser_version_manager .update_browser_versions_incrementally(browser) .await } @@ -455,6 +456,63 @@ pub async fn get_version_update_status() -> Result<(Option, u64), String> { Ok((last_update, time_until_next)) } +#[tauri::command] +pub async fn clear_all_version_cache_and_refetch( + app_handle: tauri::AppHandle, +) -> Result<(), String> { + let api_client = crate::api_client::ApiClient::instance(); + let version_updater = VersionUpdater::new(); + + // Clear all cache first + api_client + .clear_all_cache() + .map_err(|e| format!("Failed to clear version cache: {e}"))?; + + // Disable all browsers during the update process + let supported_browsers = version_updater + .browser_version_manager + .get_supported_browsers(); + + // Load current state and disable all browsers + let mut state = version_updater + .auto_updater + .load_auto_update_state() + .map_err(|e| format!("Failed to load auto update state: {e}"))?; + for browser in &supported_browsers { + state.disabled_browsers.insert(browser.clone()); + } + version_updater + .auto_updater + .save_auto_update_state(&state) + .map_err(|e| format!("Failed to save auto update state: {e}"))?; + + let updater = get_version_updater(); + let updater_guard = updater.lock().await; + + let result = updater_guard + .trigger_manual_update(&app_handle) + .await + .map_err(|e| format!("Failed to trigger version update: {e}")); + + // Re-enable all browsers after the update completes (regardless of success/failure) + let mut final_state = version_updater + .auto_updater + .load_auto_update_state() + .unwrap_or_default(); + for browser in &supported_browsers { + final_state.disabled_browsers.remove(browser); + } + if let Err(e) = version_updater + .auto_updater + .save_auto_update_state(&final_state) + { + eprintln!("Warning: Failed to re-enable browsers after cache clear: {e}"); + } + + result?; + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -607,7 +665,10 @@ mod tests { // Should have valid references to services assert!( - !std::ptr::eq(updater.version_service as *const _, std::ptr::null()), + !std::ptr::eq( + updater.browser_version_manager as *const _, + std::ptr::null() + ), "Version service should not be null" ); assert!(