From f093d2a310627aa3ee5a2820339f8a18bd251e81 Mon Sep 17 00:00:00 2001 From: "Begonia, HE" <163421589+BegoniaHe@users.noreply.github.com> Date: Wed, 14 Jan 2026 05:12:31 +0100 Subject: feat(java): integrate Adoptium API for Java runtime download Add automatic Java (Temurin) download and installation feature: - Add Adoptium API v3 integration to fetch latest Java releases - Support JRE and JDK image types with version selection (8/11/17/21) - Implement platform detection for macOS, Linux, and Windows - Add SHA256 checksum verification for downloaded archives - Add tar.gz extraction support with Unix permission preservation - Handle macOS-specific Java path structure (Contents/Home/bin) - Add frontend UI with version selector and download progress - Register Tauri commands: fetch_adoptium_java, download_adoptium_java, fetch_available_java_versions Dependencies added: sha2, flate2, tar, dirs --- src-tauri/src/core/downloader.rs | 44 +++- src-tauri/src/core/java.rs | 419 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 456 insertions(+), 7 deletions(-) (limited to 'src-tauri/src/core') diff --git a/src-tauri/src/core/downloader.rs b/src-tauri/src/core/downloader.rs index 5f6ec80..09101c9 100644 --- a/src-tauri/src/core/downloader.rs +++ b/src-tauri/src/core/downloader.rs @@ -1,5 +1,6 @@ use futures::StreamExt; use serde::{Deserialize, Serialize}; +use sha1::Digest as Sha1Digest; use std::path::PathBuf; use std::sync::Arc; use tauri::{Emitter, Window}; @@ -10,7 +11,10 @@ use tokio::sync::Semaphore; pub struct DownloadTask { pub url: String, pub path: PathBuf, + #[serde(default)] pub sha1: Option, + #[serde(default)] + pub sha256: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -21,6 +25,32 @@ pub struct ProgressEvent { pub status: String, // "Downloading", "Verifying", "Finished", "Error" } +/// calculate SHA256 hash of data +pub fn compute_sha256(data: &[u8]) -> String { + let mut hasher = sha2::Sha256::new(); + hasher.update(data); + hex::encode(hasher.finalize()) +} + +/// calculate SHA1 hash of data +pub fn compute_sha1(data: &[u8]) -> String { + let mut hasher = sha1::Sha1::new(); + hasher.update(data); + hex::encode(hasher.finalize()) +} + +/// verify file checksum, prefer SHA256, fallback to SHA1 +pub fn verify_checksum(data: &[u8], sha256: Option<&str>, sha1: Option<&str>) -> bool { + if let Some(expected) = sha256 { + return compute_sha256(data) == expected; + } + if let Some(expected) = sha1 { + return compute_sha1(data) == expected; + } + // No checksum provided, default to true + true +} + pub async fn download_files(window: Window, tasks: Vec) -> Result<(), String> { let client = reqwest::Client::new(); let semaphore = Arc::new(Semaphore::new(10)); // Max 10 concurrent downloads @@ -37,7 +67,7 @@ pub async fn download_files(window: Window, tasks: Vec) -> Result< let _permit = semaphore.acquire().await.unwrap(); let file_name = task.path.file_name().unwrap().to_string_lossy().to_string(); - // 1. Check if file exists and verify SHA1 + // 1. Check if file exists and verify checksum if task.path.exists() { let _ = window.emit( "download-progress", @@ -49,13 +79,13 @@ pub async fn download_files(window: Window, tasks: Vec) -> Result< }, ); - if let Some(expected_sha1) = &task.sha1 { + if task.sha256.is_some() || task.sha1.is_some() { if let Ok(data) = tokio::fs::read(&task.path).await { - let mut hasher = sha1::Sha1::new(); - use sha1::Digest; - hasher.update(&data); - let result = hex::encode(hasher.finalize()); - if &result == expected_sha1 { + if verify_checksum( + &data, + task.sha256.as_deref(), + task.sha1.as_deref(), + ) { // Already valid let _ = window.emit( "download-progress", diff --git a/src-tauri/src/core/java.rs b/src-tauri/src/core/java.rs index e0962fa..7ef6998 100644 --- a/src-tauri/src/core/java.rs +++ b/src-tauri/src/core/java.rs @@ -2,6 +2,9 @@ use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::process::Command; +use crate::core::downloader; +use crate::utils::zip; + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct JavaInstallation { pub path: String, @@ -9,6 +12,342 @@ pub struct JavaInstallation { pub is_64bit: bool, } +/// Java image type: JRE or JDK +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ImageType { + Jre, + Jdk, +} + +impl Default for ImageType { + fn default() -> Self { + Self::Jre + } +} + +impl std::fmt::Display for ImageType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Jre => write!(f, "jre"), + Self::Jdk => write!(f, "jdk"), + } + } +} + +/// Adoptium `/v3/assets/latest/{version}/hotspot` API response structures +#[derive(Debug, Clone, Deserialize)] +pub struct AdoptiumAsset { + pub binary: AdoptiumBinary, + pub release_name: String, + pub version: AdoptiumVersionData, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct AdoptiumBinary { + pub os: String, + pub architecture: String, + pub image_type: String, + pub package: AdoptiumPackage, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct AdoptiumPackage { + pub name: String, + pub link: String, + pub size: u64, + pub checksum: Option, // SHA256 +} + +#[derive(Debug, Clone, Deserialize)] +pub struct AdoptiumVersionData { + pub major: u32, + pub minor: u32, + pub security: u32, + pub semver: String, + pub openjdk_version: String, +} + +/// Java download information from Adoptium +#[derive(Debug, Clone, Serialize)] +pub struct JavaDownloadInfo { + pub version: String, + pub release_name: String, + pub download_url: String, + pub file_name: String, + pub file_size: u64, + pub checksum: Option, + pub image_type: String, +} + +/// Get the Adoptium OS name for the current platform +pub fn get_adoptium_os() -> &'static str { + #[cfg(target_os = "linux")] + { + // Check if Alpine Linux (musl libc) + if std::path::Path::new("/etc/alpine-release").exists() { + return "alpine-linux"; + } + "linux" + } + #[cfg(target_os = "macos")] + { + "mac" + } + #[cfg(target_os = "windows")] + { + "windows" + } + #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] + { + "linux" // fallback + } +} + +/// Get the Adoptium Architecture name for the current architecture +pub fn get_adoptium_arch() -> &'static str { + #[cfg(target_arch = "x86_64")] + { + "x64" + } + #[cfg(target_arch = "aarch64")] + { + "aarch64" + } + #[cfg(target_arch = "x86")] + { + "x86" + } + #[cfg(target_arch = "arm")] + { + "arm" + } + #[cfg(not(any( + target_arch = "x86_64", + target_arch = "aarch64", + target_arch = "x86", + target_arch = "arm" + )))] + { + "x64" // fallback + } +} + +/// Get the default Java installation directory for DropOut +pub fn get_java_install_dir() -> PathBuf { + dirs::home_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join(".dropout") + .join("java") +} + +/// Get Adoptium API download info for a specific Java version and image type +/// +/// # Arguments +/// * `major_version` - Java major version (e.g., 8, 11, 17) +/// * `image_type` - JRE or JDK +/// +/// # Returns +/// * `Ok(JavaDownloadInfo)` - Download information +/// * `Err(String)` - Error message +pub async fn fetch_java_release( + major_version: u32, + image_type: ImageType, +) -> Result { + let os = get_adoptium_os(); + let arch = get_adoptium_arch(); + + let url = format!( + "https://api.adoptium.net/v3/assets/latest/{}/hotspot?os={}&architecture={}&image_type={}", + major_version, os, arch, image_type + ); + + let client = reqwest::Client::new(); + let response = client + .get(&url) + .header("Accept", "application/json") + .send() + .await + .map_err(|e| format!("Network request failed: {}", e))?; + + if !response.status().is_success() { + return Err(format!( + "Adoptium API returned error: {} - The version/platform might be unavailable", + response.status() + )); + } + + let assets: Vec = response + .json() + .await + .map_err(|e| format!("Failed to parse API response: {}", e))?; + + let asset = assets + .into_iter() + .next() + .ok_or_else(|| format!("Java {} {} download not found", major_version, image_type))?; + + Ok(JavaDownloadInfo { + version: asset.version.semver.clone(), + release_name: asset.release_name, + download_url: asset.binary.package.link, + file_name: asset.binary.package.name, + file_size: asset.binary.package.size, + checksum: asset.binary.package.checksum, + image_type: asset.binary.image_type, + }) +} + +/// Fetch available Java versions from Adoptium API +pub async fn fetch_available_versions() -> Result, String> { + let url = "https://api.adoptium.net/v3/info/available_releases"; + + let response = reqwest::get(url) + .await + .map_err(|e| format!("Network request failed: {}", e))?; + + #[derive(Deserialize)] + struct AvailableReleases { + available_releases: Vec, + } + + let releases: AvailableReleases = response + .json() + .await + .map_err(|e| format!("Failed to parse response: {}", e))?; + + Ok(releases.available_releases) +} + +/// Download and install Java +/// +/// # Arguments +/// * `major_version` - Java major version (e.g., 8, 11, 17) +/// * `image_type` - JRE or JDK +/// * `custom_path` - Optional custom installation path +/// +/// # Returns +/// * `Ok(JavaInstallation)` - Information about the successfully installed Java +pub async fn download_and_install_java( + major_version: u32, + image_type: ImageType, + custom_path: Option, +) -> Result { + // 1. Fetch download information + let info = fetch_java_release(major_version, image_type).await?; + + // 2. Prepare installation directory + let install_base = custom_path.unwrap_or_else(get_java_install_dir); + let version_dir = install_base.join(format!("temurin-{}-{}", major_version, image_type)); + + std::fs::create_dir_all(&install_base) + .map_err(|e| format!("Failed to create installation directory: {}", e))?; + + // 3. Download the archive + let archive_path = install_base.join(&info.file_name); + + // Check if we need to download + let need_download = if archive_path.exists() { + if let Some(expected_checksum) = &info.checksum { + let data = std::fs::read(&archive_path) + .map_err(|e| format!("Failed to read downloaded file: {}", e))?; + !downloader::verify_checksum(&data, Some(expected_checksum), None) + } else { + false + } + } else { + true + }; + + if need_download { + let client = reqwest::Client::new(); + let response = client + .get(&info.download_url) + .send() + .await + .map_err(|e| format!("Download failed: {}", e))?; + + let bytes = response + .bytes() + .await + .map_err(|e| format!("Failed to read download content: {}", e))?; + + // Verify downloaded file checksum + if let Some(expected) = &info.checksum { + if !downloader::verify_checksum(&bytes, Some(expected), None) { + return Err("Downloaded file verification failed, the file may be corrupted".to_string()); + } + } + + std::fs::write(&archive_path, &bytes) + .map_err(|e| format!("Failed to save downloaded file: {}", e))?; + } + + // 4. Extract + // If the target directory exists, remove it first + if version_dir.exists() { + std::fs::remove_dir_all(&version_dir) + .map_err(|e| format!("Failed to remove old version directory: {}", e))?; + } + + std::fs::create_dir_all(&version_dir) + .map_err(|e| format!("Failed to create version directory: {}", e))?; + + let top_level_dir = if info.file_name.ends_with(".tar.gz") || info.file_name.ends_with(".tgz") { + zip::extract_tar_gz(&archive_path, &version_dir)? + } else if info.file_name.ends_with(".zip") { + zip::extract_zip(&archive_path, &version_dir)?; + // Find the top-level directory inside the extracted folder + find_top_level_dir(&version_dir)? + } else { + return Err(format!("Unsupported archive format: {}", info.file_name)); + }; + + // 5. Clean up downloaded archive + let _ = std::fs::remove_file(&archive_path); + + // 6. Locate java executable + // macOS has a different structure: jdk-xxx/Contents/Home/bin/java + // Linux/Windows: jdk-xxx/bin/java + let java_home = version_dir.join(&top_level_dir); + let java_bin = if cfg!(target_os = "macos") { + java_home.join("Contents").join("Home").join("bin").join("java") + } else if cfg!(windows) { + java_home.join("bin").join("java.exe") + } else { + java_home.join("bin").join("java") + }; + + if !java_bin.exists() { + return Err(format!( + "Installation completed but Java executable not found: {}", + java_bin.display() + )); + } + + // 7. Verify installation + let installation = check_java_installation(&java_bin) + .ok_or_else(|| "Fail to verify Java installation".to_string())?; + + Ok(installation) +} + +/// Find the top-level directory inside the extracted folder +fn find_top_level_dir(extract_dir: &PathBuf) -> Result { + let entries: Vec<_> = std::fs::read_dir(extract_dir) + .map_err(|e| format!("Failed to read directory: {}", e))? + .filter_map(|e| e.ok()) + .filter(|e| e.path().is_dir()) + .collect(); + + if entries.len() == 1 { + Ok(entries[0].file_name().to_string_lossy().to_string()) + } else { + // No single top-level directory, return empty string + Ok(String::new()) + } +} + /// Detect Java installations on the system pub fn detect_java_installations() -> Vec { let mut installations = Vec::new(); @@ -252,3 +591,83 @@ pub fn get_recommended_java(required_major_version: Option) -> Option Vec { + let mut installations = detect_java_installations(); + + // Add DropOut downloaded Java versions + let dropout_java_dir = get_java_install_dir(); + if dropout_java_dir.exists() { + if let Ok(entries) = std::fs::read_dir(&dropout_java_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + // Find the java executable in this directory + let java_bin = find_java_executable(&path); + if let Some(java_path) = java_bin { + if let Some(java) = check_java_installation(&java_path) { + if !installations.iter().any(|j| j.path == java.path) { + installations.push(java); + } + } + } + } + } + } + } + + // Sort by version + installations.sort_by(|a, b| { + let v_a = parse_java_version(&a.version); + let v_b = parse_java_version(&b.version); + v_b.cmp(&v_a) + }); + + installations +} + +/// Recursively find the java executable in a directory +fn find_java_executable(dir: &PathBuf) -> Option { + let bin_name = if cfg!(windows) { "java.exe" } else { "java" }; + + // Directly look in the bin directory + let direct_bin = dir.join("bin").join(bin_name); + if direct_bin.exists() { + return Some(direct_bin); + } + + // macOS: Contents/Home/bin/java + #[cfg(target_os = "macos")] + { + let macos_bin = dir.join("Contents").join("Home").join("bin").join(bin_name); + if macos_bin.exists() { + return Some(macos_bin); + } + } + + // Look in subdirectories (handle nested directories after Adoptium extraction) + if let Ok(entries) = std::fs::read_dir(dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + // Try direct bin path + let nested_bin = path.join("bin").join(bin_name); + if nested_bin.exists() { + return Some(nested_bin); + } + + // macOS: nested/Contents/Home/bin/java + #[cfg(target_os = "macos")] + { + let macos_nested = path.join("Contents").join("Home").join("bin").join(bin_name); + if macos_nested.exists() { + return Some(macos_nested); + } + } + } + } + } + + None +} -- cgit v1.2.3-70-g09d2 From 2cb21f2bbc601ae134095cf0e68b5bcc6966d227 Mon Sep 17 00:00:00 2001 From: "Begonia, HE" <163421589+BegoniaHe@users.noreply.github.com> Date: Wed, 14 Jan 2026 05:54:03 +0100 Subject: Update src-tauri/src/core/java.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src-tauri/src/core/java.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src-tauri/src/core') diff --git a/src-tauri/src/core/java.rs b/src-tauri/src/core/java.rs index 7ef6998..a622d60 100644 --- a/src-tauri/src/core/java.rs +++ b/src-tauri/src/core/java.rs @@ -327,7 +327,7 @@ pub async fn download_and_install_java( // 7. Verify installation let installation = check_java_installation(&java_bin) - .ok_or_else(|| "Fail to verify Java installation".to_string())?; + .ok_or_else(|| "Failed to verify Java installation".to_string())?; Ok(installation) } -- cgit v1.2.3-70-g09d2 From d9cde37560f13f44862b5ee996973842b86725d2 Mon Sep 17 00:00:00 2001 From: 简律纯 Date: Thu, 15 Jan 2026 10:39:48 +0800 Subject: Apply suggestion from @SourceryAI Co-authored-by: SourceryAI --- src-tauri/src/core/java.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src-tauri/src/core') diff --git a/src-tauri/src/core/java.rs b/src-tauri/src/core/java.rs index b223cd2..393de34 100644 --- a/src-tauri/src/core/java.rs +++ b/src-tauri/src/core/java.rs @@ -631,7 +631,7 @@ pub fn detect_all_java_installations() -> Vec { installations } -/// Recursively find the java executable in a directory +//// Find the java executable in a directory using a limited-depth search fn find_java_executable(dir: &PathBuf) -> Option { let bin_name = if cfg!(windows) { "java.exe" } else { "java" }; -- cgit v1.2.3-70-g09d2 From 1b3c84b0c78ea438c8f446054af196c620d30602 Mon Sep 17 00:00:00 2001 From: "Begonia, HE" <163421589+BegoniaHe@users.noreply.github.com> Date: Thu, 15 Jan 2026 04:24:14 +0100 Subject: fix: change Java installation path to use Tauri app handle for directory access --- src-tauri/src/core/java.rs | 17 +++++++++-------- src-tauri/src/main.rs | 7 ++++--- 2 files changed, 13 insertions(+), 11 deletions(-) (limited to 'src-tauri/src/core') diff --git a/src-tauri/src/core/java.rs b/src-tauri/src/core/java.rs index 393de34..a8fdeea 100644 --- a/src-tauri/src/core/java.rs +++ b/src-tauri/src/core/java.rs @@ -1,6 +1,8 @@ use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::process::Command; +use tauri::AppHandle; +use tauri::Manager; use crate::core::downloader; use crate::utils::zip; @@ -134,11 +136,8 @@ pub fn get_adoptium_arch() -> &'static str { } /// Get the default Java installation directory for DropOut -pub fn get_java_install_dir() -> PathBuf { - dirs::home_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join(".dropout") - .join("java") +pub fn get_java_install_dir(app_handle: &AppHandle) -> PathBuf { + app_handle.path().app_data_dir().unwrap().join("java") } /// Get Adoptium API download info for a specific Java version and image type @@ -222,6 +221,7 @@ pub async fn fetch_available_versions() -> Result, String> { /// Download and install Java /// /// # Arguments +/// * `app_handle` - Tauri app handle for accessing app directories /// * `major_version` - Java major version (e.g., 8, 11, 17) /// * `image_type` - JRE or JDK /// * `custom_path` - Optional custom installation path @@ -229,6 +229,7 @@ pub async fn fetch_available_versions() -> Result, String> { /// # Returns /// * `Ok(JavaInstallation)` - Information about the successfully installed Java pub async fn download_and_install_java( + app_handle: &AppHandle, major_version: u32, image_type: ImageType, custom_path: Option, @@ -237,7 +238,7 @@ pub async fn download_and_install_java( let info = fetch_java_release(major_version, image_type).await?; // 2. Prepare installation directory - let install_base = custom_path.unwrap_or_else(get_java_install_dir); + let install_base = custom_path.unwrap_or_else(|| get_java_install_dir(app_handle)); let version_dir = install_base.join(format!("temurin-{}-{}", major_version, image_type)); std::fs::create_dir_all(&install_base) @@ -597,11 +598,11 @@ pub fn get_recommended_java(required_major_version: Option) -> Option Vec { +pub fn detect_all_java_installations(app_handle: &AppHandle) -> Vec { let mut installations = detect_java_installations(); // Add DropOut downloaded Java versions - let dropout_java_dir = get_java_install_dir(); + let dropout_java_dir = get_java_install_dir(app_handle); if dropout_java_dir.exists() { if let Ok(entries) = std::fs::read_dir(&dropout_java_dir) { for entry in entries.flatten() { diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 2632924..a2b8c6c 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -848,8 +848,8 @@ async fn refresh_account( /// Detect Java installations on the system #[tauri::command] -async fn detect_java() -> Result, String> { - Ok(core::java::detect_all_java_installations()) +async fn detect_java(app_handle: tauri::AppHandle) -> Result, String> { + Ok(core::java::detect_all_java_installations(&app_handle)) } /// Get recommended Java for a specific Minecraft version @@ -876,6 +876,7 @@ async fn fetch_adoptium_java( /// Download and install Adoptium Java #[tauri::command] async fn download_adoptium_java( + app_handle: tauri::AppHandle, major_version: u32, image_type: String, custom_path: Option, @@ -885,7 +886,7 @@ async fn download_adoptium_java( _ => core::java::ImageType::Jre, }; let path = custom_path.map(std::path::PathBuf::from); - core::java::download_and_install_java(major_version, img_type, path).await + core::java::download_and_install_java(&app_handle, major_version, img_type, path).await } /// Get available Adoptium Java versions -- cgit v1.2.3-70-g09d2 From 43a3e9c285f3d5d04fef025041a06609a0d1c218 Mon Sep 17 00:00:00 2001 From: "Begonia, HE" <163421589+BegoniaHe@users.noreply.github.com> Date: Thu, 15 Jan 2026 05:29:58 +0100 Subject: feat(java): Implement Java catalog management and download features - Added commands to fetch and refresh the Java catalog, cancel downloads, and manage pending downloads. - Enhanced the Java download modal in the UI to support version selection, download progress, and pending downloads. - Introduced new types for Java catalog, download progress, and pending downloads. - Updated settings store to handle Java catalog state, download progress, and pending downloads. - Improved user experience with loading states, error handling, and status notifications for Java installations. --- src-tauri/Cargo.toml | 2 +- src-tauri/src/core/downloader.rs | 400 +++++++++++++++++++++++++++++++++- src-tauri/src/core/java.rs | 369 ++++++++++++++++++++++++++++--- src-tauri/src/main.rs | 52 +++++ ui/src/App.svelte | 23 +- ui/src/components/SettingsView.svelte | 375 ++++++++++++++++++++++++++----- ui/src/stores/settings.svelte.ts | 294 ++++++++++++++++++++++--- ui/src/types/index.ts | 44 ++++ 8 files changed, 1429 insertions(+), 130 deletions(-) (limited to 'src-tauri/src/core') diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index bc831fb..27a0e0c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -13,7 +13,7 @@ toml = "0.5" log = "0.4" env_logger = "0.9" tokio = { version = "1.49.0", features = ["full"] } -reqwest = { version = "0.13.1", features = ["json", "blocking"] } +reqwest = { version = "0.13.1", features = ["json", "blocking", "stream"] } serde_json = "1.0.149" tauri = { version = "2.9", features = [] } tauri-plugin-shell = "2.3" diff --git a/src-tauri/src/core/downloader.rs b/src-tauri/src/core/downloader.rs index d33c44d..bf6334f 100644 --- a/src-tauri/src/core/downloader.rs +++ b/src-tauri/src/core/downloader.rs @@ -2,10 +2,10 @@ use futures::StreamExt; use serde::{Deserialize, Serialize}; use sha1::Digest as Sha1Digest; use std::path::PathBuf; -use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; use std::sync::Arc; -use tauri::{Emitter, Window}; -use tokio::io::AsyncWriteExt; +use tauri::{AppHandle, Emitter, Manager, Window}; +use tokio::io::{AsyncSeekExt, AsyncWriteExt}; use tokio::sync::Semaphore; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -18,6 +18,400 @@ pub struct DownloadTask { pub sha256: Option, } +/// Metadata for resumable downloads stored in .part.meta file +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DownloadMetadata { + pub url: String, + pub file_name: String, + pub total_size: u64, + pub downloaded_bytes: u64, + pub checksum: Option, + pub timestamp: u64, + pub segments: Vec, +} + +/// A download segment for multi-segment parallel downloading +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DownloadSegment { + pub start: u64, + pub end: u64, + pub downloaded: u64, + pub completed: bool, +} + +/// Progress event for Java download +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JavaDownloadProgress { + pub file_name: String, + pub downloaded_bytes: u64, + pub total_bytes: u64, + pub speed_bytes_per_sec: u64, + pub eta_seconds: u64, + pub status: String, // "Downloading", "Extracting", "Verifying", "Completed", "Paused", "Error" + pub percentage: f32, +} + +/// Pending download task for queue persistence +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PendingJavaDownload { + pub major_version: u32, + pub image_type: String, + pub download_url: String, + pub file_name: String, + pub file_size: u64, + pub checksum: Option, + pub install_path: String, + pub created_at: u64, +} + +/// Download queue for persistence +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct DownloadQueue { + pub pending_downloads: Vec, +} + +impl DownloadQueue { + /// Load download queue from file + pub fn load(app_handle: &AppHandle) -> Self { + let queue_path = app_handle + .path() + .app_data_dir() + .unwrap() + .join("download_queue.json"); + if queue_path.exists() { + if let Ok(content) = std::fs::read_to_string(&queue_path) { + if let Ok(queue) = serde_json::from_str(&content) { + return queue; + } + } + } + Self::default() + } + + /// Save download queue to file + pub fn save(&self, app_handle: &AppHandle) -> Result<(), String> { + let queue_path = app_handle + .path() + .app_data_dir() + .unwrap() + .join("download_queue.json"); + let content = serde_json::to_string_pretty(self).map_err(|e| e.to_string())?; + std::fs::write(&queue_path, content).map_err(|e| e.to_string())?; + Ok(()) + } + + /// Add a pending download + pub fn add(&mut self, download: PendingJavaDownload) { + // Remove existing download for same version/type + self.pending_downloads.retain(|d| { + !(d.major_version == download.major_version && d.image_type == download.image_type) + }); + self.pending_downloads.push(download); + } + + /// Remove a completed or cancelled download + pub fn remove(&mut self, major_version: u32, image_type: &str) { + self.pending_downloads.retain(|d| { + !(d.major_version == major_version && d.image_type == image_type) + }); + } +} + +/// Global cancel flag for Java downloads +pub static JAVA_DOWNLOAD_CANCELLED: AtomicBool = AtomicBool::new(false); + +/// Reset the cancel flag +pub fn reset_java_download_cancel() { + JAVA_DOWNLOAD_CANCELLED.store(false, Ordering::SeqCst); +} + +/// Cancel the current Java download +pub fn cancel_java_download() { + JAVA_DOWNLOAD_CANCELLED.store(true, Ordering::SeqCst); +} + +/// Check if download is cancelled +pub fn is_java_download_cancelled() -> bool { + JAVA_DOWNLOAD_CANCELLED.load(Ordering::SeqCst) +} + +/// Determine optimal segment count based on file size +fn get_segment_count(file_size: u64) -> usize { + if file_size < 20 * 1024 * 1024 { + 1 // < 20MB: single segment + } else if file_size < 100 * 1024 * 1024 { + 4 // 20-100MB: 4 segments + } else { + 8 // > 100MB: 8 segments + } +} + +/// Download a large file with resume support and progress events +pub async fn download_with_resume( + app_handle: &AppHandle, + url: &str, + dest_path: &PathBuf, + checksum: Option<&str>, + total_size: u64, +) -> Result<(), String> { + reset_java_download_cancel(); + + let part_path = dest_path.with_extension( + dest_path + .extension() + .map(|e| format!("{}.part", e.to_string_lossy())) + .unwrap_or_else(|| "part".to_string()), + ); + let meta_path = PathBuf::from(format!("{}.meta", part_path.display())); + let file_name = dest_path + .file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + + // Load or create metadata + let mut metadata = if meta_path.exists() { + let content = tokio::fs::read_to_string(&meta_path) + .await + .map_err(|e| e.to_string())?; + serde_json::from_str(&content).unwrap_or_else(|_| create_new_metadata(url, &file_name, total_size, checksum)) + } else { + create_new_metadata(url, &file_name, total_size, checksum) + }; + + // Create parent directory + if let Some(parent) = dest_path.parent() { + tokio::fs::create_dir_all(parent) + .await + .map_err(|e| e.to_string())?; + } + + // Open or create part file + let file = tokio::fs::OpenOptions::new() + .create(true) + .write(true) + .read(true) + .open(&part_path) + .await + .map_err(|e| format!("Failed to open part file: {}", e))?; + + let file = Arc::new(tokio::sync::Mutex::new(file)); + let client = reqwest::Client::new(); + let progress = Arc::new(AtomicU64::new(metadata.downloaded_bytes)); + let start_time = std::time::Instant::now(); + let last_progress_bytes = Arc::new(AtomicU64::new(metadata.downloaded_bytes)); + + // Download segments concurrently + let segment_count = metadata.segments.len(); + let semaphore = Arc::new(Semaphore::new(segment_count.min(8))); + let mut handles = Vec::new(); + + for (idx, segment) in metadata.segments.iter().enumerate() { + if segment.completed { + continue; + } + + let client = client.clone(); + let url = url.to_string(); + let file = file.clone(); + let progress = progress.clone(); + let semaphore = semaphore.clone(); + let segment_start = segment.start + segment.downloaded; + let segment_end = segment.end; + let app_handle = app_handle.clone(); + let file_name = file_name.clone(); + let total_size = total_size; + let last_progress_bytes = last_progress_bytes.clone(); + let start_time = start_time.clone(); + + let handle = tokio::spawn(async move { + let _permit = semaphore.acquire().await.unwrap(); + + if is_java_download_cancelled() { + return Err("Download cancelled".to_string()); + } + + // Send Range request + let range = format!("bytes={}-{}", segment_start, segment_end); + let response = client + .get(&url) + .header("Range", &range) + .send() + .await + .map_err(|e| format!("Request failed: {}", e))?; + + if !response.status().is_success() && response.status() != reqwest::StatusCode::PARTIAL_CONTENT { + return Err(format!("Server returned error: {}", response.status())); + } + + let mut stream = response.bytes_stream(); + let mut current_pos = segment_start; + + while let Some(chunk_result) = stream.next().await { + if is_java_download_cancelled() { + return Err("Download cancelled".to_string()); + } + + let chunk = chunk_result.map_err(|e| format!("Stream error: {}", e))?; + let chunk_len = chunk.len() as u64; + + // Write to file at correct position + { + let mut file_guard = file.lock().await; + file_guard + .seek(std::io::SeekFrom::Start(current_pos)) + .await + .map_err(|e| format!("Seek error: {}", e))?; + file_guard + .write_all(&chunk) + .await + .map_err(|e| format!("Write error: {}", e))?; + } + + current_pos += chunk_len; + let total_downloaded = progress.fetch_add(chunk_len, Ordering::Relaxed) + chunk_len; + + // Emit progress event (throttled) + let last_bytes = last_progress_bytes.load(Ordering::Relaxed); + if total_downloaded - last_bytes > 100 * 1024 || total_downloaded >= total_size { + last_progress_bytes.store(total_downloaded, Ordering::Relaxed); + + let elapsed = start_time.elapsed().as_secs_f64(); + let speed = if elapsed > 0.0 { + (total_downloaded as f64 / elapsed) as u64 + } else { + 0 + }; + let remaining = total_size.saturating_sub(total_downloaded); + let eta = if speed > 0 { remaining / speed } else { 0 }; + let percentage = (total_downloaded as f32 / total_size as f32) * 100.0; + + let _ = app_handle.emit( + "java-download-progress", + JavaDownloadProgress { + file_name: file_name.clone(), + downloaded_bytes: total_downloaded, + total_bytes: total_size, + speed_bytes_per_sec: speed, + eta_seconds: eta, + status: "Downloading".to_string(), + percentage, + }, + ); + } + } + + Ok::(idx) + }); + + handles.push(handle); + } + + // Wait for all segments + let mut all_success = true; + for handle in handles { + match handle.await { + Ok(Ok(idx)) => { + metadata.segments[idx].completed = true; + } + Ok(Err(e)) => { + all_success = false; + if e.contains("cancelled") { + // Save progress for resume + metadata.downloaded_bytes = progress.load(Ordering::Relaxed); + let meta_content = serde_json::to_string_pretty(&metadata).map_err(|e| e.to_string())?; + tokio::fs::write(&meta_path, meta_content).await.ok(); + return Err(e); + } + } + Err(e) => { + all_success = false; + eprintln!("Segment task panicked: {}", e); + } + } + } + + if !all_success { + // Save progress + metadata.downloaded_bytes = progress.load(Ordering::Relaxed); + let meta_content = serde_json::to_string_pretty(&metadata).map_err(|e| e.to_string())?; + tokio::fs::write(&meta_path, meta_content).await.ok(); + return Err("Some segments failed".to_string()); + } + + // Verify checksum if provided + if let Some(expected) = checksum { + let _ = app_handle.emit( + "java-download-progress", + JavaDownloadProgress { + file_name: file_name.clone(), + downloaded_bytes: total_size, + total_bytes: total_size, + speed_bytes_per_sec: 0, + eta_seconds: 0, + status: "Verifying".to_string(), + percentage: 100.0, + }, + ); + + let data = tokio::fs::read(&part_path) + .await + .map_err(|e| format!("Failed to read file for verification: {}", e))?; + + if !verify_checksum(&data, Some(expected), None) { + // Checksum failed, delete files and retry + tokio::fs::remove_file(&part_path).await.ok(); + tokio::fs::remove_file(&meta_path).await.ok(); + return Err("Checksum verification failed".to_string()); + } + } + + // Rename part file to final destination + tokio::fs::rename(&part_path, dest_path) + .await + .map_err(|e| format!("Failed to rename file: {}", e))?; + + // Clean up metadata file + tokio::fs::remove_file(&meta_path).await.ok(); + + Ok(()) +} + +/// Create new download metadata with segments +fn create_new_metadata(url: &str, file_name: &str, total_size: u64, checksum: Option<&str>) -> DownloadMetadata { + let segment_count = get_segment_count(total_size); + let segment_size = total_size / segment_count as u64; + let mut segments = Vec::new(); + + for i in 0..segment_count { + let start = i as u64 * segment_size; + let end = if i == segment_count - 1 { + total_size - 1 + } else { + (i as u64 + 1) * segment_size - 1 + }; + segments.push(DownloadSegment { + start, + end, + downloaded: 0, + completed: false, + }); + } + + DownloadMetadata { + url: url.to_string(), + file_name: file_name.to_string(), + total_size, + downloaded_bytes: 0, + checksum: checksum.map(|s| s.to_string()), + timestamp: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + segments, + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProgressEvent { pub file: String, diff --git a/src-tauri/src/core/java.rs b/src-tauri/src/core/java.rs index a8fdeea..8341138 100644 --- a/src-tauri/src/core/java.rs +++ b/src-tauri/src/core/java.rs @@ -2,11 +2,15 @@ use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::process::Command; use tauri::AppHandle; +use tauri::Emitter; use tauri::Manager; -use crate::core::downloader; +use crate::core::downloader::{self, JavaDownloadProgress, DownloadQueue, PendingJavaDownload}; use crate::utils::zip; +const ADOPTIUM_API_BASE: &str = "https://api.adoptium.net/v3"; +const CACHE_DURATION_SECS: u64 = 24 * 60 * 60; // 24 hours + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct JavaInstallation { pub path: String, @@ -37,6 +41,42 @@ impl std::fmt::Display for ImageType { } } +/// Java release information for UI display +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JavaReleaseInfo { + pub major_version: u32, + pub image_type: String, + pub version: String, + pub release_name: String, + pub release_date: Option, + pub file_size: u64, + pub checksum: Option, + pub download_url: String, + pub is_lts: bool, + pub is_available: bool, + pub architecture: String, +} + +/// Java catalog containing all available versions +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JavaCatalog { + pub releases: Vec, + pub available_major_versions: Vec, + pub lts_versions: Vec, + pub cached_at: u64, +} + +impl Default for JavaCatalog { + fn default() -> Self { + Self { + releases: Vec::new(), + available_major_versions: Vec::new(), + lts_versions: Vec::new(), + cached_at: 0, + } + } +} + /// Adoptium `/v3/assets/latest/{version}/hotspot` API response structures #[derive(Debug, Clone, Deserialize)] pub struct AdoptiumAsset { @@ -51,6 +91,8 @@ pub struct AdoptiumBinary { pub architecture: String, pub image_type: String, pub package: AdoptiumPackage, + #[serde(default)] + pub updated_at: Option, } #[derive(Debug, Clone, Deserialize)] @@ -70,6 +112,15 @@ pub struct AdoptiumVersionData { pub openjdk_version: String, } +/// Adoptium available releases response +#[derive(Debug, Clone, Deserialize)] +pub struct AvailableReleases { + pub available_releases: Vec, + pub available_lts_releases: Vec, + pub most_recent_lts: Option, + pub most_recent_feature_release: Option, +} + /// Java download information from Adoptium #[derive(Debug, Clone, Serialize)] pub struct JavaDownloadInfo { @@ -140,6 +191,170 @@ pub fn get_java_install_dir(app_handle: &AppHandle) -> PathBuf { app_handle.path().app_data_dir().unwrap().join("java") } +/// Get the cache file path for Java catalog +fn get_catalog_cache_path(app_handle: &AppHandle) -> PathBuf { + app_handle + .path() + .app_data_dir() + .unwrap() + .join("java_catalog_cache.json") +} + +/// Load cached Java catalog if not expired +pub fn load_cached_catalog(app_handle: &AppHandle) -> Option { + let cache_path = get_catalog_cache_path(app_handle); + if !cache_path.exists() { + return None; + } + + let content = std::fs::read_to_string(&cache_path).ok()?; + let catalog: JavaCatalog = serde_json::from_str(&content).ok()?; + + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + if now - catalog.cached_at < CACHE_DURATION_SECS { + Some(catalog) + } else { + None + } +} + +/// Save Java catalog to cache +pub fn save_catalog_cache(app_handle: &AppHandle, catalog: &JavaCatalog) -> Result<(), String> { + let cache_path = get_catalog_cache_path(app_handle); + let content = serde_json::to_string_pretty(catalog).map_err(|e| e.to_string())?; + std::fs::write(&cache_path, content).map_err(|e| e.to_string())?; + Ok(()) +} + +/// Clear Java catalog cache +pub fn clear_catalog_cache(app_handle: &AppHandle) -> Result<(), String> { + let cache_path = get_catalog_cache_path(app_handle); + if cache_path.exists() { + std::fs::remove_file(&cache_path).map_err(|e| e.to_string())?; + } + Ok(()) +} + +/// Fetch complete Java catalog from Adoptium API with platform availability check +pub async fn fetch_java_catalog(app_handle: &AppHandle, force_refresh: bool) -> Result { + // Check cache first unless force refresh + if !force_refresh { + if let Some(cached) = load_cached_catalog(app_handle) { + return Ok(cached); + } + } + + let os = get_adoptium_os(); + let arch = get_adoptium_arch(); + let client = reqwest::Client::new(); + + // 1. Fetch available releases + let releases_url = format!("{}/info/available_releases", ADOPTIUM_API_BASE); + let available: AvailableReleases = client + .get(&releases_url) + .header("Accept", "application/json") + .send() + .await + .map_err(|e| format!("Failed to fetch available releases: {}", e))? + .json() + .await + .map_err(|e| format!("Failed to parse available releases: {}", e))?; + + let mut releases = Vec::new(); + + // 2. Fetch details for each major version + for major_version in &available.available_releases { + for image_type in &["jre", "jdk"] { + let url = format!( + "{}/assets/latest/{}/hotspot?os={}&architecture={}&image_type={}", + ADOPTIUM_API_BASE, major_version, os, arch, image_type + ); + + match client + .get(&url) + .header("Accept", "application/json") + .send() + .await + { + Ok(response) => { + if response.status().is_success() { + if let Ok(assets) = response.json::>().await { + if let Some(asset) = assets.into_iter().next() { + let release_date = asset.binary.updated_at.clone(); + releases.push(JavaReleaseInfo { + major_version: *major_version, + image_type: image_type.to_string(), + version: asset.version.semver.clone(), + release_name: asset.release_name.clone(), + release_date, + file_size: asset.binary.package.size, + checksum: asset.binary.package.checksum, + download_url: asset.binary.package.link, + is_lts: available.available_lts_releases.contains(major_version), + is_available: true, + architecture: asset.binary.architecture.clone(), + }); + } + } + } else { + // Platform not available for this version/type + releases.push(JavaReleaseInfo { + major_version: *major_version, + image_type: image_type.to_string(), + version: format!("{}.x", major_version), + release_name: format!("jdk-{}", major_version), + release_date: None, + file_size: 0, + checksum: None, + download_url: String::new(), + is_lts: available.available_lts_releases.contains(major_version), + is_available: false, + architecture: arch.to_string(), + }); + } + } + Err(_) => { + // Network error, mark as unavailable + releases.push(JavaReleaseInfo { + major_version: *major_version, + image_type: image_type.to_string(), + version: format!("{}.x", major_version), + release_name: format!("jdk-{}", major_version), + release_date: None, + file_size: 0, + checksum: None, + download_url: String::new(), + is_lts: available.available_lts_releases.contains(major_version), + is_available: false, + architecture: arch.to_string(), + }); + } + } + } + } + + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + let catalog = JavaCatalog { + releases, + available_major_versions: available.available_releases, + lts_versions: available.available_lts_releases, + cached_at: now, + }; + + // Save to cache + let _ = save_catalog_cache(app_handle, &catalog); + + Ok(catalog) +} + /// Get Adoptium API download info for a specific Java version and image type /// /// # Arguments @@ -157,8 +372,8 @@ pub async fn fetch_java_release( let arch = get_adoptium_arch(); let url = format!( - "https://api.adoptium.net/v3/assets/latest/{}/hotspot?os={}&architecture={}&image_type={}", - major_version, os, arch, image_type + "{}/assets/latest/{}/hotspot?os={}&architecture={}&image_type={}", + ADOPTIUM_API_BASE, major_version, os, arch, image_type ); let client = reqwest::Client::new(); @@ -199,7 +414,7 @@ pub async fn fetch_java_release( /// Fetch available Java versions from Adoptium API pub async fn fetch_available_versions() -> Result, String> { - let url = "https://api.adoptium.net/v3/info/available_releases"; + let url = format!("{}/info/available_releases", ADOPTIUM_API_BASE); let response = reqwest::get(url) .await @@ -218,7 +433,7 @@ pub async fn fetch_available_versions() -> Result, String> { Ok(releases.available_releases) } -/// Download and install Java +/// Download and install Java with resume support and progress events /// /// # Arguments /// * `app_handle` - Tauri app handle for accessing app directories @@ -236,6 +451,7 @@ pub async fn download_and_install_java( ) -> Result { // 1. Fetch download information let info = fetch_java_release(major_version, image_type).await?; + let file_name = info.file_name.clone(); // 2. Prepare installation directory let install_base = custom_path.unwrap_or_else(|| get_java_install_dir(app_handle)); @@ -244,7 +460,24 @@ pub async fn download_and_install_java( std::fs::create_dir_all(&install_base) .map_err(|e| format!("Failed to create installation directory: {}", e))?; - // 3. Download the archive + // 3. Add to download queue for persistence + let mut queue = DownloadQueue::load(app_handle); + queue.add(PendingJavaDownload { + major_version, + image_type: image_type.to_string(), + download_url: info.download_url.clone(), + file_name: info.file_name.clone(), + file_size: info.file_size, + checksum: info.checksum.clone(), + install_path: install_base.to_string_lossy().to_string(), + created_at: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + }); + queue.save(app_handle)?; + + // 4. Download the archive with resume support let archive_path = install_base.join(&info.file_name); // Check if we need to download @@ -261,30 +494,32 @@ pub async fn download_and_install_java( }; if need_download { - let client = reqwest::Client::new(); - let response = client - .get(&info.download_url) - .send() - .await - .map_err(|e| format!("Download failed: {}", e))?; - - let bytes = response - .bytes() - .await - .map_err(|e| format!("Failed to read download content: {}", e))?; - - // Verify downloaded file checksum - if let Some(expected) = &info.checksum { - if !downloader::verify_checksum(&bytes, Some(expected), None) { - return Err("Downloaded file verification failed, the file may be corrupted".to_string()); - } - } - - std::fs::write(&archive_path, &bytes) - .map_err(|e| format!("Failed to save downloaded file: {}", e))?; + // Use resumable download + downloader::download_with_resume( + app_handle, + &info.download_url, + &archive_path, + info.checksum.as_deref(), + info.file_size, + ) + .await?; } - // 4. Extract + // 5. Emit extracting status + let _ = app_handle.emit( + "java-download-progress", + JavaDownloadProgress { + file_name: file_name.clone(), + downloaded_bytes: info.file_size, + total_bytes: info.file_size, + speed_bytes_per_sec: 0, + eta_seconds: 0, + status: "Extracting".to_string(), + percentage: 100.0, + }, + ); + + // 6. Extract // If the target directory exists, remove it first if version_dir.exists() { std::fs::remove_dir_all(&version_dir) @@ -304,10 +539,10 @@ pub async fn download_and_install_java( return Err(format!("Unsupported archive format: {}", info.file_name)); }; - // 5. Clean up downloaded archive + // 7. Clean up downloaded archive let _ = std::fs::remove_file(&archive_path); - // 6. Locate java executable + // 8. Locate java executable // macOS has a different structure: jdk-xxx/Contents/Home/bin/java // Linux/Windows: jdk-xxx/bin/java let java_home = version_dir.join(&top_level_dir); @@ -326,10 +561,28 @@ pub async fn download_and_install_java( )); } - // 7. Verify installation + // 9. Verify installation let installation = check_java_installation(&java_bin) .ok_or_else(|| "Failed to verify Java installation".to_string())?; + // 10. Remove from download queue + queue.remove(major_version, &image_type.to_string()); + queue.save(app_handle)?; + + // 11. Emit completed status + let _ = app_handle.emit( + "java-download-progress", + JavaDownloadProgress { + file_name, + downloaded_bytes: info.file_size, + total_bytes: info.file_size, + speed_bytes_per_sec: 0, + eta_seconds: 0, + status: "Completed".to_string(), + percentage: 100.0, + }, + ); + Ok(installation) } @@ -676,3 +929,57 @@ fn find_java_executable(dir: &PathBuf) -> Option { None } + +/// Resume pending Java downloads from queue +pub async fn resume_pending_downloads(app_handle: &AppHandle) -> Result, String> { + let queue = DownloadQueue::load(app_handle); + let mut installed = Vec::new(); + + for pending in queue.pending_downloads.iter() { + let image_type = if pending.image_type == "jdk" { + ImageType::Jdk + } else { + ImageType::Jre + }; + + // Try to resume the download + match download_and_install_java( + app_handle, + pending.major_version, + image_type, + Some(PathBuf::from(&pending.install_path)), + ) + .await + { + Ok(installation) => { + installed.push(installation); + } + Err(e) => { + eprintln!( + "Failed to resume Java {} {} download: {}", + pending.major_version, pending.image_type, e + ); + } + } + } + + Ok(installed) +} + +/// Cancel current Java download +pub fn cancel_current_download() { + downloader::cancel_java_download(); +} + +/// Get pending downloads from queue +pub fn get_pending_downloads(app_handle: &AppHandle) -> Vec { + let queue = DownloadQueue::load(app_handle); + queue.pending_downloads +} + +/// Clear a specific pending download +pub fn clear_pending_download(app_handle: &AppHandle, major_version: u32, image_type: &str) -> Result<(), String> { + let mut queue = DownloadQueue::load(app_handle); + queue.remove(major_version, image_type); + queue.save(app_handle) +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index a2b8c6c..53589d8 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -895,6 +895,45 @@ async fn fetch_available_java_versions() -> Result, String> { core::java::fetch_available_versions().await } +/// Fetch Java catalog with platform availability (uses cache) +#[tauri::command] +async fn fetch_java_catalog( + app_handle: tauri::AppHandle, +) -> Result { + core::java::fetch_java_catalog(&app_handle, false).await +} + +/// Refresh Java catalog (bypass cache) +#[tauri::command] +async fn refresh_java_catalog( + app_handle: tauri::AppHandle, +) -> Result { + core::java::fetch_java_catalog(&app_handle, true).await +} + +/// Cancel current Java download +#[tauri::command] +async fn cancel_java_download() -> Result<(), String> { + core::java::cancel_current_download(); + Ok(()) +} + +/// Get pending Java downloads +#[tauri::command] +async fn get_pending_java_downloads( + app_handle: tauri::AppHandle, +) -> Result, String> { + Ok(core::java::get_pending_downloads(&app_handle)) +} + +/// Resume pending Java downloads +#[tauri::command] +async fn resume_java_downloads( + app_handle: tauri::AppHandle, +) -> Result, String> { + core::java::resume_pending_downloads(&app_handle).await +} + /// Get Minecraft versions supported by Fabric #[tauri::command] async fn get_fabric_game_versions() -> Result, String> { @@ -1067,6 +1106,13 @@ fn main() { println!("[Startup] Loaded saved account"); } + // Check for pending Java downloads and notify frontend + let pending = core::java::get_pending_downloads(&app.app_handle()); + if !pending.is_empty() { + println!("[Startup] Found {} pending Java download(s)", pending.len()); + let _ = app.emit("pending-java-downloads", pending.len()); + } + Ok(()) }) .invoke_handler(tauri::generate_handler![ @@ -1080,11 +1126,17 @@ fn main() { start_microsoft_login, complete_microsoft_login, refresh_account, + // Java commands detect_java, get_recommended_java, fetch_adoptium_java, download_adoptium_java, fetch_available_java_versions, + fetch_java_catalog, + refresh_java_catalog, + cancel_java_download, + get_pending_java_downloads, + resume_java_downloads, // Fabric commands get_fabric_game_versions, get_fabric_loader_versions, diff --git a/ui/src/App.svelte b/ui/src/App.svelte index 968f6c5..f32a42f 100644 --- a/ui/src/App.svelte +++ b/ui/src/App.svelte @@ -1,25 +1,23 @@