Files
donutbrowser/src-tauri/src/downloaded_browsers_registry.rs
T
2026-05-15 20:42:25 +04:00

1350 lines
43 KiB
Rust

use serde::{Deserialize, Serialize};
use std::collections::HashMap;
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,
pub version: String,
pub file_path: PathBuf,
}
#[derive(Debug, Serialize, Deserialize, Default)]
struct RegistryData {
pub browsers: HashMap<String, HashMap<String, DownloadedBrowserInfo>>, // browser -> version -> info
}
pub struct DownloadedBrowsersRegistry {
data: Mutex<RegistryData>,
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(),
}
}
pub fn instance() -> &'static DownloadedBrowsersRegistry {
&DOWNLOADED_BROWSERS_REGISTRY
}
pub fn load(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let registry_path = Self::get_registry_path()?;
if !registry_path.exists() {
return Ok(());
}
let content = fs::read_to_string(&registry_path)?;
let registry_data: RegistryData = serde_json::from_str(&content)?;
let mut data = self.data.lock().unwrap();
*data = registry_data;
Ok(())
}
pub fn save(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let registry_path = Self::get_registry_path()?;
// Ensure parent directory exists
if let Some(parent) = registry_path.parent() {
fs::create_dir_all(parent)?;
}
let data = self.data.lock().unwrap();
let content = serde_json::to_string_pretty(&*data)?;
fs::write(&registry_path, content)?;
Ok(())
}
fn get_registry_path() -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
Ok(crate::app_dirs::data_subdir().join("downloaded_browsers.json"))
}
pub fn add_browser(&self, info: DownloadedBrowserInfo) {
let mut data = self.data.lock().unwrap();
data
.browsers
.entry(info.browser.clone())
.or_default()
.insert(info.version.clone(), info);
}
pub fn remove_browser(&self, browser: &str, version: &str) -> Option<DownloadedBrowserInfo> {
let mut data = self.data.lock().unwrap();
data.browsers.get_mut(browser)?.remove(version)
}
/// 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
.get(browser)
.and_then(|versions| versions.get(version))
.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(_) => {
log::info!("Invalid browser type: {browser}");
return false;
}
};
let browser_instance = create_browser(browser_type.clone());
let binaries_dir = crate::app_dirs::binaries_dir();
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 {
log::info!("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<String> {
let data = self.data.lock().unwrap();
data
.browsers
.get(browser)
.map(|versions| versions.keys().cloned().collect())
.unwrap_or_default()
}
pub fn mark_download_started(&self, browser: &str, version: &str, file_path: PathBuf) {
// Only mark download started, don't add to registry yet
// The browser will be added to registry only after verification succeeds
log::info!(
"Marking download started for {}:{} at {}",
browser,
version,
file_path.display()
);
}
pub fn mark_download_completed(
&self,
browser: &str,
version: &str,
file_path: PathBuf,
) -> Result<(), String> {
// Only mark as completed after verification succeeds
let info = DownloadedBrowserInfo {
browser: browser.to_string(),
version: version.to_string(),
file_path,
};
self.add_browser(info);
log::info!("Browser {browser}:{version} successfully added to registry after verification");
Ok(())
}
pub fn cleanup_failed_download(
&self,
browser: &str,
version: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if let Some(info) = self.remove_browser(browser, version) {
// Clean up extracted binaries but preserve downloaded archives
if info.file_path.exists() {
if info.file_path.is_dir() {
// Allowed archive extensions to preserve
let archive_exts = [
"zip", "dmg", "tar.xz", "tar.gz", "tar.bz2", "AppImage", "exe", "pkg", "msi",
];
for entry in fs::read_dir(&info.file_path)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
fs::remove_dir_all(&path)?;
continue;
}
// For files, preserve if they look like downloaded archives/installers
let keep = path
.file_name()
.and_then(|n| n.to_str())
.map(|name| {
// Match suffixes (handles multi-part extensions like .tar.xz)
archive_exts
.iter()
.any(|ext| name.to_lowercase().ends_with(&ext.to_lowercase()))
})
.unwrap_or(false);
if !keep {
fs::remove_file(&path)?;
}
}
} else {
// It's a file. If it's not an archive, remove it; otherwise preserve it.
let file_name = info
.file_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
let archive_exts = [
"zip", "dmg", "tar.xz", "tar.gz", "tar.bz2", "AppImage", "exe", "pkg", "msi",
];
let is_archive = archive_exts
.iter()
.any(|ext| file_name.to_lowercase().ends_with(&ext.to_lowercase()));
if !is_archive {
fs::remove_file(&info.file_path)?;
}
}
}
}
Ok(())
}
/// Find and remove unused browser binaries that are not referenced by any active profiles
fn cleanup_unused_binaries_internal(
&self,
active_profiles: &[(String, String)], // (browser, version) pairs
running_profiles: &[(String, String)], // (browser, version) pairs for running profiles
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
let active_set: std::collections::HashSet<(String, String)> =
active_profiles.iter().cloned().collect();
let running_set: std::collections::HashSet<(String, String)> =
running_profiles.iter().cloned().collect();
let mut cleaned_up = Vec::new();
// Get pending update versions from auto updater
let pending_updates = match self.auto_updater.get_pending_update_versions() {
Ok(updates) => updates,
Err(e) => {
log::warn!("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();
{
let data = self.data.lock().unwrap();
for (browser, versions) in &data.browsers {
for version in versions.keys() {
let browser_version = (browser.clone(), version.clone());
// Don't remove if it's used by any active profile
if active_set.contains(&browser_version) {
log::info!("Keeping: {browser} {version} (in use by profile)");
continue;
}
// Don't remove if it's currently running (even if not in active profiles)
if running_set.contains(&browser_version) {
log::info!("Keeping: {browser} {version} (currently running)");
continue;
}
// Don't remove if this version has a pending update for a running profile
// This handles the case where a running profile has an update downloaded but not yet applied
if pending_updates.contains(&browser_version) {
// Check if there are any running profiles for this browser that could be updated
let has_running_profile_for_browser =
running_profiles.iter().any(|(b, _)| b == browser);
if has_running_profile_for_browser {
log::info!("Keeping: {browser} {version} (pending update for running profile)");
continue;
}
}
// Mark for removal
to_remove.push(browser_version);
log::info!("Marking for removal: {browser} {version} (not used by any profile)");
}
}
}
// For each browser where every registered version would be removed (no
// profile uses any), keep the newest one by semver. Without this, the
// version preserved depends on HashMap iteration order, so a freshly
// downloaded version can be deleted in favor of an older orphan — leaving
// the UI stuck on "needs to be downloaded".
{
let data = self.data.lock().unwrap();
let mut removal_versions_by_browser: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
for (browser, version) in &to_remove {
removal_versions_by_browser
.entry(browser.clone())
.or_default()
.push(version.clone());
}
let mut keep_per_browser: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
for (browser, versions) in &removal_versions_by_browser {
let total = data
.browsers
.get(browser.as_str())
.map(|v| v.len())
.unwrap_or(0);
if versions.len() >= total {
if let Some(latest) = versions
.iter()
.max_by(|a, b| crate::api_client::compare_versions(a, b))
{
keep_per_browser.insert(browser.clone(), latest.clone());
}
}
}
drop(data);
to_remove.retain(|(browser, version)| {
if keep_per_browser
.get(browser)
.is_some_and(|keep| keep == version)
{
log::info!("Keeping latest available version: {browser} {version}");
return false;
}
true
});
}
// Remove unused binaries and their version folders
for (browser, version) in to_remove {
if let Err(e) = self.cleanup_failed_download(&browser, &version) {
log::error!("Failed to cleanup unused binary {browser}:{version}: {e}");
} else {
// After removing the binary, also remove the empty version folder
if let Err(e) = self.remove_empty_version_folder(&browser, &version) {
log::error!("Failed to remove empty version folder for {browser}:{version}: {e}");
}
cleaned_up.push(format!("{browser} {version}"));
log::info!("Successfully removed unused binary: {browser} {version}");
}
}
if cleaned_up.is_empty() {
log::info!("No unused binaries found to clean up");
} else {
log::info!("Cleaned up {} unused binaries", cleaned_up.len());
}
Ok(cleaned_up)
}
/// Get all browsers and versions referenced by active profiles
pub fn get_active_browser_versions(
&self,
profiles: &[crate::profile::BrowserProfile],
) -> Vec<(String, String)> {
profiles
.iter()
.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(
&self,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
use crate::browser::{create_browser, BrowserType};
let mut cleaned_up = Vec::new();
let binaries_dir = self.profile_manager.get_binaries_dir();
let browsers_to_check: Vec<(String, String)> = {
let data = self.data.lock().unwrap();
data
.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}"));
log::info!("Removed stale registry entry for {browser_str} {version}");
}
}
}
}
if !cleaned_up.is_empty() {
self.save()?;
}
Ok(cleaned_up)
}
/// Get all browsers and versions that are currently running
pub fn get_running_browser_versions(
&self,
profiles: &[crate::profile::BrowserProfile],
) -> Vec<(String, String)> {
profiles
.iter()
.filter(|profile| profile.process_id.is_some())
.map(|profile| (profile.browser.clone(), profile.version.clone()))
.collect()
}
/// Scan the binaries directory and sync with registry
/// This ensures the registry reflects what's actually on disk
pub fn sync_with_binaries_directory(
&self,
binaries_dir: &std::path::Path,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
let mut changes = Vec::new();
if !binaries_dir.exists() {
return Ok(changes);
}
// Scan for actual browser directories
for browser_entry in fs::read_dir(binaries_dir)? {
let browser_entry = browser_entry?;
let browser_path = browser_entry.path();
if !browser_path.is_dir() {
continue;
}
let browser_name = browser_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
if browser_name.is_empty() || browser_name.starts_with('.') {
continue;
}
// Scan for version directories within this browser
for version_entry in fs::read_dir(&browser_path)? {
let version_entry = version_entry?;
let version_path = version_entry.path();
if !version_path.is_dir() {
continue;
}
let version_name = version_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
if version_name.is_empty() || version_name.starts_with('.') {
continue;
}
// Only add to registry if this looks like a valid installed browser, not just an archive
if !self.is_browser_downloaded(browser_name, version_name) {
if let Ok(browser_type) = crate::browser::BrowserType::from_str(browser_name) {
let browser = crate::browser::create_browser(browser_type);
if browser.is_version_downloaded(version_name, binaries_dir) {
let info = DownloadedBrowserInfo {
browser: browser_name.to_string(),
version: version_name.to_string(),
file_path: version_path.clone(),
};
self.add_browser(info);
changes.push(format!("Added {browser_name} {version_name} to registry"));
}
}
}
}
}
if !changes.is_empty() {
self.save()?;
}
Ok(changes)
}
/// Comprehensive cleanup that removes unused binaries and syncs registry
fn comprehensive_cleanup(
&self,
binaries_dir: &std::path::Path,
active_profiles: &[(String, String)],
running_profiles: &[(String, String)],
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
let mut cleanup_results = Vec::new();
// First, sync registry with actual binaries on disk
let sync_results = self.sync_with_binaries_directory(binaries_dir)?;
cleanup_results.extend(sync_results);
// Then perform the regular cleanup
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()?;
cleanup_results.extend(stale_cleanup);
// Clean up any remaining empty folders
let empty_folder_cleanup = self.cleanup_empty_folders(binaries_dir)?;
cleanup_results.extend(empty_folder_cleanup);
if !cleanup_results.is_empty() {
self.save()?;
}
Ok(cleanup_results)
}
/// Remove empty version folder after cleanup
fn remove_empty_version_folder(
&self,
browser: &str,
version: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Never remove a directory if a download is in progress for this browser/version
if crate::downloader::is_downloading(browser, version) {
return Ok(());
}
let binaries_dir = crate::app_dirs::binaries_dir();
let version_dir = binaries_dir.join(browser).join(version);
// Only remove if the directory exists and is empty
if version_dir.exists() && version_dir.is_dir() {
if let Ok(mut entries) = fs::read_dir(&version_dir) {
if entries.next().is_none() {
// Directory is empty, remove it
fs::remove_dir(&version_dir)?;
log::info!("Removed empty version folder: {}", version_dir.display());
// Also check if the browser folder is now empty and remove it too
let browser_dir = binaries_dir.join(browser);
if browser_dir.exists() && browser_dir.is_dir() {
if let Ok(mut browser_entries) = fs::read_dir(&browser_dir) {
if browser_entries.next().is_none() {
fs::remove_dir(&browser_dir)?;
log::info!("Removed empty browser folder: {}", browser_dir.display());
}
}
}
}
}
}
Ok(())
}
/// Clean up existing empty version and browser folders
pub fn cleanup_empty_folders(
&self,
binaries_dir: &std::path::Path,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
let mut cleaned_up = Vec::new();
if !binaries_dir.exists() {
return Ok(cleaned_up);
}
// Scan for browser directories
for browser_entry in fs::read_dir(binaries_dir)? {
let browser_entry = browser_entry?;
let browser_path = browser_entry.path();
if !browser_path.is_dir() {
continue;
}
let browser_name = browser_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
if browser_name.is_empty() || browser_name.starts_with('.') {
continue;
}
let mut empty_version_dirs = Vec::new();
let mut has_non_empty_versions = false;
// Scan for version directories within this browser
for version_entry in fs::read_dir(&browser_path)? {
let version_entry = version_entry?;
let version_path = version_entry.path();
if !version_path.is_dir() {
has_non_empty_versions = true; // Non-directory files count as non-empty
continue;
}
let version_name = version_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
if version_name.is_empty() || version_name.starts_with('.') {
continue;
}
// Skip if a download is in progress for this browser/version
if crate::downloader::is_downloading(browser_name, version_name) {
has_non_empty_versions = true;
continue;
}
// Check if version directory is empty
match fs::read_dir(&version_path) {
Ok(mut entries) => {
if entries.next().is_none() {
// Directory is empty
empty_version_dirs.push((version_path.clone(), version_name.to_string()));
} else {
has_non_empty_versions = true;
}
}
Err(_) => {
has_non_empty_versions = true; // Assume non-empty if we can't read
}
}
}
// Remove empty version directories
for (version_path, version_name) in empty_version_dirs {
if let Err(e) = fs::remove_dir(&version_path) {
log::error!(
"Failed to remove empty version folder {}: {e}",
version_path.display()
);
} else {
cleaned_up.push(format!(
"Removed empty version folder: {browser_name}/{version_name}"
));
log::info!("Removed empty version folder: {}", version_path.display());
}
}
// If browser directory is now empty, remove it too
if !has_non_empty_versions {
if let Ok(mut entries) = fs::read_dir(&browser_path) {
if entries.next().is_none() {
if let Err(e) = fs::remove_dir(&browser_path) {
log::error!(
"Failed to remove empty browser folder {}: {e}",
browser_path.display()
);
} else {
cleaned_up.push(format!("Removed empty browser folder: {browser_name}"));
log::info!("Removed empty browser folder: {}", browser_path.display());
}
}
}
}
}
Ok(cleaned_up)
}
/// Consolidate browser versions - keep only the latest version per browser
pub fn consolidate_browser_versions(
&self,
app_handle: &tauri::AppHandle,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
log::info!("Starting browser version consolidation...");
let profiles = self
.profile_manager
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {e}"))?;
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<String, Vec<&BrowserProfile>> =
std::collections::HashMap::new();
for profile in &profiles {
browser_profiles
.entry(profile.browser.clone())
.or_default()
.push(profile);
}
for (browser_name, browser_profiles) in browser_profiles.iter() {
// Find the latest version among all profiles for this browser that actually exists on disk
let mut available_versions: Vec<String> = Vec::new();
for profile in browser_profiles {
// Only consider versions that actually exist on disk
let browser_type = match crate::browser::BrowserType::from_str(browser_name) {
Ok(bt) => bt,
Err(_) => continue,
};
let browser = crate::browser::create_browser(browser_type.clone());
if browser.is_version_downloaded(&profile.version, &binaries_dir) {
available_versions.push(profile.version.clone());
} else {
log::info!(
"Profile '{}' references version {} that doesn't exist on disk",
profile.name,
profile.version
);
}
}
if available_versions.is_empty() {
log::info!("No available versions found for {browser_name}, skipping consolidation");
continue;
}
// Sort available versions to find the latest
available_versions.sort_by(|a, b| {
// Sort versions using semantic versioning logic
crate::api_client::compare_versions(b, a)
});
let latest_version = &available_versions[0];
log::info!("Latest available version for {browser_name}: {latest_version}");
// 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::<String>::new();
for profile in browser_profiles {
if profile.version != *latest_version {
// Only update if profile is not currently running
if profile.process_id.is_none() {
profiles_to_update.push(profile);
older_versions_to_remove.insert(profile.version.clone());
} else {
log::info!(
"Skipping version update for running profile: {} ({})",
profile.name,
profile.version
);
}
}
// Update profiles to latest version
for profile in &profiles_to_update {
match self.profile_manager.update_profile_version(
app_handle,
&profile.id.to_string(),
latest_version,
) {
Ok(_) => {
consolidated.push(format!(
"Updated profile '{}' from {} to {}",
profile.name, profile.version, latest_version
));
}
Err(e) => {
log::error!("Failed to update profile '{}': {}", profile.name, e);
}
}
}
// Remove older version binaries that are no longer needed
for old_version in &older_versions_to_remove {
log::info!("Consolidating: removing old version {browser_name} {old_version}");
match self.cleanup_failed_download(browser_name, old_version) {
Ok(_) => {
consolidated.push(format!("Removed old version: {browser_name} {old_version}"));
log::info!("Successfully removed old version: {browser_name} {old_version}");
}
Err(e) => {
log::error!("Failed to cleanup old version {browser_name} {old_version}: {e}");
}
}
}
}
}
// Save registry after consolidation
self
.save()
.map_err(|e| format!("Failed to save registry after consolidation: {e}"))?;
log::info!(
"Browser version consolidation completed: {} actions taken",
consolidated.len()
);
Ok(consolidated)
}
/// Check if browser binaries exist for all profiles and return missing binaries
pub async fn check_missing_binaries(
&self,
) -> Result<Vec<(String, String, String)>, Box<dyn std::error::Error + Send + Sync>> {
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();
for profile in profiles {
let browser_type = match BrowserType::from_str(&profile.browser) {
Ok(bt) => bt,
Err(_) => {
log::info!(
"Warning: Invalid browser type '{}' for profile '{}'",
profile.browser,
profile.name
);
continue;
}
};
let browser = create_browser(browser_type.clone());
let binaries_dir = crate::app_dirs::binaries_dir();
log::info!(
"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<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
// First, clean up any stale registry entries
if let Ok(cleaned_up) = self.verify_and_cleanup_stale_entries() {
if !cleaned_up.is_empty() {
log::info!(
"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() {
log::info!("Version consolidation results:");
for action in &consolidated {
log::info!(" {action}");
}
}
}
let missing_binaries = self.check_missing_binaries().await?;
let mut downloaded = Vec::new();
for (profile_name, browser, version) in missing_binaries {
log::info!("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}')"
));
// After successful download, update profiles that use this browser to the new version
match self
.update_profiles_to_version(app_handle, &browser, &version)
.await
{
Ok(updated_profiles) => {
if !updated_profiles.is_empty() {
log::info!(
"Successfully updated {} profiles to version {}:",
updated_profiles.len(),
version
);
for update_msg in updated_profiles {
log::info!(" {update_msg}");
}
}
}
Err(e) => {
log::error!("CRITICAL: Failed to update profiles to version {version}: {e}");
log::error!("This may cause profile version inconsistencies and cleanup issues");
}
}
}
Err(e) => {
log::error!("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()? {
log::info!("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());
log::info!("GeoIP database downloaded successfully");
}
Err(e) => {
log::error!("Failed to download GeoIP database: {e}");
// Don't fail the entire operation if GeoIP download fails
}
}
}
Ok(downloaded)
}
/// Update all profiles using a specific browser to a new version
async fn update_profiles_to_version(
&self,
app_handle: &tauri::AppHandle,
browser: &str,
version: &str,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
let profiles = self
.profile_manager
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {e}"))?;
let mut updated_profiles = Vec::new();
for profile in profiles {
if profile.browser == browser && profile.version != version {
// Check if profile is currently running
if profile.process_id.is_some() {
log::info!(
"Skipping version update for running profile: {} ({})",
profile.name,
profile.version
);
continue;
}
// Update the profile version
match self.profile_manager.update_profile_version(
app_handle,
&profile.id.to_string(),
version,
) {
Ok(_) => {
updated_profiles.push(format!(
"Updated profile '{}' from {} to {}",
profile.name, profile.version, version
));
log::info!(
"Successfully updated profile '{}' to version {}",
profile.name,
version
);
// Save registry after each profile update to ensure consistency
if let Err(e) = self.save() {
log::warn!("Warning: Failed to save registry after profile update: {e}");
}
}
Err(e) => {
log::error!("Failed to update profile '{}': {}", profile.name, e);
}
}
}
}
Ok(updated_profiles)
}
/// Cleanup unused binaries based on active and running profiles
pub fn cleanup_unused_binaries(
&self,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
// 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)
}
}
// Global singleton instance
lazy_static::lazy_static! {
static ref DOWNLOADED_BROWSERS_REGISTRY: DownloadedBrowsersRegistry = {
let registry = DownloadedBrowsersRegistry::new();
if let Err(e) = registry.load() {
log::warn!("Warning: Failed to load downloaded browsers registry: {e}");
}
registry
};
}
#[cfg(test)]
mod tests {
use super::*;
#[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());
}
#[test]
fn test_add_and_get_browser() {
let registry = DownloadedBrowsersRegistry::new();
let info = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "139.0".to_string(),
file_path: PathBuf::from("/test/path"),
};
registry.add_browser(info.clone());
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]
fn test_get_downloaded_versions() {
let registry = DownloadedBrowsersRegistry::new();
let info1 = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "139.0".to_string(),
file_path: PathBuf::from("/test/path1"),
};
let info2 = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "140.0".to_string(),
file_path: PathBuf::from("/test/path2"),
};
let info3 = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "141.0".to_string(),
file_path: PathBuf::from("/test/path3"),
};
registry.add_browser(info1);
registry.add_browser(info2);
registry.add_browser(info3);
let versions = registry.get_downloaded_versions("firefox");
assert_eq!(versions.len(), 3);
assert!(versions.contains(&"139.0".to_string()));
assert!(versions.contains(&"140.0".to_string()));
assert!(versions.contains(&"141.0".to_string()));
}
#[test]
fn test_mark_download_lifecycle() {
let registry = DownloadedBrowsersRegistry::new();
// Mark download started
registry.mark_download_started("firefox", "139.0", PathBuf::from("/test/path"));
// Should NOT be registered until verification completes
assert!(
!registry.is_browser_registered("firefox", "139.0"),
"Browser should NOT be registered after marking as started (only after verification)"
);
// Mark as completed (after verification)
registry
.mark_download_completed("firefox", "139.0", PathBuf::from("/test/path"))
.expect("Failed to mark download as completed");
// Should now be registered
assert!(
registry.is_browser_registered("firefox", "139.0"),
"Browser should be registered after verification completes"
);
}
#[test]
fn test_remove_browser() {
let registry = DownloadedBrowsersRegistry::new();
let info = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "139.0".to_string(),
file_path: PathBuf::from("/test/path"),
};
registry.add_browser(info);
assert!(
registry.is_browser_registered("firefox", "139.0"),
"Browser should be registered after adding"
);
let removed = registry.remove_browser("firefox", "139.0");
assert!(
removed.is_some(),
"Remove operation should return the removed browser info"
);
assert!(
!registry.is_browser_registered("firefox", "139.0"),
"Browser should not be registered after removal"
);
}
#[test]
fn test_twilight_download() {
let registry = DownloadedBrowsersRegistry::new();
// Mark twilight download started
registry.mark_download_started("zen", "twilight", PathBuf::from("/test/zen-twilight"));
// Should NOT be registered until verification completes
assert!(
!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"))
.expect("Failed to mark twilight download as completed");
// Now it should be registered
assert!(
registry.is_browser_registered("zen", "twilight"),
"Zen twilight version should be registered after verification completes"
);
}
#[test]
fn test_last_version_kept_during_cleanup() {
let registry = DownloadedBrowsersRegistry::new();
// Add a single version for "firefox"
registry.add_browser(DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "139.0".to_string(),
file_path: PathBuf::from("/test/firefox/139.0"),
});
// Add two versions for "chromium"
registry.add_browser(DownloadedBrowserInfo {
browser: "chromium".to_string(),
version: "120.0".to_string(),
file_path: PathBuf::from("/test/chromium/120.0"),
});
registry.add_browser(DownloadedBrowserInfo {
browser: "chromium".to_string(),
version: "121.0".to_string(),
file_path: PathBuf::from("/test/chromium/121.0"),
});
// No active or running profiles
let result = registry
.cleanup_unused_binaries_internal(&[], &[])
.expect("cleanup should succeed");
// firefox 139.0 should be kept (last version), chromium should lose one but keep one
// The exact one kept depends on iteration order, but at least one must remain
assert!(
!result.contains(&"firefox 139.0".to_string()),
"Last version of firefox should not be cleaned up"
);
// At most one chromium version should have been cleaned up
let chromium_cleaned: Vec<_> = result
.iter()
.filter(|r| r.starts_with("chromium"))
.collect();
assert!(
chromium_cleaned.len() <= 1,
"At most one chromium version should be cleaned up, got: {:?}",
chromium_cleaned
);
// Verify firefox is still registered
assert!(
registry.is_browser_registered("firefox", "139.0"),
"Last firefox version should still be registered"
);
}
#[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 async fn ensure_active_browsers_downloaded(
app_handle: tauri::AppHandle,
) -> Result<Vec<String>, String> {
let registry = DownloadedBrowsersRegistry::instance();
let version_manager = crate::browser_version_manager::BrowserVersionManager::instance();
let mut downloaded = Vec::new();
for browser in &["wayfern", "camoufox"] {
// Check if any version is already downloaded
let existing = registry.get_downloaded_versions(browser);
if !existing.is_empty() {
log::info!(
"ensure_active: Skipping {browser}: already have {} version(s) downloaded",
existing.len()
);
continue;
}
log::info!("ensure_active: No {browser} versions found, will download");
// Get the latest release type for this browser
let release_types = match version_manager.get_browser_release_types(browser).await {
Ok(rt) => rt,
Err(e) => {
log::warn!("Failed to get release types for {browser}: {e}");
continue;
}
};
// Use stable version (the only release type for these browsers)
let version = match release_types.stable {
Some(v) => v,
None => {
log::debug!("No stable version available for {browser} on this platform, skipping");
continue;
}
};
log::info!("Auto-downloading {browser} {version} (no versions found locally)");
match crate::downloader::download_browser(
app_handle.clone(),
browser.to_string(),
version.clone(),
)
.await
{
Ok(_) => {
downloaded.push(format!("{browser} {version}"));
log::info!("Successfully auto-downloaded {browser} {version}");
}
Err(e) => {
log::warn!("Failed to auto-download {browser} {version}: {e}");
}
}
}
Ok(downloaded)
}
#[tauri::command]
pub fn get_downloaded_browser_versions(browser_str: String) -> Result<Vec<String>, 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<Vec<(String, String, String)>, 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<Vec<String>, String> {
let registry = DownloadedBrowsersRegistry::instance();
registry
.ensure_all_binaries_exist(&app_handle)
.await
.map_err(|e| format!("Failed to ensure all binaries exist: {e}"))
}