Files
donutbrowser/src-tauri/src/api_client.rs
T
2026-07-08 01:19:52 +04:00

428 lines
12 KiB
Rust

use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VersionComponent {
pub major: u32,
pub minor: u32,
pub patch: u32,
pub build: u32,
}
impl VersionComponent {
pub fn parse(version: &str) -> Self {
let version = version.trim();
let version = if version.starts_with('v') || version.starts_with('V') {
&version[1..]
} else {
version
};
let numeric_part = Self::numeric_prefix(version);
let parts: Vec<u32> = numeric_part
.split('.')
.filter_map(|part| part.parse().ok())
.collect();
VersionComponent {
major: parts.first().copied().unwrap_or(0),
minor: parts.get(1).copied().unwrap_or(0),
patch: parts.get(2).copied().unwrap_or(0),
build: parts.get(3).copied().unwrap_or(0),
}
}
fn numeric_prefix(version: &str) -> String {
let version = version.to_lowercase();
for (i, ch) in version.char_indices() {
if ch.is_alphabetic() && i > 0 {
return version[..i].to_string();
}
}
version
}
}
impl PartialOrd for VersionComponent {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for VersionComponent {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
(self.major, self.minor, self.patch, self.build).cmp(&(
other.major,
other.minor,
other.patch,
other.build,
))
}
}
pub fn sort_versions(versions: &mut [String]) {
versions.sort_by(|a, b| {
let version_a = VersionComponent::parse(a);
let version_b = VersionComponent::parse(b);
version_b.cmp(&version_a)
});
}
pub fn compare_versions(version1: &str, version2: &str) -> std::cmp::Ordering {
let version_a = VersionComponent::parse(version1);
let version_b = VersionComponent::parse(version2);
version_a.cmp(&version_b)
}
pub fn is_version_newer(version1: &str, version2: &str) -> bool {
let version_a = VersionComponent::parse(version1);
let version_b = VersionComponent::parse(version2);
version_a > version_b
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BrowserRelease {
pub version: String,
pub date: String,
}
/// Wayfern version info from https://donutbrowser.com/wayfern.json
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct WayfernVersionInfo {
pub version: String,
pub downloads: std::collections::HashMap<String, Option<String>>,
}
#[derive(Debug, Serialize, Deserialize)]
struct CachedVersionData {
releases: Vec<BrowserRelease>,
timestamp: u64,
}
#[derive(Debug, Serialize, Deserialize)]
struct CachedWayfernData {
version_info: WayfernVersionInfo,
timestamp: u64,
}
pub struct ApiClient {
client: Client,
}
impl ApiClient {
pub fn new() -> Self {
let client = Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.unwrap_or_else(|_| Client::new());
Self { client }
}
pub fn instance() -> &'static ApiClient {
&API_CLIENT
}
fn get_cache_dir() -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
let cache_dir = crate::app_dirs::cache_dir().join("version_cache");
fs::create_dir_all(&cache_dir)?;
Ok(cache_dir)
}
fn get_current_timestamp() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
fn is_cache_valid(timestamp: u64) -> bool {
let current_time = Self::get_current_timestamp();
let cache_duration = 10 * 60;
current_time - timestamp < cache_duration
}
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"));
if !cache_file.exists() {
return None;
}
let content = fs::read_to_string(&cache_file).ok()?;
if let Ok(cached) = serde_json::from_str::<CachedVersionData>(&content) {
log::info!("Using cached versions for {browser}");
return Some(cached.releases);
}
if let Ok(legacy_versions) = serde_json::from_str::<Vec<String>>(&content) {
log::info!("Using legacy cached versions for {browser}; upgrading in-memory");
let releases: Vec<BrowserRelease> = legacy_versions
.into_iter()
.map(|version| BrowserRelease {
version,
date: "".to_string(),
})
.collect();
return Some(releases);
}
None
}
pub fn is_cache_expired(&self, browser: &str) -> bool {
let cache_dir = match Self::get_cache_dir() {
Ok(dir) => dir,
Err(_) => return true,
};
let cache_file = cache_dir.join(format!("{browser}_versions.json"));
if !cache_file.exists() {
return true;
}
let content = match fs::read_to_string(&cache_file) {
Ok(content) => content,
Err(_) => return true,
};
let cached_data: CachedVersionData = match serde_json::from_str(&content) {
Ok(data) => data,
Err(_) => return true,
};
!Self::is_cache_valid(cached_data.timestamp)
}
pub fn save_cached_versions(
&self,
browser: &str,
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 {
releases: releases.to_vec(),
timestamp: Self::get_current_timestamp(),
};
let content = serde_json::to_string_pretty(&cached_data)?;
fs::write(&cache_file, content)?;
log::info!("Cached {} versions for {}", releases.len(), browser);
Ok(())
}
fn load_cached_wayfern_version(&self) -> Option<WayfernVersionInfo> {
let cache_dir = Self::get_cache_dir().ok()?;
let cache_file = cache_dir.join("wayfern_version.json");
if !cache_file.exists() {
return None;
}
let content = fs::read_to_string(&cache_file).ok()?;
let cached_data: CachedWayfernData = serde_json::from_str(&content).ok()?;
Some(cached_data.version_info)
}
fn save_cached_wayfern_version(
&self,
version_info: &WayfernVersionInfo,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let cache_dir = Self::get_cache_dir()?;
let cache_file = cache_dir.join("wayfern_version.json");
let cached_data = CachedWayfernData {
version_info: version_info.clone(),
timestamp: Self::get_current_timestamp(),
};
let content = serde_json::to_string_pretty(&cached_data)?;
fs::write(&cache_file, content)?;
log::info!("Cached Wayfern version: {}", version_info.version);
Ok(())
}
/// Fetch Wayfern version info from https://donutbrowser.com/wayfern.json
pub async fn fetch_wayfern_version_with_caching(
&self,
no_caching: bool,
) -> Result<WayfernVersionInfo, Box<dyn std::error::Error + Send + Sync>> {
if !no_caching {
if let Some(cached_version) = self.load_cached_wayfern_version() {
log::info!("Using cached Wayfern version: {}", cached_version.version);
return Ok(cached_version);
}
}
log::info!("Fetching Wayfern version from https://donutbrowser.com/wayfern.json");
let url = "https://donutbrowser.com/wayfern.json";
let mut last_err = None;
let mut version_info: Option<WayfernVersionInfo> = None;
for attempt in 1..=3 {
match 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
{
Ok(response) => {
if !response.status().is_success() {
last_err = Some(format!("HTTP {}", response.status()));
} else {
match response.json::<WayfernVersionInfo>().await {
Ok(info) => {
version_info = Some(info);
break;
}
Err(e) => last_err = Some(format!("Failed to parse response: {e}")),
}
}
}
Err(e) => {
log::warn!("Wayfern fetch attempt {attempt}/3 failed: {e}");
last_err = Some(e.to_string());
}
}
if attempt < 3 {
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
}
}
let version_info = version_info.ok_or_else(|| {
format!(
"Failed to fetch Wayfern version after 3 attempts: {}",
last_err.unwrap_or_default()
)
})?;
log::info!("Fetched Wayfern version: {}", version_info.version);
if !no_caching {
if let Err(e) = self.save_cached_wayfern_version(&version_info) {
log::error!("Failed to cache Wayfern version: {e}");
}
}
Ok(version_info)
}
/// Get the download URL for Wayfern based on current platform
pub fn get_wayfern_download_url(&self, version_info: &WayfernVersionInfo) -> Option<String> {
let (os, arch) = Self::get_platform_info();
let platform_key = format!("{os}-{arch}");
version_info
.downloads
.get(&platform_key)
.and_then(|url| url.clone())
}
/// Check if Wayfern has a compatible download for current platform
pub fn has_wayfern_compatible_download(&self, version_info: &WayfernVersionInfo) -> bool {
self.get_wayfern_download_url(version_info).is_some()
}
fn get_platform_info() -> (String, String) {
let os = if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_os = "linux") {
"linux"
} else if cfg!(target_os = "macos") {
"macos"
} else {
"unknown"
};
let arch = if cfg!(target_arch = "x86_64") {
"x64"
} else if cfg!(target_arch = "aarch64") {
"arm64"
} else {
"unknown"
};
(os.to_string(), arch.to_string())
}
pub fn clear_all_cache(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let cache_dir = Self::get_cache_dir()?;
if cache_dir.exists() {
for entry in fs::read_dir(&cache_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
fs::remove_file(&path)?;
log::info!("Removed cache file: {path:?}");
}
}
log::info!("All version cache cleared successfully");
}
Ok(())
}
}
lazy_static::lazy_static! {
static ref API_CLIENT: ApiClient = ApiClient::new();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version_parsing() {
let v1 = VersionComponent::parse("1.2.3");
assert_eq!(v1.major, 1);
assert_eq!(v1.minor, 2);
assert_eq!(v1.patch, 3);
let v2 = VersionComponent::parse("138.0.7204.50");
assert_eq!(v2.major, 138);
assert_eq!(v2.minor, 0);
assert_eq!(v2.patch, 7204);
assert_eq!(v2.build, 50);
let v3 = VersionComponent::parse("137.0b5");
assert_eq!(v3.major, 137);
assert_eq!(v3.minor, 0);
assert_eq!(v3.patch, 0);
}
#[test]
fn test_version_comparison() {
assert!(VersionComponent::parse("1.2.4") > VersionComponent::parse("1.2.3"));
assert!(VersionComponent::parse("2.0.0") > VersionComponent::parse("1.9.9"));
assert!(VersionComponent::parse("138.0.7204.50") > VersionComponent::parse("138.0.7204.49"));
}
#[test]
fn test_version_sorting() {
let mut versions = vec![
"138.0.7204.50".to_string(),
"138.0.7204.49".to_string(),
"139.0.7204.1".to_string(),
"137.0.7204.99".to_string(),
];
sort_versions(&mut versions);
assert_eq!(versions[0], "139.0.7204.1");
assert_eq!(versions[1], "138.0.7204.50");
assert_eq!(versions[2], "138.0.7204.49");
assert_eq!(versions[3], "137.0.7204.99");
}
}