mirror of
https://github.com/OpenListTeam/OpenList-Desktop.git
synced 2025-11-25 03:14:56 +08:00
* 📦 Chore(custom): update build command * 🐛 Fix(custom): fix action error * 🐛 Fix(custom): fix ci * 🐛 Fix(custom): fix prepare.js for linux and macos * 🐛 Fix(custom): fix windows scripts syntax * 🐛 Fix(custom): add linker for linux * 🐛 Fix(custom): fix linux arm building * 🐛 Fix(custom): add creation flags for silent installer on Windows
541 lines
16 KiB
Rust
541 lines
16 KiB
Rust
use crate::{cmd::config::save_settings, object::structs::AppState};
|
|
use reqwest::Client;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::env;
|
|
use std::path::PathBuf;
|
|
use std::process::Command;
|
|
use std::time::Duration;
|
|
use tauri::{AppHandle, Emitter, State};
|
|
use tokio::io::AsyncWriteExt;
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct GitHubRelease {
|
|
tag_name: String,
|
|
name: String,
|
|
body: String,
|
|
published_at: String,
|
|
assets: Vec<GitHubAsset>,
|
|
prerelease: bool,
|
|
draft: bool,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct GitHubAsset {
|
|
id: u64,
|
|
name: String,
|
|
size: u64,
|
|
download_count: u64,
|
|
browser_download_url: String,
|
|
content_type: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct UpdateAsset {
|
|
name: String,
|
|
url: String,
|
|
size: u64,
|
|
platform: String,
|
|
#[serde(rename = "type")]
|
|
asset_type: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct UpdateCheck {
|
|
#[serde(rename = "hasUpdate")]
|
|
has_update: bool,
|
|
#[serde(rename = "currentVersion")]
|
|
current_version: String,
|
|
#[serde(rename = "latestVersion")]
|
|
latest_version: String,
|
|
#[serde(rename = "releaseDate")]
|
|
release_date: String,
|
|
#[serde(rename = "releaseNotes")]
|
|
release_notes: String,
|
|
assets: Vec<UpdateAsset>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct DownloadProgress {
|
|
downloaded: u64,
|
|
total: u64,
|
|
percentage: f64,
|
|
speed: f64,
|
|
}
|
|
|
|
fn get_current_platform() -> String {
|
|
let os = env::consts::OS;
|
|
let arch = env::consts::ARCH;
|
|
|
|
match os {
|
|
"windows" => format!("{arch}-pc-windows-msvc"),
|
|
"macos" => format!("{arch}-apple-darwin"),
|
|
"linux" => format!("{arch}-unknown-linux-gnu"),
|
|
_ => format!("{arch}-{os}"),
|
|
}
|
|
}
|
|
|
|
fn filter_assets_for_platform(assets: &[GitHubAsset]) -> Vec<UpdateAsset> {
|
|
let platform = get_current_platform();
|
|
let os = env::consts::OS;
|
|
|
|
assets
|
|
.iter()
|
|
.filter_map(|asset| {
|
|
let name = asset.name.to_lowercase();
|
|
|
|
let is_for_platform = match os {
|
|
"windows" => {
|
|
name.contains("windows")
|
|
|| name.contains("win")
|
|
|| name.contains("msvc")
|
|
|| name.contains("exe")
|
|
}
|
|
"macos" => {
|
|
name.contains("darwin")
|
|
|| name.contains("macos")
|
|
|| name.contains("mac")
|
|
|| name.contains("dmg")
|
|
}
|
|
"linux" => {
|
|
name.contains("linux")
|
|
|| name.contains("gnu")
|
|
|| name.contains("deb")
|
|
|| name.contains("rpm")
|
|
}
|
|
_ => false,
|
|
};
|
|
|
|
if !is_for_platform {
|
|
return None;
|
|
}
|
|
|
|
let asset_type = if name.ends_with(".exe") {
|
|
"exe"
|
|
} else if name.ends_with(".deb") {
|
|
"deb"
|
|
} else if name.ends_with(".rpm") {
|
|
"rpm"
|
|
} else if name.ends_with(".dmg") {
|
|
"dmg"
|
|
} else {
|
|
return None;
|
|
};
|
|
|
|
Some(UpdateAsset {
|
|
name: asset.name.clone(),
|
|
url: asset.browser_download_url.clone(),
|
|
size: asset.size,
|
|
platform: platform.clone(),
|
|
asset_type: asset_type.to_string(),
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn compare_versions(current: &str, latest: &str) -> bool {
|
|
let current = current.trim_start_matches('v');
|
|
let latest = latest.trim_start_matches('v');
|
|
|
|
let parse_version = |version: &str| -> Vec<u32> {
|
|
version
|
|
.split('.')
|
|
.filter_map(|part| {
|
|
let numeric_part = part.split('-').next().unwrap_or(part);
|
|
numeric_part.parse::<u32>().ok()
|
|
})
|
|
.collect()
|
|
};
|
|
|
|
let current_parts = parse_version(current);
|
|
let latest_parts = parse_version(latest);
|
|
|
|
let max_len = current_parts.len().max(latest_parts.len());
|
|
for i in 0..max_len {
|
|
let current_part = current_parts.get(i).unwrap_or(&0);
|
|
let latest_part = latest_parts.get(i).unwrap_or(&0);
|
|
|
|
match latest_part.cmp(current_part) {
|
|
std::cmp::Ordering::Greater => return true,
|
|
std::cmp::Ordering::Less => return false,
|
|
std::cmp::Ordering::Equal => continue,
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn check_for_updates() -> Result<UpdateCheck, String> {
|
|
log::info!("Checking for updates...");
|
|
|
|
let client = Client::new();
|
|
let url = "https://api.github.com/repos/OpenListTeam/openlist-desktop/releases/latest";
|
|
|
|
let response = client
|
|
.get(url)
|
|
.header("User-Agent", "OpenList-Desktop")
|
|
.timeout(Duration::from_secs(30))
|
|
.send()
|
|
.await
|
|
.map_err(|e| {
|
|
let error_msg = format!("Network error while checking for updates: {e}");
|
|
log::error!("{error_msg}");
|
|
error_msg
|
|
})?;
|
|
|
|
if !response.status().is_success() {
|
|
let status = response.status();
|
|
let error_msg = if status.as_u16() == 404 {
|
|
"Repository not found. Please check the repository URL.".to_string()
|
|
} else if status.as_u16() == 403 {
|
|
"API rate limit exceeded. Please try again later.".to_string()
|
|
} else {
|
|
format!(
|
|
"GitHub API returned status: {} ({})",
|
|
status.as_u16(),
|
|
status.canonical_reason().unwrap_or("Unknown")
|
|
)
|
|
};
|
|
log::error!("{error_msg}");
|
|
return Err(error_msg);
|
|
}
|
|
|
|
let release: GitHubRelease = response.json().await.map_err(|e| {
|
|
log::error!("Failed to parse GitHub response: {e}");
|
|
format!("Failed to parse update information: {e}")
|
|
})?;
|
|
|
|
let current_version = env!("CARGO_PKG_VERSION");
|
|
let latest_version = release.tag_name.as_str();
|
|
|
|
let has_update = compare_versions(current_version, latest_version);
|
|
let assets = filter_assets_for_platform(&release.assets);
|
|
|
|
log::info!(
|
|
"Update check result: current={}, latest={}, has_update={}, assets_count={}",
|
|
current_version,
|
|
latest_version,
|
|
has_update,
|
|
assets.len()
|
|
);
|
|
|
|
Ok(UpdateCheck {
|
|
has_update,
|
|
current_version: current_version.to_string(),
|
|
latest_version: latest_version.to_string(),
|
|
release_date: release.published_at,
|
|
release_notes: release.body,
|
|
assets,
|
|
})
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn download_update(
|
|
app: AppHandle,
|
|
asset_url: String,
|
|
asset_name: String,
|
|
) -> Result<String, String> {
|
|
log::info!("Starting download of update: {asset_name}");
|
|
|
|
let client = Client::new();
|
|
|
|
let temp_dir = std::env::temp_dir();
|
|
let file_path = temp_dir.join(&asset_name);
|
|
|
|
log::info!("Downloading to: {file_path:?}");
|
|
|
|
let mut response = client
|
|
.get(&asset_url)
|
|
.header("User-Agent", "OpenList-Desktop")
|
|
.timeout(Duration::from_secs(9000))
|
|
.send()
|
|
.await
|
|
.map_err(|e| {
|
|
let error_msg = format!("Failed to start download: {e}");
|
|
log::error!("{error_msg}");
|
|
error_msg
|
|
})?;
|
|
|
|
if !response.status().is_success() {
|
|
let status = response.status();
|
|
let error_msg = if status.as_u16() == 404 {
|
|
"Download file not found. The release asset may have been removed.".to_string()
|
|
} else {
|
|
format!(
|
|
"Download failed with status: {} ({})",
|
|
status.as_u16(),
|
|
status.canonical_reason().unwrap_or("Unknown")
|
|
)
|
|
};
|
|
log::error!("{error_msg}");
|
|
return Err(error_msg);
|
|
}
|
|
|
|
let total_size = response.content_length().unwrap_or(0);
|
|
log::info!("Download size: {total_size} bytes");
|
|
|
|
let mut file = tokio::fs::File::create(&file_path).await.map_err(|e| {
|
|
log::error!("Failed to create download file: {e}");
|
|
format!("Failed to create file: {e}")
|
|
})?;
|
|
|
|
let mut downloaded = 0u64;
|
|
let mut last_progress_time = std::time::Instant::now();
|
|
let mut last_downloaded = 0u64;
|
|
|
|
while let Some(chunk) = response.chunk().await.map_err(|e| {
|
|
log::error!("Download chunk error: {e}");
|
|
format!("Download error: {e}")
|
|
})? {
|
|
file.write_all(&chunk).await.map_err(|e| {
|
|
log::error!("File write error: {e}");
|
|
format!("File write error: {e}")
|
|
})?;
|
|
|
|
downloaded += chunk.len() as u64;
|
|
|
|
let now = std::time::Instant::now();
|
|
if now.duration_since(last_progress_time).as_secs() >= 1 || downloaded >= total_size {
|
|
let elapsed = now.duration_since(last_progress_time).as_secs_f64();
|
|
let speed = if elapsed > 0.0 {
|
|
(downloaded - last_downloaded) as f64 / elapsed
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
let percentage = if total_size > 0 {
|
|
(downloaded as f64 / total_size as f64) * 100.0
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
let progress = DownloadProgress {
|
|
downloaded,
|
|
total: total_size,
|
|
percentage,
|
|
speed,
|
|
};
|
|
|
|
if let Err(e) = app.emit("download-progress", &progress) {
|
|
log::error!("Failed to emit download progress: {e}");
|
|
}
|
|
|
|
last_progress_time = now;
|
|
last_downloaded = downloaded;
|
|
}
|
|
}
|
|
|
|
file.flush().await.map_err(|e| {
|
|
log::error!("Failed to flush file: {e}");
|
|
format!("File flush error: {e}")
|
|
})?;
|
|
|
|
log::info!("Download completed: {downloaded} bytes");
|
|
|
|
if let Err(e) = app.emit("update-download-completed", ()) {
|
|
log::error!("Failed to emit download completed event: {e}");
|
|
}
|
|
|
|
Ok(file_path.to_string_lossy().to_string())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn install_update_and_restart(
|
|
app: AppHandle,
|
|
installer_path: String,
|
|
) -> Result<(), String> {
|
|
log::info!("Installing update from: {installer_path}");
|
|
|
|
let path = PathBuf::from(&installer_path);
|
|
|
|
if !path.exists() {
|
|
let error_msg = "Installer file not found".to_string();
|
|
log::error!("{error_msg}");
|
|
return Err(error_msg);
|
|
}
|
|
|
|
if let Err(e) = app.emit("update-install-started", ()) {
|
|
log::error!("Failed to emit install started event: {e}");
|
|
}
|
|
|
|
let result = match env::consts::OS {
|
|
"windows" => install_windows_update(&path).await,
|
|
"macos" => install_macos_update(&path).await,
|
|
"linux" => install_linux_update(&path).await,
|
|
_ => Err("Unsupported platform for auto-update".to_string()),
|
|
};
|
|
|
|
match result {
|
|
Ok(_) => {
|
|
log::info!("Update installation started successfully");
|
|
|
|
if let Err(e) = app.emit("update-install-completed", ()) {
|
|
log::error!("Failed to emit install completed event: {e}");
|
|
}
|
|
|
|
if let Err(e) = app.emit("app-restarting", ()) {
|
|
log::error!("Failed to emit app restarting event: {e}");
|
|
}
|
|
|
|
tokio::time::sleep(Duration::from_millis(1000)).await;
|
|
std::process::exit(0);
|
|
}
|
|
Err(e) => {
|
|
log::error!("Update installation failed: {e}");
|
|
if let Err(emit_err) = app.emit("update-install-error", &e) {
|
|
log::error!("Failed to emit install error event: {emit_err}");
|
|
}
|
|
Err(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn install_windows_update(installer_path: &PathBuf) -> Result<(), String> {
|
|
log::info!("Installing Windows update...");
|
|
|
|
let mut cmd = Command::new(installer_path);
|
|
cmd.arg("/SILENT");
|
|
|
|
#[cfg(windows)]
|
|
{
|
|
use std::os::windows::process::CommandExt;
|
|
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
|
|
}
|
|
|
|
let _ = tokio::task::spawn_blocking(move || {
|
|
cmd.spawn()
|
|
.map_err(|e| format!("Failed to start Windows installer: {e}"))
|
|
})
|
|
.await
|
|
.map_err(|e| format!("Task error: {e}"))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn install_macos_update(installer_path: &PathBuf) -> Result<(), String> {
|
|
log::info!("Installing macOS update...");
|
|
|
|
let mut cmd = Command::new("open");
|
|
cmd.arg(installer_path);
|
|
|
|
let _ = tokio::task::spawn_blocking(move || {
|
|
cmd.spawn()
|
|
.map_err(|e| format!("Failed to start macOS installer: {e}"))
|
|
})
|
|
.await
|
|
.map_err(|e| format!("Task error: {e}"))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn install_linux_update(installer_path: &PathBuf) -> Result<(), String> {
|
|
log::info!("Installing Linux update...");
|
|
|
|
let extension = installer_path
|
|
.extension()
|
|
.and_then(|s| s.to_str())
|
|
.unwrap_or("");
|
|
|
|
let mut cmd = match extension {
|
|
"deb" => {
|
|
let mut cmd = Command::new("pkexec");
|
|
cmd.args(["dpkg", "-i"]);
|
|
cmd.arg(installer_path);
|
|
cmd
|
|
}
|
|
"rpm" => {
|
|
let mut cmd = Command::new("pkexec");
|
|
cmd.args(["rpm", "-i"]);
|
|
cmd.arg(installer_path);
|
|
cmd
|
|
}
|
|
_ => {
|
|
return Err("Unsupported Linux package format".to_string());
|
|
}
|
|
};
|
|
|
|
let _ = tokio::task::spawn_blocking(move || {
|
|
cmd.spawn()
|
|
.map_err(|e| format!("Failed to start Linux installer: {e}"))
|
|
})
|
|
.await
|
|
.map_err(|e| format!("Task error: {e}"))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn get_current_version() -> Result<String, String> {
|
|
Ok(env!("CARGO_PKG_VERSION").to_string())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn set_auto_check_enabled(
|
|
enabled: bool,
|
|
state: State<'_, AppState>,
|
|
) -> Result<(), String> {
|
|
log::info!("Setting auto-check updates preference to: {enabled}");
|
|
|
|
let mut settings = state.get_settings().unwrap_or_else(|| {
|
|
use crate::conf::config::MergedSettings;
|
|
MergedSettings::default()
|
|
});
|
|
|
|
settings.app.auto_update_enabled = Some(enabled);
|
|
state.update_settings(settings.clone());
|
|
save_settings(settings, state)
|
|
.await
|
|
.map_err(|e| format!("Failed to save settings: {e}"))?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn is_auto_check_enabled(state: State<'_, AppState>) -> Result<bool, String> {
|
|
let settings = state.get_settings().unwrap_or_else(|| {
|
|
use crate::conf::config::MergedSettings;
|
|
MergedSettings::default()
|
|
});
|
|
|
|
Ok(settings.app.auto_update_enabled.unwrap_or(true))
|
|
}
|
|
|
|
pub async fn perform_background_update_check(app: AppHandle) -> Result<(), String> {
|
|
log::debug!("Performing background update check...");
|
|
|
|
match check_for_updates().await {
|
|
Ok(update_check) => {
|
|
if update_check.has_update {
|
|
log::info!(
|
|
"Background check: Update available {} -> {}",
|
|
update_check.current_version,
|
|
update_check.latest_version
|
|
);
|
|
|
|
if let Err(e) = app.emit("background-update-available", &update_check) {
|
|
log::error!("Failed to emit background-update-available event: {e}");
|
|
}
|
|
} else {
|
|
log::error!("Background check: App is up to date");
|
|
}
|
|
Ok(())
|
|
}
|
|
Err(e) => {
|
|
log::error!("Background update check failed: {e}");
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn restart_app(app: AppHandle) {
|
|
log::info!("Restarting application...");
|
|
|
|
if let Err(e) = app.emit("app-restarting", ()) {
|
|
log::error!("Failed to emit app-restarting event: {e}");
|
|
}
|
|
|
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
|
|
|
app.restart();
|
|
}
|