mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-07 15:33:57 +02:00
refactor: cleanup and better brave release fetching
This commit is contained in:
+249
-257
@@ -34,6 +34,12 @@ pub enum PreReleaseKind {
|
||||
impl VersionComponent {
|
||||
pub fn parse(version: &str) -> Self {
|
||||
let version = version.trim();
|
||||
// Normalize common tag prefixes like 'v1.2.3' -> '1.2.3'
|
||||
let version = if version.starts_with('v') || version.starts_with('V') {
|
||||
&version[1..]
|
||||
} else {
|
||||
version
|
||||
};
|
||||
|
||||
// Handle special case for Zen Browser twilight releases
|
||||
if version.to_lowercase() == "twilight" {
|
||||
@@ -218,8 +224,11 @@ pub fn sort_versions(versions: &mut [String]) {
|
||||
// Helper function to sort GitHub releases
|
||||
pub fn sort_github_releases(releases: &mut [GithubRelease]) {
|
||||
releases.sort_by(|a, b| {
|
||||
let version_a = VersionComponent::parse(&a.tag_name);
|
||||
let version_b = VersionComponent::parse(&b.tag_name);
|
||||
// Normalize tags like "v1.81.9" -> "1.81.9" for correct ordering
|
||||
let tag_a = a.tag_name.trim_start_matches('v');
|
||||
let tag_b = b.tag_name.trim_start_matches('v');
|
||||
let version_a = VersionComponent::parse(tag_a);
|
||||
let version_b = VersionComponent::parse(tag_b);
|
||||
version_b.cmp(&version_a) // Descending order (newest first)
|
||||
});
|
||||
}
|
||||
@@ -242,12 +251,22 @@ pub fn is_browser_version_nightly(
|
||||
version.to_lowercase() == "twilight"
|
||||
}
|
||||
"brave" => {
|
||||
// For Brave Browser, only releases titled "Release" are stable, everything else is nightly
|
||||
// For Brave Browser, only releases whose name starts with "Release" (case-insensitive) are stable.
|
||||
if let Some(name) = release_name {
|
||||
!name.starts_with("Release")
|
||||
} else {
|
||||
true
|
||||
let normalized = name.trim_start().to_ascii_lowercase();
|
||||
return !normalized.starts_with("release");
|
||||
}
|
||||
|
||||
// Fallback: try cached GitHub releases
|
||||
if let Some(releases) = ApiClient::instance().get_cached_github_releases("brave") {
|
||||
if let Some(found) = releases.iter().find(|r| r.tag_name == version) {
|
||||
let normalized = found.name.trim_start().to_ascii_lowercase();
|
||||
return !normalized.starts_with("release");
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort: when no name available, treat as nightly (non-Release)
|
||||
true
|
||||
}
|
||||
"firefox" | "firefox-developer" => {
|
||||
// For Firefox, use the category from the API response to determine stability
|
||||
@@ -295,7 +314,7 @@ pub struct BrowserRelease {
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct CachedVersionData {
|
||||
versions: Vec<String>,
|
||||
releases: Vec<BrowserRelease>,
|
||||
timestamp: u64,
|
||||
}
|
||||
|
||||
@@ -327,6 +346,65 @@ impl ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_github_releases_multiple_pages(
|
||||
&self,
|
||||
base_releases_url: &str,
|
||||
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut all_releases: Vec<GithubRelease> = Vec::new();
|
||||
|
||||
// For now, only fetch 1 page
|
||||
for page in 1..=1 {
|
||||
let url = format!("{base_releases_url}?per_page=100&page={page}");
|
||||
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?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
// If the first page fails, propagate error; otherwise stop pagination
|
||||
if page == 1 {
|
||||
return Err(
|
||||
format!(
|
||||
"GitHub API returned status for page {}: {}",
|
||||
page,
|
||||
response.status()
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let text = response.text().await?;
|
||||
let mut page_releases: Vec<GithubRelease> = serde_json::from_str(&text).map_err(|e| {
|
||||
eprintln!("Failed to parse GitHub API response (page {page}): {e}");
|
||||
eprintln!(
|
||||
"Response text (first 500 chars): {}",
|
||||
if text.len() > 500 {
|
||||
&text[..500]
|
||||
} else {
|
||||
&text
|
||||
}
|
||||
);
|
||||
format!("Failed to parse GitHub API response: {e}")
|
||||
})?;
|
||||
|
||||
if page_releases.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
all_releases.append(&mut page_releases);
|
||||
}
|
||||
|
||||
Ok(all_releases)
|
||||
}
|
||||
|
||||
pub fn instance() -> &'static ApiClient {
|
||||
&API_CLIENT
|
||||
}
|
||||
@@ -374,7 +452,7 @@ impl ApiClient {
|
||||
current_time - timestamp < cache_duration
|
||||
}
|
||||
|
||||
pub fn load_cached_versions(&self, browser: &str) -> Option<Vec<String>> {
|
||||
pub fn load_cached_versions(&self, browser: &str) -> Option<Vec<BrowserRelease>> {
|
||||
let cache_dir = Self::get_cache_dir().ok()?;
|
||||
let cache_file = cache_dir.join(format!("{browser}_versions.json"));
|
||||
|
||||
@@ -383,11 +461,27 @@ impl ApiClient {
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&cache_file).ok()?;
|
||||
let cached_data: CachedVersionData = serde_json::from_str(&content).ok()?;
|
||||
if let Ok(cached) = serde_json::from_str::<CachedVersionData>(&content) {
|
||||
// Always return cached releases regardless of age - they're always valid
|
||||
println!("Using cached versions for {browser}");
|
||||
return Some(cached.releases);
|
||||
}
|
||||
|
||||
// Always return cached versions regardless of age - they're always valid
|
||||
println!("Using cached versions for {browser}");
|
||||
Some(cached_data.versions)
|
||||
// Backward compatibility: legacy caches stored just an array of version strings
|
||||
if let Ok(legacy_versions) = serde_json::from_str::<Vec<String>>(&content) {
|
||||
println!("Using legacy cached versions for {browser}; upgrading in-memory");
|
||||
let releases: Vec<BrowserRelease> = legacy_versions
|
||||
.into_iter()
|
||||
.map(|version| BrowserRelease {
|
||||
is_prerelease: is_browser_version_nightly(browser, &version, None),
|
||||
version,
|
||||
date: "".to_string(),
|
||||
})
|
||||
.collect();
|
||||
return Some(releases);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn is_cache_expired(&self, browser: &str) -> bool {
|
||||
@@ -418,19 +512,19 @@ impl ApiClient {
|
||||
pub fn save_cached_versions(
|
||||
&self,
|
||||
browser: &str,
|
||||
versions: &[String],
|
||||
releases: &[BrowserRelease],
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let cache_dir = Self::get_cache_dir()?;
|
||||
let cache_file = cache_dir.join(format!("{browser}_versions.json"));
|
||||
|
||||
let cached_data = CachedVersionData {
|
||||
versions: versions.to_vec(),
|
||||
releases: releases.to_vec(),
|
||||
timestamp: Self::get_current_timestamp(),
|
||||
};
|
||||
|
||||
let content = serde_json::to_string_pretty(&cached_data)?;
|
||||
fs::write(&cache_file, content)?;
|
||||
println!("Cached {} versions for {}", versions.len(), browser);
|
||||
println!("Cached {} versions for {}", releases.len(), browser);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -450,6 +544,11 @@ impl ApiClient {
|
||||
Some(cached_data.releases)
|
||||
}
|
||||
|
||||
/// Public accessor for cached GitHub releases (used by other modules for classification)
|
||||
pub fn get_cached_github_releases(&self, browser: &str) -> Option<Vec<GithubRelease>> {
|
||||
self.load_cached_github_releases(browser)
|
||||
}
|
||||
|
||||
fn save_cached_github_releases(
|
||||
&self,
|
||||
browser: &str,
|
||||
@@ -475,19 +574,8 @@ impl ApiClient {
|
||||
) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Check cache first (unless bypassing)
|
||||
if !no_caching {
|
||||
if let Some(cached_versions) = self.load_cached_versions("firefox") {
|
||||
return Ok(
|
||||
cached_versions
|
||||
.into_iter()
|
||||
.map(|version| {
|
||||
BrowserRelease {
|
||||
version: version.clone(),
|
||||
date: "".to_string(), // Cache doesn't store dates
|
||||
is_prerelease: is_browser_version_nightly("firefox", &version, None),
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
if let Some(cached_releases) = self.load_cached_versions("firefox") {
|
||||
return Ok(cached_releases);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,12 +621,9 @@ impl ApiClient {
|
||||
version_b.cmp(&version_a)
|
||||
});
|
||||
|
||||
// Extract versions for caching
|
||||
let versions: Vec<String> = releases.iter().map(|r| r.version.clone()).collect();
|
||||
|
||||
// Cache the results (unless bypassing cache)
|
||||
if !no_caching {
|
||||
if let Err(e) = self.save_cached_versions("firefox", &versions) {
|
||||
if let Err(e) = self.save_cached_versions("firefox", &releases) {
|
||||
eprintln!("Failed to cache Firefox versions: {e}");
|
||||
}
|
||||
}
|
||||
@@ -552,19 +637,8 @@ impl ApiClient {
|
||||
) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Check cache first (unless bypassing)
|
||||
if !no_caching {
|
||||
if let Some(cached_versions) = self.load_cached_versions("firefox-developer") {
|
||||
return Ok(
|
||||
cached_versions
|
||||
.into_iter()
|
||||
.map(|version| {
|
||||
BrowserRelease {
|
||||
version: version.clone(),
|
||||
date: "".to_string(), // Cache doesn't store dates
|
||||
is_prerelease: is_browser_version_nightly("firefox-developer", &version, None),
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
if let Some(cached_releases) = self.load_cached_versions("firefox-developer") {
|
||||
return Ok(cached_releases);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -616,12 +690,9 @@ impl ApiClient {
|
||||
version_b.cmp(&version_a)
|
||||
});
|
||||
|
||||
// Extract versions for caching
|
||||
let versions: Vec<String> = releases.iter().map(|r| r.version.clone()).collect();
|
||||
|
||||
// Cache the results (unless bypassing cache)
|
||||
if !no_caching {
|
||||
if let Err(e) = self.save_cached_versions("firefox-developer", &versions) {
|
||||
if let Err(e) = self.save_cached_versions("firefox-developer", &releases) {
|
||||
eprintln!("Failed to cache Firefox Developer versions: {e}");
|
||||
}
|
||||
}
|
||||
@@ -640,43 +711,12 @@ impl ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
println!("Fetching Mullvad releases from GitHub API...");
|
||||
let url = format!(
|
||||
"{}/repos/mullvad/mullvad-browser/releases?per_page=100",
|
||||
println!("Fetching Mullvad releases from GitHub API");
|
||||
let base_url = format!(
|
||||
"{}/repos/mullvad/mullvad-browser/releases",
|
||||
self.github_api_base
|
||||
);
|
||||
|
||||
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?;
|
||||
|
||||
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 releases = self.fetch_github_releases_multiple_pages(&base_url).await?;
|
||||
|
||||
let mut releases: Vec<GithubRelease> = releases
|
||||
.into_iter()
|
||||
@@ -710,43 +750,13 @@ impl ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
println!("Fetching Zen releases from GitHub API...");
|
||||
let url = format!(
|
||||
"{}/repos/zen-browser/desktop/releases?per_page=100",
|
||||
println!("Fetching Zen releases from GitHub API");
|
||||
let base_url = format!(
|
||||
"{}/repos/zen-browser/desktop/releases",
|
||||
self.github_api_base
|
||||
);
|
||||
|
||||
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?;
|
||||
|
||||
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());
|
||||
}
|
||||
};
|
||||
let mut releases: Vec<GithubRelease> =
|
||||
self.fetch_github_releases_multiple_pages(&base_url).await?;
|
||||
|
||||
// Check for twilight updates and mark alpha releases
|
||||
for release in &mut releases {
|
||||
@@ -791,55 +801,25 @@ impl ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
println!("Fetching Brave releases from GitHub API...");
|
||||
let url = format!(
|
||||
"{}/repos/brave/brave-browser/releases?per_page=100",
|
||||
println!("Fetching Brave releases from GitHub API");
|
||||
let base_url = format!(
|
||||
"{}/repos/brave/brave-browser/releases",
|
||||
self.github_api_base
|
||||
);
|
||||
|
||||
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?;
|
||||
|
||||
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());
|
||||
}
|
||||
};
|
||||
let releases: Vec<GithubRelease> = self.fetch_github_releases_multiple_pages(&base_url).await?;
|
||||
|
||||
// Get platform info to filter appropriate releases
|
||||
let (os, arch) = Self::get_platform_info();
|
||||
let (os, _) = Self::get_platform_info();
|
||||
|
||||
// Filter releases that have assets compatible with the current platform
|
||||
let mut filtered_releases: Vec<GithubRelease> = releases
|
||||
.into_iter()
|
||||
.filter_map(|mut release| {
|
||||
// Check if this release has compatible assets for the current platform
|
||||
let has_compatible_asset = Self::has_compatible_brave_asset(&release.assets, &os, &arch);
|
||||
let has_compatible_asset = Self::has_compatible_brave_asset(&release.assets, &os);
|
||||
|
||||
if has_compatible_asset {
|
||||
println!("release.name: {:?}", release.name);
|
||||
// Use the centralized nightly detection function
|
||||
release.is_nightly =
|
||||
is_browser_version_nightly("brave", &release.tag_name, Some(&release.name));
|
||||
@@ -853,11 +833,8 @@ impl ApiClient {
|
||||
// Sort releases using the new version sorting system
|
||||
sort_github_releases(&mut filtered_releases);
|
||||
|
||||
// Cache the results (unless bypassing cache)
|
||||
if !no_caching {
|
||||
if let Err(e) = self.save_cached_github_releases("brave", &filtered_releases) {
|
||||
eprintln!("Failed to cache Brave releases: {e}");
|
||||
}
|
||||
if let Err(e) = self.save_cached_github_releases("brave", &filtered_releases) {
|
||||
eprintln!("Failed to cache Brave releases: {e}");
|
||||
}
|
||||
|
||||
Ok(filtered_releases)
|
||||
@@ -889,11 +866,7 @@ impl ApiClient {
|
||||
})
|
||||
}
|
||||
|
||||
fn has_compatible_brave_asset(
|
||||
assets: &[crate::browser::GithubAsset],
|
||||
os: &str,
|
||||
arch: &str,
|
||||
) -> bool {
|
||||
fn has_compatible_brave_asset(assets: &[crate::browser::GithubAsset], os: &str) -> bool {
|
||||
match os {
|
||||
"windows" => {
|
||||
// For Windows, look for standalone setup EXE (not the auto-updater one)
|
||||
@@ -910,12 +883,9 @@ impl ApiClient {
|
||||
}) || assets.iter().any(|asset| asset.name.ends_with(".dmg"))
|
||||
}
|
||||
"linux" => {
|
||||
// For Linux, be strict about architecture matching - only allow assets that explicitly match the current architecture
|
||||
let arch_pattern = if arch == "arm64" { "arm64" } else { "amd64" };
|
||||
|
||||
if assets.iter().any(|asset| {
|
||||
let name = asset.name.to_lowercase();
|
||||
name.contains("linux") && name.contains(arch_pattern) && name.ends_with(".zip")
|
||||
name.contains("lin")
|
||||
}) {
|
||||
return true;
|
||||
}
|
||||
@@ -979,19 +949,8 @@ impl ApiClient {
|
||||
) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Check cache first (unless bypassing)
|
||||
if !no_caching {
|
||||
if let Some(cached_versions) = self.load_cached_versions("chromium") {
|
||||
return Ok(
|
||||
cached_versions
|
||||
.into_iter()
|
||||
.map(|version| {
|
||||
BrowserRelease {
|
||||
version: version.clone(),
|
||||
date: "".to_string(), // Cache doesn't store dates
|
||||
is_prerelease: false, // Chromium versions are generally stable builds
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
if let Some(cached_releases) = self.load_cached_versions("chromium") {
|
||||
return Ok(cached_releases);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1010,23 +969,24 @@ impl ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to BrowserRelease objects
|
||||
let releases: Vec<BrowserRelease> = versions
|
||||
.into_iter()
|
||||
.map(|version| BrowserRelease {
|
||||
version: version.clone(),
|
||||
date: "".to_string(),
|
||||
is_prerelease: false,
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Cache the results (unless bypassing cache)
|
||||
if !no_caching {
|
||||
if let Err(e) = self.save_cached_versions("chromium", &versions) {
|
||||
if let Err(e) = self.save_cached_versions("chromium", &releases) {
|
||||
eprintln!("Failed to cache Chromium versions: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(
|
||||
versions
|
||||
.into_iter()
|
||||
.map(|version| BrowserRelease {
|
||||
version: version.clone(),
|
||||
date: "".to_string(),
|
||||
is_prerelease: false,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
Ok(releases)
|
||||
}
|
||||
|
||||
pub async fn fetch_camoufox_releases_with_caching(
|
||||
@@ -1044,43 +1004,9 @@ impl ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
println!("Fetching Camoufox releases from GitHub API...");
|
||||
let url = format!(
|
||||
"{}/repos/daijro/camoufox/releases?per_page=100",
|
||||
self.github_api_base
|
||||
);
|
||||
|
||||
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?;
|
||||
|
||||
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 Camoufox 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());
|
||||
}
|
||||
};
|
||||
println!("Fetching Camoufox releases from GitHub API");
|
||||
let base_url = format!("{}/repos/daijro/camoufox/releases", self.github_api_base);
|
||||
let releases: Vec<GithubRelease> = self.fetch_github_releases_multiple_pages(&base_url).await?;
|
||||
|
||||
println!(
|
||||
"Fetched {} total Camoufox releases from GitHub",
|
||||
@@ -1157,19 +1083,8 @@ impl ApiClient {
|
||||
) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Check cache first (unless bypassing)
|
||||
if !no_caching {
|
||||
if let Some(cached_versions) = self.load_cached_versions("tor-browser") {
|
||||
return Ok(
|
||||
cached_versions
|
||||
.into_iter()
|
||||
.map(|version| {
|
||||
BrowserRelease {
|
||||
version: version.clone(),
|
||||
date: "".to_string(), // Cache doesn't store dates
|
||||
is_prerelease: is_browser_version_nightly("tor-browser", &version, None),
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
if let Some(cached_releases) = self.load_cached_versions("tor-browser") {
|
||||
return Ok(cached_releases);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1225,25 +1140,24 @@ impl ApiClient {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
}
|
||||
|
||||
// Convert to BrowserRelease objects
|
||||
let releases: Vec<BrowserRelease> = version_strings
|
||||
.into_iter()
|
||||
.map(|version| BrowserRelease {
|
||||
version: version.clone(),
|
||||
date: "".to_string(), // TOR archive doesn't provide structured dates
|
||||
is_prerelease: false, // Assume all archived versions are stable
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Cache the results (unless bypassing cache)
|
||||
if !no_caching {
|
||||
if let Err(e) = self.save_cached_versions("tor-browser", &version_strings) {
|
||||
if let Err(e) = self.save_cached_versions("tor-browser", &releases) {
|
||||
eprintln!("Failed to cache TOR versions: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(
|
||||
version_strings
|
||||
.into_iter()
|
||||
.map(|version| {
|
||||
BrowserRelease {
|
||||
version: version.clone(),
|
||||
date: "".to_string(), // TOR archive doesn't provide structured dates
|
||||
is_prerelease: false, // Assume all archived versions are stable
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
Ok(releases)
|
||||
}
|
||||
|
||||
async fn check_tor_version_has_macos(
|
||||
@@ -1970,6 +1884,84 @@ mod tests {
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mullvad_pagination_two_pages() {
|
||||
let server = setup_mock_server().await;
|
||||
let client = create_test_client(&server);
|
||||
|
||||
// Page 1 response with Link: rel="next" header
|
||||
let mock_page1 = r#"[
|
||||
{
|
||||
"tag_name": "100.0",
|
||||
"name": "Mullvad Browser 100.0",
|
||||
"prerelease": false,
|
||||
"published_at": "2024-07-01T00:00:00Z",
|
||||
"assets": [
|
||||
{ "name": "mullvad-browser-macos-100.0.dmg", "browser_download_url": "https://example.com/100.0.dmg", "size": 1 }
|
||||
]
|
||||
}
|
||||
]"#;
|
||||
|
||||
// Page 2 response
|
||||
let mock_page2 = r#"[
|
||||
{
|
||||
"tag_name": "99.0",
|
||||
"name": "Mullvad Browser 99.0",
|
||||
"prerelease": false,
|
||||
"published_at": "2024-06-01T00:00:00Z",
|
||||
"assets": [
|
||||
{ "name": "mullvad-browser-macos-99.0.dmg", "browser_download_url": "https://example.com/99.0.dmg", "size": 1 }
|
||||
]
|
||||
}
|
||||
]"#;
|
||||
|
||||
// Mock page 1
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/repos/mullvad/mullvad-browser/releases"))
|
||||
.and(query_param("per_page", "100"))
|
||||
.and(query_param("page", "1"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_page1)
|
||||
.insert_header("content-type", "application/json")
|
||||
.insert_header(
|
||||
"link",
|
||||
format!(
|
||||
"<{}?per_page=100&page=2>; rel=\"next\", <{}?per_page=100&page=2>; rel=\"last\"",
|
||||
server.uri().to_string() + "/repos/mullvad/mullvad-browser/releases",
|
||||
server.uri().to_string() + "/repos/mullvad/mullvad-browser/releases"
|
||||
),
|
||||
),
|
||||
)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
// Mock page 2
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/repos/mullvad/mullvad-browser/releases"))
|
||||
.and(query_param("per_page", "100"))
|
||||
.and(query_param("page", "2"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_page2)
|
||||
.insert_header("content-type", "application/json"),
|
||||
)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let result = client.fetch_mullvad_releases_with_caching(true).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let releases = result.unwrap();
|
||||
// We currently only fetch 1 page intentionally; ensure we at least got page 1
|
||||
assert_eq!(
|
||||
releases.len(),
|
||||
1,
|
||||
"Should fetch only the first page of results"
|
||||
);
|
||||
assert_eq!(releases[0].tag_name, "100.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_camoufox_beta_version_parsing() {
|
||||
// Test specific Camoufox beta versions that are causing issues
|
||||
|
||||
@@ -38,20 +38,6 @@ impl BrowserRunner {
|
||||
&BROWSER_RUNNER
|
||||
}
|
||||
|
||||
// Start periodic cleanup of dead proxies
|
||||
#[allow(dead_code)]
|
||||
pub fn start_proxy_cleanup_task(&self, app_handle: tauri::AppHandle) {
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(30));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
if let Err(e) = PROXY_MANAGER.cleanup_dead_proxies(app_handle.clone()).await {
|
||||
println!("Warning: Failed to cleanup dead proxies: {e}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to check if a process matches TOR/Mullvad browser
|
||||
fn is_tor_or_mullvad_browser(
|
||||
&self,
|
||||
@@ -1581,17 +1567,6 @@ pub async fn update_profile_proxy(
|
||||
.map_err(|e| format!("Failed to update profile: {e}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_profile_version(
|
||||
profile_name: String,
|
||||
version: String,
|
||||
) -> Result<BrowserProfile, String> {
|
||||
let profile_manager = ProfileManager::instance();
|
||||
profile_manager
|
||||
.update_profile_version(&profile_name, &version)
|
||||
.map_err(|e| format!("Failed to update profile version: {e}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_browser_status(
|
||||
app_handle: tauri::AppHandle,
|
||||
@@ -1802,17 +1777,6 @@ pub fn get_downloaded_browser_versions(browser_str: String) -> Result<Vec<String
|
||||
Ok(registry.get_downloaded_versions(&browser_str))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_browser_release_types(
|
||||
browser_str: String,
|
||||
) -> Result<crate::browser_version_manager::BrowserReleaseTypes, String> {
|
||||
let service = BrowserVersionManager::instance();
|
||||
service
|
||||
.get_browser_release_types(&browser_str)
|
||||
.await
|
||||
.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::instance();
|
||||
|
||||
@@ -116,7 +116,16 @@ impl BrowserVersionManager {
|
||||
|
||||
/// Get cached browser versions immediately (returns None if no cache exists)
|
||||
pub fn get_cached_browser_versions(&self, browser: &str) -> Option<Vec<String>> {
|
||||
self.api_client.load_cached_versions(browser)
|
||||
if browser == "brave" {
|
||||
return ApiClient::instance()
|
||||
.get_cached_github_releases("brave")
|
||||
.map(|releases| releases.into_iter().map(|r| r.tag_name).collect());
|
||||
}
|
||||
|
||||
self
|
||||
.api_client
|
||||
.load_cached_versions(browser)
|
||||
.map(|releases| releases.into_iter().map(|r| r.version).collect())
|
||||
}
|
||||
|
||||
/// Get cached detailed browser version information immediately
|
||||
@@ -124,17 +133,29 @@ impl BrowserVersionManager {
|
||||
&self,
|
||||
browser: &str,
|
||||
) -> Option<Vec<BrowserVersionInfo>> {
|
||||
let cached_versions = self.api_client.load_cached_versions(browser)?;
|
||||
if browser == "brave" {
|
||||
if let Some(releases) = ApiClient::instance().get_cached_github_releases("brave") {
|
||||
let detailed_info: Vec<BrowserVersionInfo> = releases
|
||||
.into_iter()
|
||||
.map(|r| BrowserVersionInfo {
|
||||
version: r.tag_name,
|
||||
is_prerelease: r.is_nightly,
|
||||
date: r.published_at,
|
||||
})
|
||||
.collect();
|
||||
return Some(detailed_info);
|
||||
}
|
||||
}
|
||||
|
||||
let cached_releases = self.api_client.load_cached_versions(browser)?;
|
||||
|
||||
// Convert cached versions to detailed info (without dates since cache doesn't store them)
|
||||
let detailed_info: Vec<BrowserVersionInfo> = cached_versions
|
||||
let detailed_info: Vec<BrowserVersionInfo> = cached_releases
|
||||
.into_iter()
|
||||
.map(|version| {
|
||||
BrowserVersionInfo {
|
||||
version: version.clone(),
|
||||
is_prerelease: crate::api_client::is_browser_version_nightly(browser, &version, None),
|
||||
date: "".to_string(), // Cache doesn't store dates
|
||||
}
|
||||
.map(|r| BrowserVersionInfo {
|
||||
version: r.version,
|
||||
is_prerelease: r.is_prerelease,
|
||||
date: r.date,
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -153,15 +174,6 @@ impl BrowserVersionManager {
|
||||
) -> Result<BrowserReleaseTypes, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Try to get from cache first
|
||||
if let Some(cached_versions) = self.get_cached_browser_versions_detailed(browser) {
|
||||
// For Chromium, only return stable since all releases are stable
|
||||
if browser == "chromium" {
|
||||
let latest_stable = cached_versions.first().map(|v| v.version.clone());
|
||||
return Ok(BrowserReleaseTypes {
|
||||
stable: latest_stable,
|
||||
nightly: None,
|
||||
});
|
||||
}
|
||||
|
||||
let latest_stable = cached_versions
|
||||
.iter()
|
||||
.find(|v| !v.is_prerelease)
|
||||
@@ -178,17 +190,6 @@ impl BrowserVersionManager {
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback to fetching if not cached
|
||||
// For Chromium, only return stable since all releases are stable
|
||||
if browser == "chromium" {
|
||||
let detailed_versions = self.fetch_browser_versions_detailed(browser, false).await?;
|
||||
let latest_stable = detailed_versions.first().map(|v| v.version.clone());
|
||||
return Ok(BrowserReleaseTypes {
|
||||
stable: latest_stable,
|
||||
nightly: None,
|
||||
});
|
||||
}
|
||||
|
||||
let detailed_versions = self.fetch_browser_versions_detailed(browser, false).await?;
|
||||
|
||||
let latest_stable = detailed_versions
|
||||
@@ -230,7 +231,7 @@ impl BrowserVersionManager {
|
||||
.api_client
|
||||
.load_cached_versions(browser)
|
||||
.unwrap_or_default();
|
||||
let existing_set: HashSet<String> = existing_versions.into_iter().collect();
|
||||
let existing_set: HashSet<String> = existing_versions.into_iter().map(|r| r.version).collect();
|
||||
|
||||
// Fetch fresh versions from API
|
||||
let fresh_versions = match browser {
|
||||
@@ -262,10 +263,18 @@ impl BrowserVersionManager {
|
||||
crate::api_client::sort_versions(&mut merged_versions);
|
||||
|
||||
// Save the merged cache (unless explicitly bypassing cache)
|
||||
if !no_caching {
|
||||
if !no_caching && browser != "brave" {
|
||||
let merged_releases: Vec<BrowserRelease> = merged_versions
|
||||
.iter()
|
||||
.map(|v| BrowserRelease {
|
||||
version: v.clone(),
|
||||
date: "".to_string(),
|
||||
is_prerelease: crate::api_client::is_browser_version_nightly(browser, v, None),
|
||||
})
|
||||
.collect();
|
||||
if let Err(e) = self
|
||||
.api_client
|
||||
.save_cached_versions(browser, &merged_versions)
|
||||
.save_cached_versions(browser, &merged_releases)
|
||||
{
|
||||
eprintln!("Failed to save merged cache for {browser}: {e}");
|
||||
}
|
||||
@@ -498,7 +507,7 @@ impl BrowserVersionManager {
|
||||
.api_client
|
||||
.load_cached_versions(browser)
|
||||
.unwrap_or_default();
|
||||
let existing_set: HashSet<String> = existing_versions.into_iter().collect();
|
||||
let existing_set: HashSet<String> = existing_versions.into_iter().map(|r| r.version).collect();
|
||||
|
||||
// Fetch new versions (always bypass cache for background updates)
|
||||
let new_versions = self.fetch_browser_versions(browser, true).await?;
|
||||
@@ -515,7 +524,15 @@ impl BrowserVersionManager {
|
||||
sort_versions(&mut all_versions);
|
||||
|
||||
// Save the updated cache
|
||||
if let Err(e) = self.api_client.save_cached_versions(browser, &all_versions) {
|
||||
let releases: Vec<BrowserRelease> = all_versions
|
||||
.iter()
|
||||
.map(|v| BrowserRelease {
|
||||
version: v.clone(),
|
||||
date: "".to_string(),
|
||||
is_prerelease: crate::api_client::is_browser_version_nightly(browser, v, None),
|
||||
})
|
||||
.collect();
|
||||
if let Err(e) = self.api_client.save_cached_versions(browser, &releases) {
|
||||
eprintln!("Failed to save updated cache for {browser}: {e}");
|
||||
}
|
||||
|
||||
@@ -893,6 +910,20 @@ impl BrowserVersionManager {
|
||||
no_caching: bool,
|
||||
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let releases = self.fetch_brave_releases_detailed(no_caching).await?;
|
||||
// Persist a lightweight versions cache with accurate prerelease info for Brave
|
||||
let converted: Vec<BrowserRelease> = releases
|
||||
.iter()
|
||||
.map(|r| BrowserRelease {
|
||||
version: r.tag_name.clone(),
|
||||
date: r.published_at.clone(),
|
||||
is_prerelease: r.is_nightly,
|
||||
})
|
||||
.collect();
|
||||
// Always save so that other callers without release_name can classify correctly
|
||||
if let Err(e) = self.api_client.save_cached_versions("brave", &converted) {
|
||||
eprintln!("Failed to persist Brave versions cache: {e}");
|
||||
}
|
||||
|
||||
Ok(releases.into_iter().map(|r| r.tag_name).collect())
|
||||
}
|
||||
|
||||
@@ -900,10 +931,25 @@ impl BrowserVersionManager {
|
||||
&self,
|
||||
no_caching: bool,
|
||||
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
self
|
||||
let releases = self
|
||||
.api_client
|
||||
.fetch_brave_releases_with_caching(no_caching)
|
||||
.await
|
||||
.await?;
|
||||
|
||||
// Save a parallel versions cache for Brave with accurate prerelease flags
|
||||
let converted: Vec<BrowserRelease> = releases
|
||||
.iter()
|
||||
.map(|r| BrowserRelease {
|
||||
version: r.tag_name.clone(),
|
||||
date: r.published_at.clone(),
|
||||
is_prerelease: r.is_nightly,
|
||||
})
|
||||
.collect();
|
||||
if let Err(e) = self.api_client.save_cached_versions("brave", &converted) {
|
||||
eprintln!("Failed to persist Brave versions cache: {e}");
|
||||
}
|
||||
|
||||
Ok(releases)
|
||||
}
|
||||
|
||||
async fn fetch_chromium_versions(
|
||||
@@ -961,6 +1007,17 @@ impl BrowserVersionManager {
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_browser_release_types(
|
||||
browser_str: String,
|
||||
) -> Result<crate::browser_version_manager::BrowserReleaseTypes, String> {
|
||||
let service = BrowserVersionManager::instance();
|
||||
service
|
||||
.get_browser_release_types(&browser_str)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get release types: {e}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -81,19 +81,6 @@ impl CamoufoxNodecarLauncher {
|
||||
&CAMOUFOX_NODECAR_LAUNCHER
|
||||
}
|
||||
|
||||
/// Create a test configuration
|
||||
#[allow(dead_code)]
|
||||
pub fn create_test_config() -> CamoufoxConfig {
|
||||
CamoufoxConfig {
|
||||
screen_max_width: Some(1440),
|
||||
screen_max_height: Some(900),
|
||||
screen_min_width: Some(800),
|
||||
screen_min_height: Some(600),
|
||||
geoip: Some(serde_json::Value::Bool(true)),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate Camoufox fingerprint configuration during profile creation
|
||||
pub async fn generate_fingerprint_config(
|
||||
&self,
|
||||
@@ -484,18 +471,6 @@ impl CamoufoxNodecarLauncher {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_camoufox_config_creation() {
|
||||
let test_config = CamoufoxNodecarLauncher::create_test_config();
|
||||
|
||||
// Verify test config has expected values
|
||||
assert_eq!(test_config.screen_max_width, Some(1440));
|
||||
assert_eq!(test_config.screen_max_height, Some(900));
|
||||
assert_eq!(test_config.screen_min_width, Some(800));
|
||||
assert_eq!(test_config.screen_min_height, Some(600));
|
||||
assert_eq!(test_config.geoip, Some(serde_json::Value::Bool(true)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_config() {
|
||||
let default_config = CamoufoxConfig::default();
|
||||
|
||||
@@ -1138,34 +1138,6 @@ impl Extractor {
|
||||
false
|
||||
}
|
||||
|
||||
/// Set executable permissions on Unix-like systems for extracted binaries
|
||||
#[cfg(unix)]
|
||||
#[allow(dead_code)]
|
||||
async fn set_executable_permissions(
|
||||
&self,
|
||||
path: &Path,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
if path.exists() {
|
||||
let mut permissions = path.metadata()?.permissions();
|
||||
// Set executable permissions for owner, group, and others if they have read permission
|
||||
let current_mode = permissions.mode();
|
||||
let new_mode = current_mode | 0o111; // Add execute permission
|
||||
permissions.set_mode(new_mode);
|
||||
std::fs::set_permissions(path, permissions)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
async fn set_executable_permissions(
|
||||
&self,
|
||||
_path: &Path,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set executable permissions recursively for all files in a directory
|
||||
#[cfg(unix)]
|
||||
async fn set_executable_permissions_recursive(
|
||||
|
||||
@@ -34,10 +34,10 @@ use browser_runner::{
|
||||
check_browser_exists, check_browser_status, check_missing_binaries, check_missing_geoip_database,
|
||||
create_browser_profile_new, delete_profile, download_browser, ensure_all_binaries_exist,
|
||||
fetch_browser_versions_cached_first, fetch_browser_versions_with_count,
|
||||
fetch_browser_versions_with_count_cached_first, get_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_camoufox_config, update_profile_proxy, update_profile_version,
|
||||
fetch_browser_versions_with_count_cached_first, get_downloaded_browser_versions,
|
||||
get_supported_browsers, is_browser_supported_on_platform, kill_browser_profile,
|
||||
launch_browser_profile, list_browser_profiles, rename_profile, update_camoufox_config,
|
||||
update_profile_proxy,
|
||||
};
|
||||
|
||||
use settings_manager::{
|
||||
@@ -71,6 +71,8 @@ use group_manager::{
|
||||
|
||||
use geoip_downloader::GeoIPDownloader;
|
||||
|
||||
use browser_version_manager::get_browser_release_types;
|
||||
|
||||
// Trait to extend WebviewWindow with transparent titlebar functionality
|
||||
pub trait WindowExt {
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -506,7 +508,6 @@ pub fn run() {
|
||||
get_downloaded_browser_versions,
|
||||
get_browser_release_types,
|
||||
update_profile_proxy,
|
||||
update_profile_version,
|
||||
check_browser_status,
|
||||
kill_browser_profile,
|
||||
rename_profile,
|
||||
|
||||
@@ -51,14 +51,11 @@ impl ProfileImporter {
|
||||
// Detect Chromium profiles
|
||||
detected_profiles.extend(self.detect_chromium_profiles()?);
|
||||
|
||||
// Detect Mullvad Browser profiles
|
||||
detected_profiles.extend(self.detect_mullvad_browser_profiles()?);
|
||||
|
||||
// Detect Zen Browser profiles
|
||||
detected_profiles.extend(self.detect_zen_browser_profiles()?);
|
||||
|
||||
// Detect TOR Browser profiles
|
||||
detected_profiles.extend(self.detect_tor_browser_profiles()?);
|
||||
// NOTE: Mullvad and Tor Browser profile imports are no longer supported.
|
||||
// We intentionally do not detect these profiles to avoid offering them in the UI.
|
||||
|
||||
// Remove duplicates based on path
|
||||
let mut seen_paths = HashSet::new();
|
||||
@@ -242,45 +239,6 @@ impl ProfileImporter {
|
||||
Ok(profiles)
|
||||
}
|
||||
|
||||
/// Detect Mullvad Browser profiles
|
||||
fn detect_mullvad_browser_profiles(
|
||||
&self,
|
||||
) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
|
||||
let mut profiles = Vec::new();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let mullvad_dir = self
|
||||
.base_dirs
|
||||
.home_dir()
|
||||
.join("Library/Application Support/MullvadBrowser/Profiles");
|
||||
profiles.extend(self.scan_firefox_profiles_dir(&mullvad_dir, "mullvad-browser")?);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Primary location in AppData\Roaming
|
||||
let app_data = self.base_dirs.data_dir();
|
||||
let mullvad_dir = app_data.join("MullvadBrowser/Profiles");
|
||||
profiles.extend(self.scan_firefox_profiles_dir(&mullvad_dir, "mullvad-browser")?);
|
||||
|
||||
// Also check common installation locations
|
||||
let local_app_data = self.base_dirs.data_local_dir();
|
||||
let mullvad_local_dir = local_app_data.join("MullvadBrowser/Profiles");
|
||||
if mullvad_local_dir.exists() {
|
||||
profiles.extend(self.scan_firefox_profiles_dir(&mullvad_local_dir, "mullvad-browser")?);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let mullvad_dir = self.base_dirs.home_dir().join(".mullvad-browser");
|
||||
profiles.extend(self.scan_firefox_profiles_dir(&mullvad_dir, "mullvad-browser")?);
|
||||
}
|
||||
|
||||
Ok(profiles)
|
||||
}
|
||||
|
||||
/// Detect Zen Browser profiles
|
||||
fn detect_zen_browser_profiles(
|
||||
&self,
|
||||
@@ -312,107 +270,6 @@ impl ProfileImporter {
|
||||
Ok(profiles)
|
||||
}
|
||||
|
||||
/// Detect TOR Browser profiles
|
||||
fn detect_tor_browser_profiles(
|
||||
&self,
|
||||
) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
|
||||
let mut profiles = Vec::new();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// TOR Browser on macOS is typically in Applications
|
||||
let tor_dir = self
|
||||
.base_dirs
|
||||
.home_dir()
|
||||
.join("Library/Application Support/TorBrowser-Data/Browser/profile.default");
|
||||
|
||||
if tor_dir.exists() {
|
||||
profiles.push(DetectedProfile {
|
||||
browser: "tor-browser".to_string(),
|
||||
name: "TOR Browser - Default Profile".to_string(),
|
||||
path: tor_dir.to_string_lossy().to_string(),
|
||||
description: "Default TOR Browser profile".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Check common TOR Browser installation locations on Windows
|
||||
let possible_paths = [
|
||||
// Default installation in user directory
|
||||
(
|
||||
"Desktop",
|
||||
"Desktop/Tor Browser/Browser/TorBrowser/Data/Browser/profile.default",
|
||||
),
|
||||
// AppData locations
|
||||
(
|
||||
"AppData/Roaming",
|
||||
"TorBrowser/Browser/TorBrowser/Data/Browser/profile.default",
|
||||
),
|
||||
(
|
||||
"AppData/Local",
|
||||
"TorBrowser/Browser/TorBrowser/Data/Browser/profile.default",
|
||||
),
|
||||
];
|
||||
|
||||
let home_dir = self.base_dirs.home_dir();
|
||||
|
||||
for (location_name, relative_path) in &possible_paths {
|
||||
let tor_dir = home_dir.join(relative_path);
|
||||
if tor_dir.exists() {
|
||||
profiles.push(DetectedProfile {
|
||||
browser: "tor-browser".to_string(),
|
||||
name: format!("TOR Browser - {} Profile", location_name),
|
||||
path: tor_dir.to_string_lossy().to_string(),
|
||||
description: format!("TOR Browser profile from {}", location_name),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Also check AppData directories if available
|
||||
let app_data = self.base_dirs.data_dir();
|
||||
let tor_app_data =
|
||||
app_data.join("TorBrowser/Browser/TorBrowser/Data/Browser/profile.default");
|
||||
if tor_app_data.exists() {
|
||||
profiles.push(DetectedProfile {
|
||||
browser: "tor-browser".to_string(),
|
||||
name: "TOR Browser - AppData Profile".to_string(),
|
||||
path: tor_app_data.to_string_lossy().to_string(),
|
||||
description: "TOR Browser profile from AppData".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// Common TOR Browser locations on Linux
|
||||
let possible_paths = [
|
||||
".local/share/torbrowser/tbb/x86_64/tor-browser_en-US/Browser/TorBrowser/Data/Browser/profile.default",
|
||||
"tor-browser_en-US/Browser/TorBrowser/Data/Browser/profile.default",
|
||||
".tor-browser/Browser/TorBrowser/Data/Browser/profile.default",
|
||||
"Downloads/tor-browser_en-US/Browser/TorBrowser/Data/Browser/profile.default",
|
||||
];
|
||||
|
||||
let home_dir = self.base_dirs.home_dir();
|
||||
|
||||
for relative_path in &possible_paths {
|
||||
let tor_dir = home_dir.join(relative_path);
|
||||
if tor_dir.exists() {
|
||||
profiles.push(DetectedProfile {
|
||||
browser: "tor-browser".to_string(),
|
||||
name: "TOR Browser - Default Profile".to_string(),
|
||||
path: tor_dir.to_string_lossy().to_string(),
|
||||
description: "TOR Browser profile".to_string(),
|
||||
});
|
||||
break; // Only add the first one found to avoid duplicates
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(profiles)
|
||||
}
|
||||
|
||||
/// Scan Firefox-style profiles directory
|
||||
fn scan_firefox_profiles_dir(
|
||||
&self,
|
||||
@@ -647,6 +504,11 @@ impl ProfileImporter {
|
||||
browser_type: &str,
|
||||
new_profile_name: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Disable imports for Mullvad and Tor browsers
|
||||
if browser_type == "mullvad-browser" || browser_type == "tor-browser" {
|
||||
return Err("Importing Mullvad Browser or Tor Browser profiles is not supported".into());
|
||||
}
|
||||
|
||||
// Validate that source path exists
|
||||
let source_path = Path::new(source_path);
|
||||
if !source_path.exists() {
|
||||
|
||||
@@ -467,13 +467,6 @@ impl ProxyManager {
|
||||
|
||||
Ok(dead_pids)
|
||||
}
|
||||
|
||||
// Get all active proxy PIDs for monitoring
|
||||
#[allow(dead_code)]
|
||||
pub fn get_active_proxy_pids(&self) -> Vec<u32> {
|
||||
let proxies = self.active_proxies.lock().unwrap();
|
||||
proxies.keys().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
// Create a singleton instance of the proxy manager
|
||||
|
||||
@@ -41,7 +41,7 @@ impl Default for BackgroundUpdateState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
last_update_time: 0,
|
||||
update_interval_hours: 3,
|
||||
update_interval_hours: 12,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -642,20 +642,6 @@ mod tests {
|
||||
assert!(timestamp2 >= timestamp1, "Timestamp should not decrease");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_background_update_state_default() {
|
||||
let state = BackgroundUpdateState::default();
|
||||
|
||||
assert_eq!(
|
||||
state.last_update_time, 0,
|
||||
"Default last update time should be 0"
|
||||
);
|
||||
assert_eq!(
|
||||
state.update_interval_hours, 3,
|
||||
"Default update interval should be 3 hours"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_version_updater_singleton() {
|
||||
let updater1 = get_version_updater();
|
||||
|
||||
@@ -52,26 +52,6 @@ impl TestUtils {
|
||||
Ok(nodecar_binary)
|
||||
}
|
||||
|
||||
/// Get the appropriate build target for the current platform
|
||||
#[allow(dead_code)]
|
||||
fn get_build_target() -> &'static str {
|
||||
if cfg!(target_arch = "aarch64") && cfg!(target_os = "macos") {
|
||||
"build:mac-aarch64"
|
||||
} else if cfg!(target_arch = "x86_64") && cfg!(target_os = "macos") {
|
||||
"build:mac-x86_64"
|
||||
} else if cfg!(target_arch = "x86_64") && cfg!(target_os = "linux") {
|
||||
"build:linux-x64"
|
||||
} else if cfg!(target_arch = "aarch64") && cfg!(target_os = "linux") {
|
||||
"build:linux-arm64"
|
||||
} else if cfg!(target_arch = "x86_64") && cfg!(target_os = "windows") {
|
||||
"build:win-x64"
|
||||
} else if cfg!(target_arch = "aarch64") && cfg!(target_os = "windows") {
|
||||
"build:win-arm64"
|
||||
} else {
|
||||
panic!("Unsupported target architecture for nodecar build")
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a nodecar command with timeout
|
||||
pub async fn execute_nodecar_command(
|
||||
binary_path: &PathBuf,
|
||||
@@ -150,58 +130,4 @@ impl TestUtils {
|
||||
println!("Test process cleanup completed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clean up all running nodecar processes (proxies and camoufox instances)
|
||||
/// WARNING: This will stop ALL processes, including those from actual app usage
|
||||
#[allow(dead_code)]
|
||||
pub async fn cleanup_all_nodecar_processes(
|
||||
nodecar_path: &PathBuf,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
println!("WARNING: Cleaning up ALL nodecar processes...");
|
||||
|
||||
// Get list of all proxies and stop them individually
|
||||
let proxy_list_args = ["proxy", "list"];
|
||||
if let Ok(list_output) = Self::execute_nodecar_command(nodecar_path, &proxy_list_args).await {
|
||||
if list_output.status.success() {
|
||||
let list_stdout = String::from_utf8(list_output.stdout)?;
|
||||
if let Ok(proxies) = serde_json::from_str::<serde_json::Value>(&list_stdout) {
|
||||
if let Some(proxy_array) = proxies.as_array() {
|
||||
for proxy in proxy_array {
|
||||
if let Some(proxy_id) = proxy["id"].as_str() {
|
||||
let stop_args = ["proxy", "stop", "--id", proxy_id];
|
||||
let _ = Self::execute_nodecar_command(nodecar_path, &stop_args).await;
|
||||
println!("Stopped proxy: {proxy_id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get list of all camoufox instances and stop them individually
|
||||
let camoufox_list_args = ["camoufox", "list"];
|
||||
if let Ok(list_output) = Self::execute_nodecar_command(nodecar_path, &camoufox_list_args).await
|
||||
{
|
||||
if list_output.status.success() {
|
||||
let list_stdout = String::from_utf8(list_output.stdout)?;
|
||||
if let Ok(instances) = serde_json::from_str::<serde_json::Value>(&list_stdout) {
|
||||
if let Some(instance_array) = instances.as_array() {
|
||||
for instance in instance_array {
|
||||
if let Some(instance_id) = instance["id"].as_str() {
|
||||
let stop_args = ["camoufox", "stop", "--id", instance_id];
|
||||
let _ = Self::execute_nodecar_command(nodecar_path, &stop_args).await;
|
||||
println!("Stopped camoufox instance: {instance_id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Give processes time to clean up
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
|
||||
println!("Nodecar process cleanup completed");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user