use anyhow::Result;
use serde_json;
use tokio::sync::mpsc;
#[derive(Debug, Clone)]
pub enum UpdateMsg {
UpdateAvailable { current: String, latest: String, download_url: String },
UpToDate,
DownloadProgress { downloaded: u64, total: Option<u64> },
Installing,
Done { new_version: String },
Failed(String),
}
pub fn detect_target() -> String {
if cfg!(target_os = "android") {
"termux-aarch64".to_string()
} else if cfg!(target_arch = "x86_64") {
"x86_64-linux".to_string()
} else {
"aarch64-linux".to_string()
}
}
pub fn build_download_url(version: &str, target: &str) -> String {
format!(
"https://github.com/SharkMI-0x7E/mc-minder/releases/download/v{}/mc-minder-{}",
version, target
)
}
#[allow(dead_code)]
pub fn is_newer_version(current: &str, latest: &str) -> bool {
fn parse_ver(v: &str) -> Vec<u32> {
v.split('.').filter_map(|s| s.parse().ok()).collect()
}
parse_ver(latest) > parse_ver(current)
}
pub fn format_bytes(bytes: u64) -> String {
const KB: f64 = 1024.0;
const MB: f64 = 1024.0 * KB;
if bytes as f64 >= MB {
format!("{:.1} MB", bytes as f64 / MB)
} else if bytes as f64 >= KB {
format!("{:.1} KB", bytes as f64 / KB)
} else {
format!("{} B", bytes)
}
}
pub struct UpdateEngine {
client: reqwest::Client,
}
impl UpdateEngine {
pub fn new() -> Self {
Self { client: reqwest::Client::new() }
}
pub async fn check_update(&self, current_version: &str) -> UpdateMsg {
let response = match self.client
.get("https://api.github.com/repos/SharkMI-0x7E/mc-minder/releases/latest")
.header("User-Agent", "mc-minder")
.send()
.await {
Ok(r) => r,
Err(e) => return UpdateMsg::Failed(format!("Network error: {}", e)),
};
if !response.status().is_success() {
return UpdateMsg::Failed(format!("HTTP error: {}", response.status()));
}
let release: serde_json::Value = match response.json().await {
Ok(v) => v,
Err(e) => return UpdateMsg::Failed(format!("Parse error: {}", e)),
};
let latest = release["tag_name"]
.as_str()
.unwrap_or("unknown")
.trim_start_matches('v')
.to_string();
let target = detect_target();
let download_url = build_download_url(&latest, &target);
if latest == current_version {
UpdateMsg::UpToDate
} else {
UpdateMsg::UpdateAvailable {
current: current_version.to_string(),
latest,
download_url,
}
}
}
pub async fn download_and_install(
&self,
download_url: &str,
latest_version: &str,
tx: mpsc::Sender<UpdateMsg>,
) -> Result<(), String> {
let response = self.client
.get(download_url)
.send()
.await
.map_err(|e| format!("Download failed: {}", e))?;
if !response.status().is_success() {
return Err(format!("HTTP error: {}", response.status()));
}
let total_size = response.content_length();
let mut downloaded: u64 = 0;
let mut stream = response.bytes_stream();
let mut data = Vec::new();
use futures_util::StreamExt;
while let Some(chunk) = stream.next().await {
match chunk {
Ok(bytes) => {
downloaded += bytes.len() as u64;
data.extend_from_slice(&bytes);
let _ = tx.send(UpdateMsg::DownloadProgress {
downloaded,
total: total_size,
}).await;
}
Err(e) => {
return Err(format!("Download error: {}", e));
}
}
}
let _ = tx.send(UpdateMsg::Installing).await;
let exe_path = std::env::current_exe().map_err(|e| format!("Failed to get exe path: {}", e))?;
let backup_path = format!("{}.old", exe_path.display());
std::fs::rename(&exe_path, &backup_path).map_err(|e| format!("Backup failed: {}", e))?;
std::fs::write(&exe_path, &data).map_err(|e| format!("Write failed: {}", e))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&exe_path, fs::Permissions::from_mode(0o755))
.map_err(|e| format!("Permission failed: {}", e))?;
}
let _ = tx.send(UpdateMsg::Done { new_version: latest_version.to_string() }).await;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_download_url() {
let url = build_download_url("0.4.7", "x86_64-linux");
assert_eq!(url, "https://github.com/SharkMI-0x7E/mc-minder/releases/download/v0.4.7/mc-minder-x86_64-linux");
}
#[test]
fn test_is_newer_version() {
assert!(is_newer_version("0.4.6", "0.4.7"));
assert!(!is_newer_version("0.4.7", "0.4.6"));
assert!(!is_newer_version("0.4.7", "0.4.7"));
assert!(is_newer_version("0.3.9", "0.4.0"));
assert!(is_newer_version("0.4.6", "1.0.0"));
}
#[test]
fn test_format_bytes() {
assert_eq!(format_bytes(500), "500 B");
assert_eq!(format_bytes(1024), "1.0 KB");
assert_eq!(format_bytes(1048576), "1.0 MB");
assert_eq!(format_bytes(1572864), "1.5 MB");
}
#[test]
fn test_detect_target() {
let t = detect_target();
assert!(t == "termux-aarch64" || t == "x86_64-linux" || t == "aarch64-linux");
}
}