use std::fs::{self, File};
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
use indicatif::{ProgressBar, ProgressStyle};
use log::debug;
use reqwest::blocking::Client;
use zip::ZipArchive;
use crate::app::STACKQL_RELEASE_BASE_URL;
use crate::error::AppError;
use crate::utils::platform::{get_platform, Platform};
pub fn get_download_url() -> Result<String, AppError> {
let platform = get_platform();
match platform {
Platform::MacOS => Ok(format!(
"{}/stackql_darwin_multiarch.pkg",
STACKQL_RELEASE_BASE_URL
)),
Platform::Windows => Ok(format!(
"{}/stackql_windows_amd64.zip",
STACKQL_RELEASE_BASE_URL
)),
Platform::Linux => {
let arch = if cfg!(target_arch = "aarch64") {
"arm64"
} else {
"amd64"
};
Ok(format!(
"{}/stackql_linux_{}.zip",
STACKQL_RELEASE_BASE_URL, arch
))
}
Platform::Unknown => Err(AppError::CommandFailed(
"Unsupported platform for stackql download".to_string(),
)),
}
}
pub fn download_binary() -> Result<PathBuf, AppError> {
let download_url = get_download_url()?;
let current_dir = std::env::current_dir().map_err(AppError::IoError)?;
let binary_name = crate::utils::platform::get_binary_name();
let archive_name = Path::new(&download_url)
.file_name()
.ok_or_else(|| AppError::CommandFailed("Invalid URL".to_string()))?
.to_string_lossy()
.to_string();
let archive_path = current_dir.join(&archive_name);
debug!("Downloading from {}", download_url);
let client = Client::builder()
.timeout(std::time::Duration::from_secs(300))
.build()
.map_err(|e| AppError::CommandFailed(format!("Failed to create HTTP client: {}", e)))?;
let mut response = client
.get(&download_url)
.send()
.map_err(|e| AppError::CommandFailed(format!("Failed to download: {}", e)))?;
let total_size = response.content_length().unwrap_or(0);
let progress_bar = ProgressBar::new(total_size);
progress_bar.set_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})")
.unwrap()
.progress_chars("#>-"));
let mut file = File::create(&archive_path).map_err(AppError::IoError)?;
let mut buffer = [0u8; 8192];
let mut downloaded: u64 = 0;
loop {
let bytes_read = response
.read(&mut buffer)
.map_err(|e| AppError::CommandFailed(format!("Failed to read response: {}", e)))?;
if bytes_read == 0 {
break;
}
file.write_all(&buffer[..bytes_read])
.map_err(AppError::IoError)?;
downloaded += bytes_read as u64;
progress_bar.set_position(downloaded);
}
progress_bar.finish_with_message("Download complete");
debug!("Extracting the binary...");
let binary_path = extract_binary(&archive_path, ¤t_dir, &binary_name)?;
fs::remove_file(&archive_path).ok();
if get_platform() != Platform::Windows {
Command::new("chmod")
.arg("+x")
.arg(&binary_path)
.output()
.map_err(|e| {
AppError::CommandFailed(format!("Failed to set executable permission: {}", e))
})?;
}
debug!(
"StackQL executable successfully installed at: {}",
binary_path.display()
);
Ok(binary_path)
}
fn extract_binary(
archive_path: &Path,
dest_dir: &Path,
binary_name: &str,
) -> Result<PathBuf, AppError> {
let binary_path = dest_dir.join(binary_name);
match get_platform() {
Platform::MacOS => {
let unpacked_dir = dest_dir.join("stackql_unpacked");
if unpacked_dir.exists() {
fs::remove_dir_all(&unpacked_dir).map_err(AppError::IoError)?;
}
let output = Command::new("pkgutil")
.arg("--expand-full")
.arg(archive_path)
.arg(&unpacked_dir)
.output()
.map_err(|e| AppError::CommandFailed(format!("Failed to extract pkg: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(AppError::CommandFailed(format!(
"pkgutil failed: {}",
stderr
)));
}
let extracted_binary =
find_file_recursive(&unpacked_dir, "stackql").ok_or_else(|| {
AppError::CommandFailed(
"Could not find stackql binary in extracted pkg".to_string(),
)
})?;
fs::copy(&extracted_binary, &binary_path).map_err(AppError::IoError)?;
fs::remove_dir_all(unpacked_dir).ok();
}
_ => {
let file = File::open(archive_path).map_err(AppError::IoError)?;
let mut archive = ZipArchive::new(file).map_err(|e| {
AppError::CommandFailed(format!("Failed to open zip archive: {}", e))
})?;
for i in 0..archive.len() {
let mut file = archive.by_index(i).map_err(|e| {
AppError::CommandFailed(format!("Failed to extract file: {}", e))
})?;
let outpath = match file.enclosed_name() {
Some(path) => dest_dir.join(path),
_none => continue,
};
if file.name().ends_with('/') {
fs::create_dir_all(&outpath).map_err(AppError::IoError)?;
} else {
let mut outfile = File::create(&outpath).map_err(AppError::IoError)?;
io::copy(&mut file, &mut outfile).map_err(AppError::IoError)?;
}
}
if get_platform() == Platform::Windows {
let potential_binary = dest_dir.join("stackql");
if potential_binary.exists() && !binary_path.exists() {
fs::rename(potential_binary, &binary_path).map_err(AppError::IoError)?;
}
}
}
}
if !binary_path.exists() {
return Err(AppError::CommandFailed(format!(
"Binary {} not found after extraction",
binary_name
)));
}
Ok(binary_path)
}
fn find_file_recursive(dir: &Path, target_name: &str) -> Option<PathBuf> {
let entries = fs::read_dir(dir).ok()?;
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(found) = find_file_recursive(&path, target_name) {
return Some(found);
}
} else if path.file_name().and_then(|n| n.to_str()) == Some(target_name) {
return Some(path);
}
}
None
}