use flate2::read::GzDecoder;
use stakpak_shared::tls_client::{TlsClientConfig, create_tls_client};
use std::fs;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use std::process::Command;
use tar::Archive;
use zip::ZipArchive;
pub struct PluginConfig {
pub name: String,
pub base_url: String,
pub targets: Vec<String>,
pub version: Option<String>,
}
pub async fn get_plugin_path(config: PluginConfig) -> String {
let config = PluginConfig {
name: config.name,
base_url: config.base_url.trim_end_matches('/').to_string(), targets: config.targets,
version: config.version,
};
let target_version = match config.version.clone() {
Some(version) => version,
None => match get_latest_version(&config).await {
Ok(version) => version,
Err(e) => {
eprintln!(
"Warning: Failed to check latest version for {}: {}",
config.name, e
);
return get_plugin_path_without_version_check(&config).await;
}
},
};
if let Ok(system_version) = get_version_from_command(&config.name, &config.name) {
if is_same_version(&system_version, &target_version) {
return config.name.clone();
} else {
}
}
if let Ok(existing_path) = get_existing_plugin_path(&config.name)
&& let Ok(current_version) = get_version_from_command(&existing_path, &config.name)
{
if is_same_version(¤t_version, &target_version) {
return existing_path;
} else {
}
}
match download_and_install_plugin(&config).await {
Ok(path) => {
path
}
Err(e) => {
eprintln!("Failed to download {}: {}", config.name, e);
if let Ok(existing_path) = get_existing_plugin_path(&config.name) {
eprintln!("Using existing {} version", config.name);
existing_path
} else if is_plugin_available(&config.name) {
eprintln!("Using system PATH version of {}", config.name);
config.name.clone()
} else {
eprintln!("No fallback available for {}", config.name);
config.name.clone() }
}
}
}
async fn get_plugin_path_without_version_check(config: &PluginConfig) -> String {
if is_plugin_available(&config.name) {
return config.name.clone();
}
if let Ok(existing_path) = get_existing_plugin_path(&config.name) {
return existing_path;
}
match download_and_install_plugin(config).await {
Ok(path) => path,
Err(e) => {
eprintln!("Failed to download {}: {}", config.name, e);
config.name.clone() }
}
}
fn get_version_from_command(command: &str, display_name: &str) -> Result<String, String> {
let output = Command::new(command)
.arg("version")
.output()
.map_err(|e| format!("Failed to run {} version command: {}", display_name, e))?;
if !output.status.success() {
return Err(format!("{} version command failed", display_name));
}
let version_output = String::from_utf8_lossy(&output.stdout);
let full_output = version_output.trim();
if full_output.is_empty() {
return Err(format!("Could not determine {} version", display_name));
}
let parts: Vec<&str> = full_output.split_whitespace().collect();
if parts.len() >= 2 {
Ok(parts[1].to_string())
} else {
Ok(full_output.to_string())
}
}
pub fn is_plugin_available(plugin_name: &str) -> bool {
get_version_from_command(plugin_name, plugin_name).is_ok()
}
async fn get_latest_version(config: &PluginConfig) -> Result<String, String> {
let version_url = format!("{}/latest_version.txt", config.base_url);
let client = create_tls_client(TlsClientConfig::default())?;
let response = client
.get(&version_url)
.send()
.await
.map_err(|e| format!("Failed to fetch latest version for {}: {}", config.name, e))?;
if !response.status().is_success() {
return Err(format!(
"Failed to fetch latest version for {}: HTTP {}",
config.name,
response.status()
));
}
let version_text = response
.text()
.await
.map_err(|e| format!("Failed to read version response: {}", e))?;
Ok(version_text.trim().to_string())
}
fn is_same_version(current: &str, latest: &str) -> bool {
let current_clean = current.strip_prefix('v').unwrap_or(current);
let latest_clean = latest.strip_prefix('v').unwrap_or(latest);
current_clean == latest_clean
}
fn get_existing_plugin_path(plugin_name: &str) -> Result<String, String> {
let home_dir =
std::env::var("HOME").map_err(|_| "HOME environment variable not set".to_string())?;
let stakpak_dir = PathBuf::from(&home_dir).join(".stakpak");
let plugins_dir = stakpak_dir.join("plugins");
let binary_name = if cfg!(windows) {
format!("{}.exe", plugin_name)
} else {
plugin_name.to_string()
};
let plugin_path = plugins_dir.join(&binary_name);
if plugin_path.exists() && is_executable(&plugin_path) {
Ok(plugin_path.to_string_lossy().to_string())
} else {
Err(format!(
"{} binary not found in plugins directory",
plugin_name
))
}
}
async fn download_and_install_plugin(config: &PluginConfig) -> Result<String, String> {
let home_dir =
std::env::var("HOME").map_err(|_| "HOME environment variable not set".to_string())?;
let stakpak_dir = PathBuf::from(&home_dir).join(".stakpak");
let plugins_dir = stakpak_dir.join("plugins");
fs::create_dir_all(&plugins_dir)
.map_err(|e| format!("Failed to create plugins directory: {}", e))?;
let (download_url, binary_name, is_zip) = get_download_info(config)?;
let plugin_path = plugins_dir.join(&binary_name);
let client = create_tls_client(TlsClientConfig::default())?;
let response = client
.get(&download_url)
.send()
.await
.map_err(|e| format!("Failed to download {}: {}", config.name, e))?;
if !response.status().is_success() {
return Err(format!(
"Failed to download {}: HTTP {}",
config.name,
response.status()
));
}
let archive_bytes = response
.bytes()
.await
.map_err(|e| format!("Failed to read download response: {}", e))?;
if is_zip {
extract_zip(&archive_bytes, &plugins_dir)?;
} else {
extract_tar_gz(&archive_bytes, &plugins_dir)?;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut permissions = fs::metadata(&plugin_path)
.map_err(|e| format!("Failed to get file metadata: {}", e))?
.permissions();
permissions.set_mode(0o755);
fs::set_permissions(&plugin_path, permissions)
.map_err(|e| format!("Failed to set executable permissions: {}", e))?;
}
Ok(plugin_path.to_string_lossy().to_string())
}
pub fn get_download_info(config: &PluginConfig) -> Result<(String, String, bool), String> {
let os = std::env::consts::OS;
let arch = std::env::consts::ARCH;
let current_target = match (os, arch) {
("linux", "x86_64") => "linux-x86_64",
("macos", "x86_64") => "darwin-x86_64",
("macos", "aarch64") => "darwin-aarch64",
("windows", "x86_64") => "windows-x86_64",
_ => return Err(format!("Unsupported platform: {} {}", os, arch)),
};
if !config.targets.contains(¤t_target.to_string()) {
return Err(format!(
"Plugin {} does not support target: {}",
config.name, current_target
));
}
let (binary_name, is_zip) = if current_target.starts_with("windows") {
(format!("{}.exe", config.name), true)
} else {
(config.name.clone(), false)
};
let extension = if is_zip { "zip" } else { "tar.gz" };
let download_url = format!(
"{}/{}/{}-{}.{}",
config.base_url,
config.version.clone().unwrap_or("latest".to_string()),
config.name,
current_target,
extension
);
Ok((download_url, binary_name, is_zip))
}
fn is_executable(path: &Path) -> bool {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(metadata) = fs::metadata(path) {
let permissions = metadata.permissions();
return permissions.mode() & 0o111 != 0;
}
}
#[cfg(windows)]
{
return path.extension().map_or(false, |ext| ext == "exe");
}
false
}
pub fn extract_tar_gz(archive_bytes: &[u8], dest_dir: &Path) -> Result<(), String> {
let cursor = Cursor::new(archive_bytes);
let tar = GzDecoder::new(cursor);
let mut archive = Archive::new(tar);
archive
.unpack(dest_dir)
.map_err(|e| format!("Failed to extract tar.gz archive: {}", e))?;
Ok(())
}
pub fn extract_zip(archive_bytes: &[u8], dest_dir: &Path) -> Result<(), String> {
let cursor = Cursor::new(archive_bytes);
let mut archive =
ZipArchive::new(cursor).map_err(|e| format!("Failed to read zip archive: {}", e))?;
for i in 0..archive.len() {
let mut file = archive
.by_index(i)
.map_err(|e| format!("Failed to access file {} in zip: {}", i, e))?;
let outpath = match file.enclosed_name() {
Some(path) => dest_dir.join(path),
None => continue,
};
if file.is_dir() {
fs::create_dir_all(&outpath)
.map_err(|e| format!("Failed to create directory {}: {}", outpath.display(), e))?;
} else {
if let Some(p) = outpath.parent()
&& !p.exists()
{
fs::create_dir_all(p).map_err(|e| {
format!("Failed to create parent directory {}: {}", p.display(), e)
})?;
}
let mut outfile = fs::File::create(&outpath)
.map_err(|e| format!("Failed to create file {}: {}", outpath.display(), e))?;
std::io::copy(&mut file, &mut outfile)
.map_err(|e| format!("Failed to extract file {}: {}", outpath.display(), e))?;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Some(mode) = file.unix_mode() {
fs::set_permissions(&outpath, fs::Permissions::from_mode(mode)).map_err(|e| {
format!("Failed to set permissions for {}: {}", outpath.display(), e)
})?;
}
}
}
Ok(())
}