diff --git a/src-tauri/src/api_client.rs b/src-tauri/src/api_client.rs index 83a7e92..4814e9d 100644 --- a/src-tauri/src/api_client.rs +++ b/src-tauri/src/api_client.rs @@ -637,15 +637,39 @@ impl ApiClient { "{}/repos/mullvad/mullvad-browser/releases?per_page=100", self.github_api_base ); - let releases = self + + let response = self .client .get(url) .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36") .send() - .await? - .json::>() .await?; + if !response.status().is_success() { + return Err(format!("GitHub API returned status: {}", response.status()).into()); + } + + // Get the response text first for better error reporting + let response_text = response.text().await?; + + // Try to parse the JSON with better error handling + let releases: Vec = match serde_json::from_str(&response_text) { + Ok(releases) => releases, + Err(e) => { + eprintln!("Failed to parse GitHub API response for Mullvad releases:"); + eprintln!("Error: {e}"); + eprintln!( + "Response text (first 500 chars): {}", + if response_text.len() > 500 { + &response_text[..500] + } else { + &response_text + } + ); + return Err(format!("Failed to parse GitHub API response: {e}").into()); + } + }; + let mut releases: Vec = releases .into_iter() .map(|mut release| { @@ -683,15 +707,39 @@ impl ApiClient { "{}/repos/zen-browser/desktop/releases?per_page=100", self.github_api_base ); - let mut releases = self + + let response = self .client .get(url) .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36") .send() - .await? - .json::>() .await?; + if !response.status().is_success() { + return Err(format!("GitHub API returned status: {}", response.status()).into()); + } + + // Get the response text first for better error reporting + let response_text = response.text().await?; + + // Try to parse the JSON with better error handling + let mut releases: Vec = match serde_json::from_str(&response_text) { + Ok(releases) => releases, + Err(e) => { + eprintln!("Failed to parse GitHub API response for Zen releases:"); + eprintln!("Error: {e}"); + eprintln!( + "Response text (first 500 chars): {}", + if response_text.len() > 500 { + &response_text[..500] + } else { + &response_text + } + ); + return Err(format!("Failed to parse GitHub API response: {e}").into()); + } + }; + // Check for twilight updates and mark alpha releases for release in &mut releases { // Use browser-specific alpha detection for Zen Browser - only "twilight" is nightly @@ -740,15 +788,39 @@ impl ApiClient { "{}/repos/brave/brave-browser/releases?per_page=100", self.github_api_base ); - let releases = self + + let response = self .client .get(url) .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36") .send() - .await? - .json::>() .await?; + if !response.status().is_success() { + return Err(format!("GitHub API returned status: {}", response.status()).into()); + } + + // Get the response text first for better error reporting + let response_text = response.text().await?; + + // Try to parse the JSON with better error handling + let releases: Vec = match serde_json::from_str(&response_text) { + Ok(releases) => releases, + Err(e) => { + eprintln!("Failed to parse GitHub API response for Brave releases:"); + eprintln!("Error: {e}"); + eprintln!( + "Response text (first 500 chars): {}", + if response_text.len() > 500 { + &response_text[..500] + } else { + &response_text + } + ); + return Err(format!("Failed to parse GitHub API response: {e}").into()); + } + }; + // Get platform info to filter appropriate releases let (os, arch) = Self::get_platform_info(); diff --git a/src-tauri/src/auto_updater.rs b/src-tauri/src/auto_updater.rs index 0af246c..aec4bc4 100644 --- a/src-tauri/src/auto_updater.rs +++ b/src-tauri/src/auto_updater.rs @@ -348,16 +348,9 @@ impl AutoUpdater { state.auto_update_downloads.remove(&download_key); self.save_auto_update_state(&state)?; - // Check if auto-delete of unused binaries is enabled and perform cleanup - let settings = self - .settings_manager - .load_settings() - .map_err(|e| format!("Failed to load settings: {e}"))?; - if settings.auto_delete_unused_binaries { - // Perform cleanup in the background - 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}"); - } + // 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) diff --git a/src-tauri/src/browser.rs b/src-tauri/src/browser.rs index 65692e1..3182a92 100644 --- a/src-tauri/src/browser.rs +++ b/src-tauri/src/browser.rs @@ -720,6 +720,24 @@ pub struct GithubRelease { pub is_nightly: bool, #[serde(default)] pub prerelease: bool, + #[serde(default)] + pub draft: bool, + #[serde(default)] + pub body: Option, + #[serde(default)] + pub html_url: Option, + #[serde(default)] + pub id: Option, + #[serde(default)] + pub node_id: Option, + #[serde(default)] + pub target_commitish: Option, + #[serde(default)] + pub created_at: Option, + #[serde(default)] + pub tarball_url: Option, + #[serde(default)] + pub zipball_url: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -728,6 +746,22 @@ pub struct GithubAsset { pub browser_download_url: String, #[serde(default)] pub size: u64, + #[serde(default)] + pub download_count: Option, + #[serde(default)] + pub id: Option, + #[serde(default)] + pub node_id: Option, + #[serde(default)] + pub label: Option, + #[serde(default)] + pub content_type: Option, + #[serde(default)] + pub state: Option, + #[serde(default)] + pub created_at: Option, + #[serde(default)] + pub updated_at: Option, } #[cfg(test)] diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index 7a20034..aab5ab3 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -1058,6 +1058,8 @@ impl BrowserRunner { release_type: &str, proxy: Option, ) -> Result> { + println!("Attempting to create profile: {name}"); + // Check if a profile with this name already exists (case insensitive) let existing_profiles = self.list_profiles()?; if existing_profiles @@ -1068,10 +1070,16 @@ impl BrowserRunner { } let snake_case_name = name.to_lowercase().replace(" ", "_"); + let profiles_dir = self.get_profiles_dir(); + let profile_file = profiles_dir.join(format!("{snake_case_name}.json")); + let profile_path = profiles_dir.join(&snake_case_name); + + // Double-check that the profile file doesn't exist + if profile_file.exists() { + return Err(format!("Profile file for '{name}' already exists").into()); + } // Create profile directory - let mut profile_path = self.get_profiles_dir(); - profile_path.push(&snake_case_name); create_dir_all(&profile_path)?; let profile = BrowserProfile { @@ -1088,6 +1096,13 @@ impl BrowserRunner { // Save profile info self.save_profile(&profile)?; + // Verify the profile was saved correctly + if !profile_file.exists() { + return Err(format!("Failed to create profile file for '{name}'").into()); + } + + println!("Profile '{name}' created successfully"); + // Create user.js with common Firefox preferences and apply proxy settings if provided if let Some(proxy_settings) = &proxy { self.apply_proxy_settings_to_profile(&profile_path, proxy_settings, None)?; @@ -1264,14 +1279,8 @@ impl BrowserRunner { // Save the updated profile self.save_profile(&profile)?; - // Check if auto-delete of unused binaries is enabled - let settings_manager = crate::settings_manager::SettingsManager::new(); - if let Ok(settings) = settings_manager.load_settings() { - if settings.auto_delete_unused_binaries { - // Perform cleanup in the background - let _ = self.cleanup_unused_binaries_internal(); - } - } + // Always perform cleanup after profile version update to remove unused binaries + let _ = self.cleanup_unused_binaries_internal(); Ok(profile) } @@ -1911,33 +1920,53 @@ impl BrowserRunner { } pub fn delete_profile(&self, profile_name: &str) -> Result<(), Box> { - let profiles_dir = self.get_profiles_dir(); - let profile_file = profiles_dir.join(format!( - "{}.json", - profile_name.to_lowercase().replace(" ", "_") - )); - let profile_path = profiles_dir.join(profile_name.to_lowercase().replace(" ", "_")); + println!("Attempting to delete profile: {profile_name}"); - // Delete profile directory + let profiles_dir = self.get_profiles_dir(); + let snake_case_name = profile_name.to_lowercase().replace(" ", "_"); + let profile_file = profiles_dir.join(format!("{snake_case_name}.json")); + let profile_path = profiles_dir.join(&snake_case_name); + + // Verify the profile exists before attempting to delete + if !profile_file.exists() { + return Err(format!("Profile '{profile_name}' not found").into()); + } + + // Read the profile to check if browser is running + if let Ok(content) = fs::read_to_string(&profile_file) { + if let Ok(profile) = serde_json::from_str::(&content) { + if profile.process_id.is_some() { + return Err( + "Cannot delete profile while browser is running. Please stop the browser first.".into(), + ); + } + } + } + + // Delete profile directory first (if it exists) if profile_path.exists() { - fs::remove_dir_all(profile_path)? + println!("Deleting profile directory: {}", profile_path.display()); + fs::remove_dir_all(&profile_path)?; + println!("Profile directory deleted successfully"); } // Delete profile JSON file if profile_file.exists() { - fs::remove_file(profile_file)? + println!("Deleting profile file: {}", profile_file.display()); + fs::remove_file(&profile_file)?; + println!("Profile file deleted successfully"); } - // Check if auto-delete of unused binaries is enabled - let settings_manager = crate::settings_manager::SettingsManager::new(); - if let Ok(settings) = settings_manager.load_settings() { - if settings.auto_delete_unused_binaries { - // Perform cleanup in the background after profile deletion - // Ignore errors since this is not critical for profile deletion - if let Err(e) = self.cleanup_unused_binaries_internal() { - println!("Warning: Failed to cleanup unused binaries: {e}"); - } - } + // Verify deletion was successful + if profile_file.exists() || profile_path.exists() { + return Err(format!("Failed to completely delete profile '{profile_name}'").into()); + } + + println!("Profile '{profile_name}' deleted successfully"); + + // 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}"); } Ok(()) @@ -2229,6 +2258,292 @@ impl BrowserRunner { Ok(()) } + + /// 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) + } + + /// 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(mut registry) = DownloadedBrowsersRegistry::load() { + 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(", ") + ); + } + } + } + + 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}"); + } + } + } + + Ok(downloaded) + } + + pub async fn download_browser_impl( + &self, + app_handle: tauri::AppHandle, + browser_str: String, + version: String, + ) -> Result> { + let browser_type = + BrowserType::from_str(&browser_str).map_err(|e| format!("Invalid browser type: {e}"))?; + let browser = create_browser(browser_type.clone()); + + // Load registry and check if already downloaded + let mut registry = DownloadedBrowsersRegistry::load() + .map_err(|e| format!("Failed to load browser registry: {e}"))?; + + // 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 = BrowserVersionService::new(); + + 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_type.as_str()); + browser_dir.push(&version); + + // Clean up any failed previous download + if let Err(e) = registry.cleanup_failed_download(&browser_str, &version) { + println!("Warning: Failed to cleanup previous download: {e}"); + } + + create_dir_all(&browser_dir).map_err(|e| format!("Failed to create browser directory: {e}"))?; + + // Mark download as started in registry + registry.mark_download_started(&browser_str, &version, browser_dir.clone()); + registry + .save() + .map_err(|e| format!("Failed to save registry: {e}"))?; + + // Use the new download module + let downloader = Downloader::new(); + let download_path = match downloader + .download_browser( + &app_handle, + browser_type.clone(), + &version, + &download_info, + &browser_dir, + ) + .await + { + Ok(path) => path, + Err(e) => { + // Clean up failed download + let _ = registry.cleanup_failed_download(&browser_str, &version); + let _ = registry.save(); + return Err(format!("Failed to download browser: {e}").into()); + } + }; + + // Use the new extraction module + if download_info.is_archive { + let extractor = Extractor::new(); + match extractor + .extract_browser( + &app_handle, + browser_type.clone(), + &version, + &download_path, + &browser_dir, + ) + .await + { + Ok(_) => { + // Clean up the downloaded archive + if let Err(e) = std::fs::remove_file(&download_path) { + println!("Warning: Could not delete archive file: {e}"); + } + } + Err(e) => { + // Clean up failed download + let _ = registry.cleanup_failed_download(&browser_str, &version); + let _ = registry.save(); + 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) { + let _ = registry.cleanup_failed_download(&browser_str, &version); + let _ = registry.save(); + return Err("Browser download completed but verification failed".into()); + } + + // Mark download as completed in registry + let actual_version = if browser_str == "chromium" { + Some(version.clone()) + } else { + None + }; + + registry + .mark_download_completed_with_actual_version(&browser_str, &version, actual_version) + .map_err(|e| format!("Failed to mark download as completed: {e}"))?; + registry + .save() + .map_err(|e| format!("Failed to save registry: {e}"))?; + + // 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); + + Ok(version) + } + + /// 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 { + if let Ok(mut registry) = DownloadedBrowsersRegistry::load() { + 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 + } } #[tauri::command] @@ -2493,174 +2808,16 @@ pub async fn download_browser( version: String, ) -> Result { let browser_runner = BrowserRunner::new(); - let browser_type = - BrowserType::from_str(&browser_str).map_err(|e| format!("Invalid browser type: {e}"))?; - let browser = create_browser(browser_type.clone()); - - // Load registry and check if already downloaded - let mut registry = DownloadedBrowsersRegistry::load() - .map_err(|e| format!("Failed to load browser registry: {e}"))?; - - if registry.is_browser_downloaded(&browser_str, &version) { - return Ok(version); - } - - // Check if browser is supported on current platform before attempting download - let version_service = BrowserVersionService::new(); - - 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(", ") - )); - } - - 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 = browser_runner.get_binaries_dir(); - browser_dir.push(browser_type.as_str()); - browser_dir.push(&version); - - // Clean up any failed previous download - if let Err(e) = registry.cleanup_failed_download(&browser_str, &version) { - println!("Warning: Failed to cleanup previous download: {e}"); - } - - create_dir_all(&browser_dir).map_err(|e| format!("Failed to create browser directory: {e}"))?; - - // Mark download as started in registry - registry.mark_download_started(&browser_str, &version, browser_dir.clone()); - registry - .save() - .map_err(|e| format!("Failed to save registry: {e}"))?; - - // Use the new download module - let downloader = Downloader::new(); - let download_path = match downloader - .download_browser( - &app_handle, - browser_type.clone(), - &version, - &download_info, - &browser_dir, - ) + browser_runner + .download_browser_impl(app_handle, browser_str, version) .await - { - Ok(path) => path, - Err(e) => { - // Clean up failed download - let _ = registry.cleanup_failed_download(&browser_str, &version); - let _ = registry.save(); - return Err(format!("Failed to download browser: {e}")); - } - }; - - // Use the new extraction module - if download_info.is_archive { - let extractor = Extractor::new(); - match extractor - .extract_browser( - &app_handle, - browser_type.clone(), - &version, - &download_path, - &browser_dir, - ) - .await - { - Ok(_) => { - // Clean up the downloaded archive - if let Err(e) = std::fs::remove_file(&download_path) { - println!("Warning: Could not delete archive file: {e}"); - } - } - Err(e) => { - // Clean up failed download - let _ = registry.cleanup_failed_download(&browser_str, &version); - let _ = registry.save(); - return Err(format!("Failed to extract browser: {e}")); - } - } - - // 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 = browser_runner.get_binaries_dir(); - if !browser.is_version_downloaded(&version, &binaries_dir) { - let _ = registry.cleanup_failed_download(&browser_str, &version); - let _ = registry.save(); - return Err("Browser download completed but verification failed".to_string()); - } - - // Mark download as completed in registry - let actual_version = if browser_str == "chromium" { - Some(version.clone()) - } else { - None - }; - - registry - .mark_download_completed_with_actual_version(&browser_str, &version, actual_version) - .map_err(|e| format!("Failed to mark download as completed: {e}"))?; - registry - .save() - .map_err(|e| format!("Failed to save registry: {e}"))?; - - // 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); - - Ok(version) + .map_err(|e| format!("Failed to download browser: {e}")) } #[tauri::command] pub fn is_browser_downloaded(browser_str: String, version: String) -> bool { - if let Ok(registry) = DownloadedBrowsersRegistry::load() { - if registry.is_browser_downloaded(&browser_str, &version) { - return true; - } - } - let browser_type = BrowserType::from_str(&browser_str).expect("Invalid browser type"); let browser_runner = BrowserRunner::new(); - let browser = create_browser(browser_type.clone()); - let binaries_dir = browser_runner.get_binaries_dir(); - browser.is_version_downloaded(&version, &binaries_dir) + browser_runner.is_browser_downloaded(&browser_str, &version) } #[tauri::command] @@ -2726,7 +2883,27 @@ pub async fn get_browser_release_types( service .get_browser_release_types(&browser_str) .await - .map_err(|e| format!("Failed to get browser release types: {e}")) + .map_err(|e| format!("Failed to get release types: {e}")) +} + +#[tauri::command] +pub async fn check_missing_binaries() -> Result, String> { + let browser_runner = BrowserRunner::new(); + browser_runner + .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 browser_runner = BrowserRunner::new(); + browser_runner + .ensure_all_binaries_exist(&app_handle) + .await + .map_err(|e| format!("Failed to ensure all binaries exist: {e}")) } #[cfg(test)] diff --git a/src-tauri/src/download.rs b/src-tauri/src/download.rs index 867b25b..6bdca10 100644 --- a/src-tauri/src/download.rs +++ b/src-tauri/src/download.rs @@ -79,15 +79,29 @@ impl Downloader { } BrowserType::Zen => { // For Zen, verify the asset exists and handle different naming patterns - let releases = self - .api_client - .fetch_zen_releases_with_caching(true) - .await?; + let releases = match self.api_client.fetch_zen_releases_with_caching(true).await { + Ok(releases) => releases, + Err(e) => { + eprintln!("Failed to fetch Zen releases: {e}"); + return Err(format!("Failed to fetch Zen releases from GitHub API: {e}. This might be due to GitHub API rate limiting or network issues. Please try again later.").into()); + } + }; let release = releases .iter() .find(|r| r.tag_name == version) - .ok_or(format!("Zen version {version} not found"))?; + .ok_or_else(|| { + format!( + "Zen version {} not found. Available versions: {}", + version, + releases + .iter() + .take(5) + .map(|r| r.tag_name.as_str()) + .collect::>() + .join(", ") + ) + })?; // Get platform and architecture info let (os, arch) = Self::get_platform_info(); @@ -95,9 +109,17 @@ impl Downloader { // Find the appropriate asset let asset_url = self .find_zen_asset(&release.assets, &os, &arch) - .ok_or(format!( - "No compatible asset found for Zen version {version} on {os}/{arch}" - ))?; + .ok_or_else(|| { + let available_assets: Vec<&str> = + release.assets.iter().map(|a| a.name.as_str()).collect(); + format!( + "No compatible asset found for Zen version {} on {}/{}. Available assets: {}", + version, + os, + arch, + available_assets.join(", ") + ) + })?; Ok(asset_url) } diff --git a/src-tauri/src/downloaded_browsers.rs b/src-tauri/src/downloaded_browsers.rs index 2e3b67b..837bbe8 100644 --- a/src-tauri/src/downloaded_browsers.rs +++ b/src-tauri/src/downloaded_browsers.rs @@ -189,8 +189,22 @@ impl DownloadedBrowsersRegistry { let mut to_remove = Vec::new(); for (browser, versions) in &self.browsers { for (version, info) in versions { + // Only remove verified downloads that are not used by any active profile if info.verified && !active_set.contains(&(browser.clone(), version.clone())) { - to_remove.push((browser.clone(), version.clone())); + // Double-check that this browser+version is truly not in use + // by looking for exact matches in the active profiles + let is_in_use = active_profiles + .iter() + .any(|(active_browser, active_version)| { + active_browser == browser && active_version == version + }); + + if !is_in_use { + to_remove.push((browser.clone(), version.clone())); + println!("Marking for removal: {browser} {version} (not used by any profile)"); + } else { + println!("Keeping: {browser} {version} (in use by profile)"); + } } } } @@ -201,9 +215,16 @@ impl DownloadedBrowsersRegistry { eprintln!("Failed to cleanup unused binary {browser}:{version}: {e}"); } else { cleaned_up.push(format!("{browser} {version}")); + println!("Successfully removed unused binary: {browser} {version}"); } } + if cleaned_up.is_empty() { + println!("No unused binaries found to clean up"); + } else { + println!("Cleaned up {} unused binaries", cleaned_up.len()); + } + Ok(cleaned_up) } @@ -217,6 +238,45 @@ impl DownloadedBrowsersRegistry { .map(|profile| (profile.browser.clone(), profile.version.clone())) .collect() } + + /// Verify that all registered browsers actually exist on disk and clean up stale entries + pub fn verify_and_cleanup_stale_entries( + &mut 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 browsers_to_check: Vec<(String, String)> = self + .browsers + .iter() + .flat_map(|(browser, versions)| { + versions + .keys() + .map(|version| (browser.clone(), version.clone())) + }) + .collect(); + + for (browser_str, version) in browsers_to_check { + if let Ok(browser_type) = BrowserType::from_str(&browser_str) { + let browser = create_browser(browser_type); + if !browser.is_version_downloaded(&version, &binaries_dir) { + // Files don't exist, remove from registry + if let Some(_removed) = self.remove_browser(&browser_str, &version) { + cleaned_up.push(format!("{browser_str} {version}")); + println!("Removed stale registry entry for {browser_str} {version}"); + } + } + } + } + + if !cleaned_up.is_empty() { + self.save()?; + } + + Ok(cleaned_up) + } } #[cfg(test)] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d1e82db..a638fff 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -26,12 +26,12 @@ mod version_updater; extern crate lazy_static; use browser_runner::{ - check_browser_exists, check_browser_status, create_browser_profile_new, delete_profile, - download_browser, fetch_browser_versions_cached_first, fetch_browser_versions_with_count, - fetch_browser_versions_with_count_cached_first, get_browser_release_types, - get_downloaded_browser_versions, get_supported_browsers, is_browser_supported_on_platform, - kill_browser_profile, launch_browser_profile, list_browser_profiles, rename_profile, - update_profile_proxy, update_profile_version, + check_browser_exists, check_browser_status, check_missing_binaries, 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_browser_release_types, get_downloaded_browser_versions, get_supported_browsers, + is_browser_supported_on_platform, kill_browser_profile, launch_browser_profile, + list_browser_profiles, rename_profile, update_profile_proxy, update_profile_version, }; use settings_manager::{ @@ -374,6 +374,8 @@ pub fn run() { get_system_theme, detect_existing_profiles, import_browser_profile, + check_missing_binaries, + ensure_all_binaries_exist, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/settings_manager.rs b/src-tauri/src/settings_manager.rs index 647fe08..28800c8 100644 --- a/src-tauri/src/settings_manager.rs +++ b/src-tauri/src/settings_manager.rs @@ -29,8 +29,6 @@ pub struct AppSettings { pub show_settings_on_startup: bool, #[serde(default = "default_theme")] pub theme: String, // "light", "dark", or "system" - #[serde(default)] - pub auto_delete_unused_binaries: bool, } fn default_theme() -> String { @@ -43,7 +41,6 @@ impl Default for AppSettings { set_as_default_browser: false, show_settings_on_startup: true, theme: "system".to_string(), - auto_delete_unused_binaries: true, } } } diff --git a/src/app/page.tsx b/src/app/page.tsx index c1bd0a6..926a421 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -76,12 +76,59 @@ export default function Home() { "list_browser_profiles", ); setProfiles(profileList); + + // Check for missing binaries after loading profiles + await checkMissingBinaries(); } catch (err: unknown) { console.error("Failed to load profiles:", err); setError(`Failed to load profiles: ${JSON.stringify(err)}`); } }, []); + // Check for missing binaries and offer to download them + const checkMissingBinaries = useCallback(async () => { + try { + const missingBinaries = await invoke<[string, string, string][]>( + "check_missing_binaries", + ); + + if (missingBinaries.length > 0) { + console.log("Found missing binaries:", missingBinaries); + + // Show a toast notification about missing binaries and auto-download them + const missingList = missingBinaries + .map( + ([profileName, browser, version]) => + `${browser} ${version} (for ${profileName})`, + ) + .join(", "); + + console.log(`Downloading missing binaries: ${missingList}`); + + try { + const downloaded = await invoke( + "ensure_all_binaries_exist", + ); + if (downloaded.length > 0) { + console.log( + "Successfully downloaded missing binaries:", + downloaded, + ); + } + } catch (downloadError) { + console.error("Failed to download missing binaries:", downloadError); + setError( + `Failed to download missing binaries: ${JSON.stringify( + downloadError, + )}`, + ); + } + } + } catch (err: unknown) { + console.error("Failed to check missing binaries:", err); + } + }, []); + // Version updater for handling version fetching progress events and auto-updates useVersionUpdater(); @@ -99,11 +146,12 @@ export default function Home() { // Check for updates after loading profiles await checkForUpdates(); + await checkMissingBinaries(); } catch (err: unknown) { console.error("Failed to load profiles:", err); setError(`Failed to load profiles: ${JSON.stringify(err)}`); } - }, [checkForUpdates]); + }, [checkForUpdates, checkMissingBinaries]); useAppUpdateNotifications(); @@ -439,12 +487,36 @@ export default function Home() { const handleDeleteProfile = useCallback( async (profile: BrowserProfile) => { setError(null); + console.log("Attempting to delete profile:", profile.name); + try { + // First check if the browser is running for this profile + const isRunning = await invoke("check_browser_status", { + profile, + }); + + if (isRunning) { + setError( + "Cannot delete profile while browser is running. Please stop the browser first.", + ); + return; + } + + // Attempt to delete the profile await invoke("delete_profile", { profileName: profile.name }); + console.log("Profile deletion command completed successfully"); + + // Give a small delay to ensure file system operations complete + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Reload profiles to ensure UI is updated await loadProfiles(); + + console.log("Profile deleted and profiles reloaded successfully"); } catch (err: unknown) { console.error("Failed to delete profile:", err); - setError(`Failed to delete profile: ${JSON.stringify(err)}`); + const errorMessage = err instanceof Error ? err.message : String(err); + setError(`Failed to delete profile: ${errorMessage}`); } }, [loadProfiles], diff --git a/src/components/release-type-selector.tsx b/src/components/release-type-selector.tsx index 7492228..56990ed 100644 --- a/src/components/release-type-selector.tsx +++ b/src/components/release-type-selector.tsx @@ -55,6 +55,16 @@ export function ReleaseTypeSelector({ : []), ]; + // Only show dropdown if there are multiple release types available + const showDropdown = releaseOptions.length > 1; + + // If only one release type is available, auto-select it + if (!showDropdown && releaseOptions.length === 1 && !selectedReleaseType) { + setTimeout(() => { + onReleaseTypeSelect(releaseOptions[0].type); + }, 0); + } + const selectedDisplayText = selectedReleaseType ? selectedReleaseType === "stable" ? "Stable" @@ -73,75 +83,99 @@ export function ReleaseTypeSelector({ return (
- - - - - - - No release types available. - - - {releaseOptions.map((option) => { - const isDownloaded = downloadedVersions.includes( - option.version, - ); - return ( - { - const selectedType = currentValue as - | "stable" - | "nightly"; - onReleaseTypeSelect( - selectedType === selectedReleaseType - ? null - : selectedType, - ); - setPopoverOpen(false); - }} - > - -
- {option.type} - {option.type === "nightly" && ( - - Nightly + {showDropdown ? ( + + + + + + + No release types available. + + + {releaseOptions.map((option) => { + const isDownloaded = downloadedVersions.includes( + option.version, + ); + return ( + { + const selectedType = currentValue as + | "stable" + | "nightly"; + onReleaseTypeSelect( + selectedType === selectedReleaseType + ? null + : selectedType, + ); + setPopoverOpen(false); + }} + > + +
+ {option.type} + {option.type === "nightly" && ( + + Nightly + + )} + + {option.version} - )} - - {option.version} - - {isDownloaded && ( - - Downloaded - - )} -
-
- ); - })} -
-
-
-
-
+ {isDownloaded && ( + + Downloaded + + )} +
+
+ ); + })} +
+
+
+
+
+ ) : ( + // Show a simple display when only one release type is available + releaseOptions.length === 1 && ( +
+ + {releaseOptions[0].type} + + {releaseOptions[0].type === "nightly" && ( + + Nightly + + )} + + {releaseOptions[0].version} + + {downloadedVersions.includes(releaseOptions[0].version) && ( + + Downloaded + + )} +
+ ) + )} {showDownloadButton && selectedReleaseType && diff --git a/src/components/settings-dialog.tsx b/src/components/settings-dialog.tsx index 0df63d4..32d5c39 100644 --- a/src/components/settings-dialog.tsx +++ b/src/components/settings-dialog.tsx @@ -31,7 +31,6 @@ interface AppSettings { set_as_default_browser: boolean; show_settings_on_startup: boolean; theme: string; - auto_delete_unused_binaries: boolean; } interface PermissionInfo { @@ -50,13 +49,11 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) { set_as_default_browser: false, show_settings_on_startup: true, theme: "system", - auto_delete_unused_binaries: true, }); const [originalSettings, setOriginalSettings] = useState({ set_as_default_browser: false, show_settings_on_startup: true, theme: "system", - auto_delete_unused_binaries: true, }); const [isDefaultBrowser, setIsDefaultBrowser] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -287,9 +284,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) { const hasChanges = settings.show_settings_on_startup !== originalSettings.show_settings_on_startup || - settings.theme !== originalSettings.theme || - settings.auto_delete_unused_binaries !== - originalSettings.auto_delete_unused_binaries; + settings.theme !== originalSettings.theme; return ( @@ -358,33 +353,6 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {

- {/* Auto-Update Section */} -
- - -
- { - updateSetting( - "auto_delete_unused_binaries", - checked as boolean, - ); - }} - /> - -
- -

- When enabled, Donut Browser will check for browser updates and - notify you when updates are available for your profiles. Unused - binaries will be automatically deleted to save disk space. -

-
- {/* Startup Behavior Section */}