use anyhow::{anyhow, Context, Result};
use std::fs::{self, File};
use std::io::{self, BufReader, Read, Write};
use std::path::{Path, PathBuf};
const JRE_VERSION: &str = "21";
const TIKA_JAR_URL: &str = "https://github.com/AmplifyTechnology/blazegraph-io/raw/v0.1.1/blazegraph-core/deps/tika/jni-jars/blazing-tika-jni.jar";
const TIKA_JAR_FILENAME: &str = "blazing-tika-jni.jar";
pub struct JreManager {
data_dir: PathBuf,
}
impl JreManager {
pub fn new() -> Result<Self> {
let data_dir = Self::get_data_dir()?;
Ok(Self { data_dir })
}
fn get_data_dir() -> Result<PathBuf> {
let home = dirs::home_dir()
.ok_or_else(|| anyhow!("Could not determine home directory"))?;
#[cfg(windows)]
{
let base = dirs::data_local_dir()
.ok_or_else(|| anyhow!("Could not determine local data directory"))?;
Ok(base.join("blazegraph"))
}
#[cfg(not(windows))]
{
Ok(home.join(".local").join("share").join("blazegraph"))
}
}
pub fn jre_path(&self) -> PathBuf {
self.data_dir.join("jre")
}
pub fn find_jar_path() -> Result<PathBuf> {
let candidates = [
PathBuf::from("../blazegraph-core/deps/tika/jni-jars/blazing-tika-jni.jar"),
PathBuf::from("blazegraph-core/deps/tika/jni-jars/blazing-tika-jni.jar"),
PathBuf::from("blazegraph-io/blazegraph-core/deps/tika/jni-jars/blazing-tika-jni.jar"),
PathBuf::from("src/tika/jars/blazing-tika-jni.jar"),
std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|p| p.join(TIKA_JAR_FILENAME)))
.unwrap_or_default(),
Self::get_data_dir()
.ok()
.map(|p| p.join(TIKA_JAR_FILENAME))
.unwrap_or_default(),
];
for candidate in &candidates {
if candidate.exists() && !candidate.to_string_lossy().is_empty() {
return Ok(candidate.clone());
}
}
Self::download_tika_jar()
}
fn download_tika_jar() -> Result<PathBuf> {
let data_dir = Self::get_data_dir()?;
fs::create_dir_all(&data_dir)
.with_context(|| format!("Failed to create data directory: {}", data_dir.display()))?;
let jar_path = data_dir.join(TIKA_JAR_FILENAME);
println!("📦 Tika JAR not found, downloading (~4.5 MB)...");
let temp_path = data_dir.join("blazing-tika-jni.jar.tmp");
let manager = Self { data_dir: data_dir.clone() };
manager.download_file(TIKA_JAR_URL, &temp_path)?;
fs::rename(&temp_path, &jar_path)
.with_context(|| "Failed to move downloaded JAR to final location")?;
println!("✅ Tika JAR installed at: {}", jar_path.display());
Ok(jar_path)
}
pub fn is_jre_installed(&self) -> bool {
let jre_path = self.jre_path();
let java_binary = if cfg!(windows) {
jre_path.join("bin").join("java.exe")
} else {
jre_path.join("bin").join("java")
};
java_binary.exists()
}
pub fn ensure_jre(&self) -> Result<PathBuf> {
let jre_path = self.jre_path();
if self.is_jre_installed() {
println!("✅ JRE found at: {}", jre_path.display());
return Ok(jre_path);
}
println!(
"📦 JRE not found, downloading Eclipse Temurin {}...",
JRE_VERSION
);
self.download_and_install_jre()?;
Ok(jre_path)
}
fn download_and_install_jre(&self) -> Result<()> {
fs::create_dir_all(&self.data_dir).with_context(|| {
format!(
"Failed to create data directory: {}",
self.data_dir.display()
)
})?;
let platform = Platform::detect()?;
println!(" Platform: {}-{}", platform.os, platform.arch);
let url = platform.adoptium_url(JRE_VERSION);
println!(" URL: {}", url);
let temp_path = self.data_dir.join("jre_download.tmp");
self.download_file(&url, &temp_path)?;
println!("📂 Extracting JRE...");
let jre_path = self.jre_path();
if jre_path.exists() {
fs::remove_dir_all(&jre_path)
.with_context(|| "Failed to remove existing JRE directory")?;
}
self.extract_archive(&temp_path, &platform)?;
let _ = fs::remove_file(&temp_path);
if self.is_jre_installed() {
println!("✅ JRE installed successfully at: {}", jre_path.display());
Ok(())
} else {
Err(anyhow!(
"JRE installation failed - java binary not found after extraction"
))
}
}
fn download_file(&self, url: &str, dest: &Path) -> Result<()> {
let response = ureq::get(url)
.call()
.with_context(|| format!("Failed to download from {}", url))?;
let total_size = response
.header("Content-Length")
.and_then(|s| s.parse::<u64>().ok());
let mut reader = response.into_reader();
let mut file = File::create(dest)
.with_context(|| format!("Failed to create file: {}", dest.display()))?;
let mut downloaded: u64 = 0;
let mut buffer = [0u8; 8192];
let mut last_progress = 0;
loop {
let bytes_read = reader.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
file.write_all(&buffer[..bytes_read])?;
downloaded += bytes_read as u64;
if let Some(total) = total_size {
let progress = ((downloaded * 100) / total) as usize;
if progress >= last_progress + 10 {
print!(
"\r Downloading: {}% ({:.1} MB)",
progress,
downloaded as f64 / 1_000_000.0
);
io::stdout().flush()?;
last_progress = progress;
}
}
}
if total_size.is_some() {
println!("\r Downloading: 100% ");
}
Ok(())
}
fn extract_archive(&self, archive_path: &Path, platform: &Platform) -> Result<()> {
let jre_path = self.jre_path();
if platform.is_zip() {
self.extract_zip(archive_path, &jre_path)?;
} else {
self.extract_tar_gz(archive_path, &jre_path)?;
}
Ok(())
}
fn extract_tar_gz(&self, archive_path: &Path, dest: &Path) -> Result<()> {
let file = File::open(archive_path)
.with_context(|| format!("Failed to open archive: {}", archive_path.display()))?;
let decoder = flate2::read::GzDecoder::new(BufReader::new(file));
let mut archive = tar::Archive::new(decoder);
let temp_extract = self.data_dir.join("jre_extract_tmp");
if temp_extract.exists() {
fs::remove_dir_all(&temp_extract)?;
}
fs::create_dir_all(&temp_extract)?;
archive
.unpack(&temp_extract)
.with_context(|| "Failed to extract tar.gz archive")?;
self.flatten_extracted_dir(&temp_extract, dest)?;
let _ = fs::remove_dir_all(&temp_extract);
Ok(())
}
fn extract_zip(&self, archive_path: &Path, dest: &Path) -> Result<()> {
let file = File::open(archive_path)
.with_context(|| format!("Failed to open archive: {}", archive_path.display()))?;
let mut archive = zip::ZipArchive::new(BufReader::new(file))
.with_context(|| "Failed to read zip archive")?;
let temp_extract = self.data_dir.join("jre_extract_tmp");
if temp_extract.exists() {
fs::remove_dir_all(&temp_extract)?;
}
fs::create_dir_all(&temp_extract)?;
archive
.extract(&temp_extract)
.with_context(|| "Failed to extract zip archive")?;
self.flatten_extracted_dir(&temp_extract, dest)?;
let _ = fs::remove_dir_all(&temp_extract);
Ok(())
}
fn flatten_extracted_dir(&self, extracted: &Path, dest: &Path) -> Result<()> {
let entries: Vec<_> = fs::read_dir(extracted)?
.filter_map(|e| e.ok())
.filter(|e| e.path().is_dir())
.collect();
let source = if entries.len() == 1 {
entries[0].path()
} else {
extracted.to_path_buf()
};
let actual_source = if cfg!(target_os = "macos") {
let contents_home = source.join("Contents").join("Home");
if contents_home.exists() {
contents_home
} else {
source
}
} else {
source
};
fs::rename(&actual_source, dest)
.or_else(|_| {
Self::copy_dir_recursive(&actual_source, dest)
})
.with_context(|| {
format!(
"Failed to move JRE from {} to {}",
actual_source.display(),
dest.display()
)
})?;
Ok(())
}
fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if src_path.is_dir() {
Self::copy_dir_recursive(&src_path, &dst_path)?;
} else {
fs::copy(&src_path, &dst_path)?;
}
}
Ok(())
}
}
struct Platform {
os: &'static str,
arch: &'static str,
}
impl Platform {
fn detect() -> Result<Self> {
let os = if cfg!(target_os = "linux") {
"linux"
} else if cfg!(target_os = "macos") {
"mac"
} else if cfg!(target_os = "windows") {
"windows"
} else {
return Err(anyhow!("Unsupported operating system"));
};
let arch = if cfg!(target_arch = "x86_64") {
"x64"
} else if cfg!(target_arch = "aarch64") {
"aarch64"
} else {
return Err(anyhow!("Unsupported architecture"));
};
Ok(Self { os, arch })
}
fn adoptium_url(&self, version: &str) -> String {
format!(
"https://api.adoptium.net/v3/binary/latest/{}/ga/{}/{}/jre/hotspot/normal/eclipse",
version, self.os, self.arch
)
}
fn is_zip(&self) -> bool {
self.os == "windows"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_platform_detection() {
let platform = Platform::detect().unwrap();
assert!(!platform.os.is_empty());
assert!(!platform.arch.is_empty());
}
#[test]
fn test_adoptium_url_format() {
let platform = Platform {
os: "linux",
arch: "x64",
};
let url = platform.adoptium_url("21");
assert!(url.contains("adoptium.net"));
assert!(url.contains("linux"));
assert!(url.contains("x64"));
assert!(url.contains("jre"));
}
}