use crate::{
downloader::Downloader,
formats::ArchiveExtractor,
progress::{ProgressContext, ProgressStyle},
Error, Result,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
pub struct Installer {
downloader: Downloader,
extractor: ArchiveExtractor,
}
impl Installer {
pub async fn new() -> Result<Self> {
let downloader = Downloader::new()?;
let extractor = ArchiveExtractor::new();
Ok(Self {
downloader,
extractor,
})
}
pub async fn install(&self, config: &InstallConfig) -> Result<PathBuf> {
if !config.force && self.is_installed(config).await? {
return Err(Error::AlreadyInstalled {
tool_name: config.tool_name.clone(),
version: config.version.clone(),
});
}
let progress = ProgressContext::new(
crate::progress::create_progress_reporter(ProgressStyle::default(), true),
true,
);
match &config.install_method {
InstallMethod::Archive { format: _ } => {
self.install_from_archive(config, &progress).await
}
InstallMethod::Binary => self.install_binary(config, &progress).await,
InstallMethod::Script { url } => self.install_from_script(config, url, &progress).await,
InstallMethod::PackageManager { manager, package } => {
self.install_from_package_manager(config, manager, package, &progress)
.await
}
InstallMethod::Custom { method } => {
self.install_custom(config, method, &progress).await
}
}
}
pub async fn is_installed(&self, config: &InstallConfig) -> Result<bool> {
let install_dir = &config.install_dir;
if !install_dir.exists() {
return Ok(false);
}
let bin_dir = install_dir.join("bin");
if bin_dir.exists() {
let exe_name = if cfg!(windows) {
format!("{}.exe", config.tool_name)
} else {
config.tool_name.clone()
};
let exe_path = bin_dir.join(&exe_name);
Ok(exe_path.exists() && exe_path.is_file())
} else {
self.has_executables(install_dir)
}
}
pub async fn uninstall(&self, _tool_name: &str, install_dir: &Path) -> Result<()> {
if install_dir.exists() {
std::fs::remove_dir_all(install_dir)?;
}
Ok(())
}
async fn install_from_archive(
&self,
config: &InstallConfig,
progress: &ProgressContext,
) -> Result<PathBuf> {
let download_url = config
.download_url
.as_ref()
.ok_or_else(|| Error::InvalidConfig {
message: "Download URL is required for archive installation".to_string(),
})?;
let temp_path = self
.downloader
.download_temp(download_url, progress)
.await?;
let extracted_files = self
.extractor
.extract(&temp_path, &config.install_dir, progress)
.await?;
let executable_path = self
.extractor
.find_best_executable(&extracted_files, &config.tool_name)?;
let _ = std::fs::remove_file(temp_path);
Ok(executable_path)
}
async fn install_binary(
&self,
config: &InstallConfig,
progress: &ProgressContext,
) -> Result<PathBuf> {
let download_url = config
.download_url
.as_ref()
.ok_or_else(|| Error::InvalidConfig {
message: "Download URL is required for binary installation".to_string(),
})?;
let bin_dir = config.install_dir.join("bin");
std::fs::create_dir_all(&bin_dir)?;
let exe_name = if cfg!(windows) {
format!("{}.exe", config.tool_name)
} else {
config.tool_name.clone()
};
let exe_path = bin_dir.join(&exe_name);
self.downloader
.download(download_url, &exe_path, progress)
.await?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let metadata = std::fs::metadata(&exe_path)?;
let mut permissions = metadata.permissions();
permissions.set_mode(0o755);
std::fs::set_permissions(&exe_path, permissions)?;
}
Ok(exe_path)
}
async fn install_from_script(
&self,
_config: &InstallConfig,
_script_url: &str,
_progress: &ProgressContext,
) -> Result<PathBuf> {
Err(Error::unsupported_format("script installation"))
}
async fn install_from_package_manager(
&self,
_config: &InstallConfig,
_manager: &str,
_package: &str,
_progress: &ProgressContext,
) -> Result<PathBuf> {
Err(Error::unsupported_format("package manager installation"))
}
async fn install_custom(
&self,
_config: &InstallConfig,
_method: &str,
_progress: &ProgressContext,
) -> Result<PathBuf> {
Err(Error::unsupported_format("custom installation"))
}
fn has_executables(&self, dir: &Path) -> Result<bool> {
if !dir.exists() {
return Ok(false);
}
for entry in walkdir::WalkDir::new(dir).max_depth(3) {
let entry = entry?;
let path = entry.path();
if path.is_file() {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(metadata) = std::fs::metadata(path) {
let permissions = metadata.permissions();
if permissions.mode() & 0o111 != 0 {
return Ok(true);
}
}
}
#[cfg(windows)]
{
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
if matches!(ext.to_lowercase().as_str(), "exe" | "bat" | "cmd" | "com") {
return Ok(true);
}
}
}
}
}
Ok(false)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstallConfig {
pub tool_name: String,
pub version: String,
pub install_method: InstallMethod,
pub download_url: Option<String>,
pub install_dir: PathBuf,
pub force: bool,
pub checksum: Option<String>,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum InstallMethod {
Archive { format: ArchiveFormat },
PackageManager { manager: String, package: String },
Script { url: String },
Binary,
Custom { method: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ArchiveFormat {
Zip,
TarGz,
TarXz,
TarBz2,
SevenZip,
}
pub struct InstallConfigBuilder {
config: InstallConfig,
}
impl Default for InstallConfigBuilder {
fn default() -> Self {
Self::new()
}
}
impl InstallConfigBuilder {
pub fn new() -> Self {
Self {
config: InstallConfig {
tool_name: String::new(),
version: String::new(),
install_method: InstallMethod::Binary,
download_url: None,
install_dir: PathBuf::new(),
force: false,
checksum: None,
metadata: HashMap::new(),
},
}
}
pub fn tool_name(mut self, name: impl Into<String>) -> Self {
self.config.tool_name = name.into();
self
}
pub fn version(mut self, version: impl Into<String>) -> Self {
self.config.version = version.into();
self
}
pub fn install_method(mut self, method: InstallMethod) -> Self {
self.config.install_method = method;
self
}
pub fn download_url(mut self, url: impl Into<String>) -> Self {
self.config.download_url = Some(url.into());
self
}
pub fn install_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.config.install_dir = dir.into();
self
}
pub fn force(mut self, force: bool) -> Self {
self.config.force = force;
self
}
pub fn checksum(mut self, checksum: impl Into<String>) -> Self {
self.config.checksum = Some(checksum.into());
self
}
pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.config.metadata.insert(key.into(), value.into());
self
}
pub fn build(self) -> InstallConfig {
self.config
}
}
impl InstallConfig {
pub fn builder() -> InstallConfigBuilder {
InstallConfigBuilder::new()
}
}