aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src-tauri/src/core
diff options
context:
space:
mode:
Diffstat (limited to 'src-tauri/src/core')
-rw-r--r--src-tauri/src/core/downloader.rs444
-rw-r--r--src-tauri/src/core/java.rs727
2 files changed, 1161 insertions, 10 deletions
diff --git a/src-tauri/src/core/downloader.rs b/src-tauri/src/core/downloader.rs
index 3add9b7..bf6334f 100644
--- a/src-tauri/src/core/downloader.rs
+++ b/src-tauri/src/core/downloader.rs
@@ -1,17 +1,415 @@
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)]
pub struct DownloadTask {
pub url: String,
pub path: PathBuf,
+ #[serde(default)]
pub sha1: Option<String>,
+ #[serde(default)]
+ 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)]
@@ -25,6 +423,32 @@ pub struct ProgressEvent {
pub total_downloaded_bytes: u64,
}
+/// 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
+}
+
/// Snapshot of global progress state
struct ProgressSnapshot {
completed_files: usize,
@@ -129,17 +553,17 @@ pub async fn download_files(
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() {
emit_progress(&window, &file_name, "Verifying", 0, 0, &progress.snapshot());
- 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, skip download
let skipped_size = tokio::fs::metadata(&task.path)
.await
diff --git a/src-tauri/src/core/java.rs b/src-tauri/src/core/java.rs
index 9cf3053..8341138 100644
--- a/src-tauri/src/core/java.rs
+++ b/src-tauri/src/core/java.rs
@@ -1,6 +1,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::{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 {
@@ -9,6 +18,590 @@ 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"),
+ }
+ }
+}
+
+/// 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 {
+ 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,
+ #[serde(default)]
+ pub updated_at: Option<String>,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct AdoptiumPackage {
+ pub name: String,
+ pub link: String,
+ pub size: u64,
+ pub checksum: Option<String>, // SHA256
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct AdoptiumVersionData {
+ pub major: u32,
+ pub minor: u32,
+ pub security: u32,
+ pub semver: String,
+ 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 {
+ pub version: String,
+ pub release_name: String,
+ pub download_url: String,
+ pub file_name: String,
+ pub file_size: u64,
+ pub checksum: Option<String>,
+ 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(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
+/// * `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<JavaDownloadInfo, String> {
+ let os = get_adoptium_os();
+ let arch = get_adoptium_arch();
+
+ let url = format!(
+ "{}/assets/latest/{}/hotspot?os={}&architecture={}&image_type={}",
+ ADOPTIUM_API_BASE, 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<AdoptiumAsset> = 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<Vec<u32>, String> {
+ let url = format!("{}/info/available_releases", ADOPTIUM_API_BASE);
+
+ let response = reqwest::get(url)
+ .await
+ .map_err(|e| format!("Network request failed: {}", e))?;
+
+ #[derive(Deserialize)]
+ struct AvailableReleases {
+ available_releases: Vec<u32>,
+ }
+
+ let releases: AvailableReleases = response
+ .json()
+ .await
+ .map_err(|e| format!("Failed to parse response: {}", e))?;
+
+ Ok(releases.available_releases)
+}
+
+/// Download and install Java with resume support and progress events
+///
+/// # 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
+///
+/// # 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<PathBuf>,
+) -> 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));
+ 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. 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
+ 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 {
+ // Use resumable download
+ downloader::download_with_resume(
+ app_handle,
+ &info.download_url,
+ &archive_path,
+ info.checksum.as_deref(),
+ info.file_size,
+ )
+ .await?;
+ }
+
+ // 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)
+ .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));
+ };
+
+ // 7. Clean up downloaded archive
+ let _ = std::fs::remove_file(&archive_path);
+
+ // 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);
+ 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()
+ ));
+ }
+
+ // 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)
+}
+
+/// Find the top-level directory inside the extracted folder
+fn find_top_level_dir(extract_dir: &PathBuf) -> Result<String, String> {
+ 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<JavaInstallation> {
let mut installations = Vec::new();
@@ -256,3 +849,137 @@ pub fn get_recommended_java(required_major_version: Option<u64>) -> Option<JavaI
installations.into_iter().next()
}
}
+
+/// Detect all installed Java versions (including system installations and DropOut downloads)
+pub fn detect_all_java_installations(app_handle: &AppHandle) -> Vec<JavaInstallation> {
+ let mut installations = detect_java_installations();
+
+ // Add DropOut downloaded Java versions
+ 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() {
+ 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
+}
+
+//// Find the java executable in a directory using a limited-depth search
+fn find_java_executable(dir: &PathBuf) -> Option<PathBuf> {
+ 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
+}
+
+/// 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)
+}