lighty_java/
jre_downloader.rs

1// Copyright (c) 2025 Hamadi
2// Licensed under the MIT License
3
4//! JRE Download and Installation
5//!
6//! This module handles downloading and extracting Java Runtime Environments.
7//! Implementation is based on standard Rust async patterns and public APIs.
8
9use std::io::Cursor;
10use std::path::{Path, PathBuf};
11use crate::errors::{JreError, JreResult};
12use path_absolutize::Absolutize;
13use tokio::fs;
14
15use lighty_core::system::{OperatingSystem, OS};
16use lighty_core::download::download_file;
17use lighty_core::extract::{tar_gz_extract, zip_extract};
18
19use super::JavaDistribution;
20
21#[cfg(feature = "events")]
22use lighty_event::{EventBus, Event, JavaEvent};
23
24/// Locates an existing Java binary in the runtime directory
25///
26/// Searches for the java executable in the expected directory structure
27/// based on the distribution and version.
28///
29/// # Arguments
30/// * `runtimes_folder` - Base directory containing installed JREs
31/// * `distribution` - The Java distribution to locate
32/// * `version` - Java major version number
33///
34/// # Returns
35/// Absolute path to the java binary, or error if not found
36pub async fn find_java_binary(
37    runtimes_folder: &Path,
38    distribution: &JavaDistribution,
39    version: &u8,
40) -> JreResult<PathBuf> {
41    let runtime_dir = build_runtime_path(runtimes_folder, distribution, version);
42
43    let binary_path = locate_binary_in_directory(&runtime_dir).await?;
44
45    // Ensure execution permissions on Unix systems
46    #[cfg(unix)]
47    ensure_executable_permissions(&binary_path).await?;
48
49    Ok(binary_path.absolutize()?.to_path_buf())
50}
51
52/// Downloads and installs a JRE to the specified directory (with events feature)
53///
54/// # Arguments
55/// * `runtimes_folder` - Base directory for JRE installation
56/// * `distribution` - Java distribution to download
57/// * `version` - Java major version number
58/// * `on_progress` - Callback for download progress (bytes_downloaded, total_bytes)
59/// * `event_bus` - Optional event bus for emitting events
60///
61/// # Returns
62/// Path to the installed java binary
63#[cfg(feature = "events")]
64pub async fn jre_download<F>(
65    runtimes_folder: &Path,
66    distribution: &JavaDistribution,
67    version: &u8,
68    on_progress: F,
69    event_bus: Option<&EventBus>,
70) -> JreResult<PathBuf>
71where
72    F: Fn(u64, u64),
73{
74    let runtime_dir = build_runtime_path(runtimes_folder, distribution, version);
75
76    // Clean existing installation
77    prepare_installation_directory(&runtime_dir).await?;
78
79    // Get download URL
80    let download_url = distribution
81        .get_download_url(version)
82        .await
83        .map_err(|e| JreError::Download(format!("Failed to get download URL: {}", e)))?;
84
85    // Emit JavaDownloadStarted event
86    if let Some(bus) = event_bus {
87        // Get total bytes first
88        let response = lighty_core::hosts::HTTP_CLIENT
89            .get(&download_url)
90            .send()
91            .await
92            .map_err(|e| JreError::Download(format!("Failed to check file size: {}", e)))?;
93
94        let total_bytes = response.content_length().unwrap_or(0);
95
96        bus.emit(Event::Java(JavaEvent::JavaDownloadStarted {
97            distribution: distribution.get_name().to_string(),
98            version: *version,
99            total_bytes,
100        }));
101    }
102
103    // Download JRE archive with progress tracking
104    let archive_bytes = {
105        let event_bus_ref = event_bus;
106        download_file(&download_url, |current, _total| {
107            on_progress(current, _total);
108            if let Some(bus) = event_bus_ref {
109                // Only emit progress for actual chunks (not initial 0)
110                if current > 0 {
111                    bus.emit(Event::Java(JavaEvent::JavaDownloadProgress {
112                        bytes: current,
113                    }));
114                }
115            }
116        })
117        .await
118        .map_err(|e| JreError::Download(format!("Download failed: {}", e)))?
119    };
120
121    // Emit JavaDownloadCompleted event
122    if let Some(bus) = event_bus {
123        bus.emit(Event::Java(JavaEvent::JavaDownloadCompleted {
124            distribution: distribution.get_name().to_string(),
125            version: *version,
126        }));
127    }
128
129    // Emit JavaExtractionStarted event
130    if let Some(bus) = event_bus {
131        bus.emit(Event::Java(JavaEvent::JavaExtractionStarted {
132            distribution: distribution.get_name().to_string(),
133            version: *version,
134        }));
135    }
136
137    // Extract archive based on OS
138    extract_archive(
139        &archive_bytes,
140        &runtime_dir,
141        event_bus,
142    ).await?;
143
144    // Locate and return the java binary
145    let binary_path = find_java_binary(runtimes_folder, distribution, version).await?;
146
147    // Emit JavaExtractionCompleted event
148    if let Some(bus) = event_bus {
149        bus.emit(Event::Java(JavaEvent::JavaExtractionCompleted {
150            distribution: distribution.get_name().to_string(),
151            version: *version,
152            binary_path: binary_path.to_string_lossy().to_string(),
153        }));
154    }
155
156    Ok(binary_path)
157}
158
159/// Downloads and installs a JRE to the specified directory (without events feature)
160///
161/// # Arguments
162/// * `runtimes_folder` - Base directory for JRE installation
163/// * `distribution` - Java distribution to download
164/// * `version` - Java major version number
165/// * `on_progress` - Callback for download progress (bytes_downloaded, total_bytes)
166///
167/// # Returns
168/// Path to the installed java binary
169#[cfg(not(feature = "events"))]
170pub async fn jre_download<F>(
171    runtimes_folder: &Path,
172    distribution: &JavaDistribution,
173    version: &u8,
174    on_progress: F,
175) -> JreResult<PathBuf>
176where
177    F: Fn(u64, u64),
178{
179    let runtime_dir = build_runtime_path(runtimes_folder, distribution, version);
180
181    // Clean existing installation
182    prepare_installation_directory(&runtime_dir).await?;
183
184    // Get download URL
185    let download_url = distribution
186        .get_download_url(version)
187        .await
188        .map_err(|e| JreError::Download(format!("Failed to get download URL: {}", e)))?;
189
190    // Download JRE archive
191    let archive_bytes = download_file(&download_url, on_progress)
192        .await
193        .map_err(|e| JreError::Download(format!("Download failed: {}", e)))?;
194
195    // Extract archive based on OS
196    extract_archive(&archive_bytes, &runtime_dir).await?;
197
198    // Locate and return the java binary
199    find_java_binary(runtimes_folder, distribution, version).await
200}
201
202// ============================================================================
203// Private Helper Functions
204// ============================================================================
205
206/// Constructs the runtime installation path for a given distribution and version
207fn build_runtime_path(
208    runtimes_folder: &Path,
209    distribution: &JavaDistribution,
210    version: &u8,
211) -> PathBuf {
212    // Optimized: Build path directly without intermediate String allocation
213    let mut path = runtimes_folder.to_path_buf();
214    path.push(format!("{}_{}", distribution.get_name(), version));
215    path
216}
217
218/// Prepares the installation directory by removing existing files
219async fn prepare_installation_directory(runtime_dir: &Path) -> JreResult<()> {
220    if runtime_dir.exists() {
221        fs::remove_dir_all(runtime_dir).await?;
222    }
223    fs::create_dir_all(runtime_dir).await?;
224    Ok(())
225}
226
227/// Extracts the JRE archive based on the operating system (with events feature)
228#[cfg(feature = "events")]
229async fn extract_archive(
230    archive_bytes: &[u8],
231    destination: &Path,
232    event_bus: Option<&EventBus>,
233) -> JreResult<()> {
234    let cursor = Cursor::new(archive_bytes);
235
236    match OS {
237        OperatingSystem::WINDOWS => {
238            zip_extract(cursor, destination, event_bus)
239                .await
240                .map_err(|e| JreError::Extraction(format!("ZIP extraction failed: {}", e)))?;
241        }
242        OperatingSystem::LINUX | OperatingSystem::OSX => {
243            tar_gz_extract(cursor, destination, event_bus)
244                .await
245                .map_err(|e| JreError::Extraction(format!("TAR.GZ extraction failed: {}", e)))?;
246        }
247        OperatingSystem::UNKNOWN => {
248            return Err(JreError::UnsupportedOS);
249        }
250    }
251
252    Ok(())
253}
254
255/// Extracts the JRE archive based on the operating system (without events feature)
256#[cfg(not(feature = "events"))]
257async fn extract_archive(archive_bytes: &[u8], destination: &Path) -> JreResult<()> {
258    let cursor = Cursor::new(archive_bytes);
259
260    match OS {
261        OperatingSystem::WINDOWS => {
262            zip_extract(cursor, destination)
263                .await
264                .map_err(|e| JreError::Extraction(format!("ZIP extraction failed: {}", e)))?;
265        }
266        OperatingSystem::LINUX | OperatingSystem::OSX => {
267            tar_gz_extract(cursor, destination)
268                .await
269                .map_err(|e| JreError::Extraction(format!("TAR.GZ extraction failed: {}", e)))?;
270        }
271        OperatingSystem::UNKNOWN => {
272            return Err(JreError::UnsupportedOS);
273        }
274    }
275
276    Ok(())
277}
278
279/// Locates the java binary within the extracted JRE directory
280///
281/// The structure varies by OS:
282/// - Windows: jre_root/bin/java.exe
283/// - macOS: jre_root/Contents/Home/bin/java
284/// - Linux: jre_root/bin/java
285async fn locate_binary_in_directory(runtime_dir: &Path) -> JreResult<PathBuf> {
286    // Find the first subdirectory (JRE root)
287    let mut entries = fs::read_dir(runtime_dir).await?;
288
289    let jre_root = entries
290        .next_entry()
291        .await?
292        .ok_or_else(|| JreError::NotFound {
293            path: runtime_dir.to_path_buf(),
294        })?
295        .path();
296
297    // Build path to java binary based on OS
298    let java_binary = match OS {
299        OperatingSystem::WINDOWS => jre_root.join("bin").join("java.exe"),
300        OperatingSystem::OSX => jre_root.join("Contents").join("Home").join("bin").join("java"),
301        _ => jre_root.join("bin").join("java"),
302    };
303
304    // Verify the binary exists
305    if !java_binary.exists() {
306        return Err(JreError::NotFound {
307            path: java_binary.clone(),
308        });
309    }
310
311    Ok(java_binary)
312}
313
314/// Ensures the java binary has execution permissions on Unix systems
315#[cfg(unix)]
316async fn ensure_executable_permissions(binary_path: &Path) -> JreResult<()> {
317    use std::os::unix::fs::PermissionsExt;
318
319    let metadata = fs::metadata(binary_path).await?;
320    let current_permissions = metadata.permissions();
321
322    // Check if any execute bit is set (owner, group, or other)
323    if current_permissions.mode() & 0o111 == 0 {
324        // No execute permissions, set them (rwxr-xr-x)
325        let mut new_permissions = current_permissions;
326        new_permissions.set_mode(0o755);
327        fs::set_permissions(binary_path, new_permissions).await?;
328    }
329
330    Ok(())
331}