mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-06 23:13:58 +02:00
refactor: improve auto-delete and auto-install browser logic
This commit is contained in:
@@ -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::<Vec<GithubRelease>>()
|
||||
.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<GithubRelease> = 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<GithubRelease> = 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::<Vec<GithubRelease>>()
|
||||
.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<GithubRelease> = 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::<Vec<GithubRelease>>()
|
||||
.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<GithubRelease> = 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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<String>,
|
||||
#[serde(default)]
|
||||
pub html_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub id: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub node_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub target_commitish: Option<String>,
|
||||
#[serde(default)]
|
||||
pub created_at: Option<String>,
|
||||
#[serde(default)]
|
||||
pub tarball_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub zipball_url: Option<String>,
|
||||
}
|
||||
|
||||
#[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<u64>,
|
||||
#[serde(default)]
|
||||
pub id: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub node_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub label: Option<String>,
|
||||
#[serde(default)]
|
||||
pub content_type: Option<String>,
|
||||
#[serde(default)]
|
||||
pub state: Option<String>,
|
||||
#[serde(default)]
|
||||
pub created_at: Option<String>,
|
||||
#[serde(default)]
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+369
-192
@@ -1058,6 +1058,8 @@ impl BrowserRunner {
|
||||
release_type: &str,
|
||||
proxy: Option<ProxySettings>,
|
||||
) -> Result<BrowserProfile, Box<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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::<BrowserProfile>(&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<Vec<(String, String, String)>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// 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<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// 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<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||
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<String, String> {
|
||||
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<Vec<(String, String, String)>, 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<Vec<String>, 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)]
|
||||
|
||||
@@ -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::<Vec<_>>()
|
||||
.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)
|
||||
}
|
||||
|
||||
@@ -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<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
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)]
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user