aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src-tauri
diff options
context:
space:
mode:
authorBegonia, HE <163421589+BegoniaHe@users.noreply.github.com>2026-01-15 05:29:58 +0100
committerBegonia, HE <163421589+BegoniaHe@users.noreply.github.com>2026-01-15 05:29:58 +0100
commit43a3e9c285f3d5d04fef025041a06609a0d1c218 (patch)
treec18970f3e926e079d5857219031bbaf8d37a6901 /src-tauri
parent1b3c84b0c78ea438c8f446054af196c620d30602 (diff)
downloadDropOut-43a3e9c285f3d5d04fef025041a06609a0d1c218.tar.gz
DropOut-43a3e9c285f3d5d04fef025041a06609a0d1c218.zip
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.
Diffstat (limited to 'src-tauri')
-rw-r--r--src-tauri/Cargo.toml2
-rw-r--r--src-tauri/src/core/downloader.rs400
-rw-r--r--src-tauri/src/core/java.rs369
-rw-r--r--src-tauri/src/main.rs52
4 files changed, 788 insertions, 35 deletions
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<String>,
}
+/// 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<String>,
+ pub timestamp: u64,
+ pub segments: Vec<DownloadSegment>,
+}
+
+/// 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<String>,
+ 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<PendingJavaDownload>,
+}
+
+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::<usize, String>(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<String>,
+ pub file_size: u64,
+ pub checksum: Option<String>,
+ 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<JavaReleaseInfo>,
+ pub available_major_versions: Vec<u32>,
+ pub lts_versions: Vec<u32>,
+ 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<String>,
}
#[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<u32>,
+ pub available_lts_releases: Vec<u32>,
+ pub most_recent_lts: Option<u32>,
+ pub most_recent_feature_release: Option<u32>,
+}
+
/// 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<JavaCatalog> {
+ 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<JavaCatalog, String> {
+ // 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::<Vec<AdoptiumAsset>>().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<Vec<u32>, 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<Vec<u32>, 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<JavaInstallation, String> {
// 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<PathBuf> {
None
}
+
+/// Resume pending Java downloads from queue
+pub async fn resume_pending_downloads(app_handle: &AppHandle) -> Result<Vec<JavaInstallation>, 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<PendingJavaDownload> {
+ 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<Vec<u32>, 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::JavaCatalog, String> {
+ 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::JavaCatalog, String> {
+ 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<Vec<core::downloader::PendingJavaDownload>, 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<Vec<core::java::JavaInstallation>, String> {
+ core::java::resume_pending_downloads(&app_handle).await
+}
+
/// Get Minecraft versions supported by Fabric
#[tauri::command]
async fn get_fabric_game_versions() -> Result<Vec<core::fabric::FabricGameVersion>, 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,