use crate::dir_context::DirContext;
use crate::pack::PackIdentity;
use crate::packer::PackToml;
use crate::packer::pack_toml::parse_validate_pack_toml;
use crate::packer::support;
use crate::support::zip;
use crate::{Error, Result};
use reqwest::Client;
use serde::Deserialize;
use simple_fs::{SPath, ensure_dir};
use std::str::FromStr;
use time::OffsetDateTime;
use time_tz::OffsetDateTimeExt;
#[derive(Debug, Clone)]
pub enum PackUri {
RepoPack(PackIdentity),
LocalPath(String),
HttpLink(String),
}
impl PackUri {
pub fn parse(uri: &str) -> Self {
if let Ok(pack_identity) = PackIdentity::from_str(uri) {
return PackUri::RepoPack(pack_identity);
}
if uri.starts_with("http://") || uri.starts_with("https://") {
PackUri::HttpLink(uri.to_string())
} else {
PackUri::LocalPath(uri.to_string())
}
}
}
impl std::fmt::Display for PackUri {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PackUri::RepoPack(identity) => write!(f, "{}", identity),
PackUri::LocalPath(path) => write!(f, "local file '{}'", path),
PackUri::HttpLink(url) => write!(f, "URL '{}'", url),
}
}
}
#[derive(Deserialize, Debug)]
struct LatestToml {
latest_stable: Option<LatestStableInfo>,
}
#[derive(Deserialize, Debug)]
struct LatestStableInfo {
version: Option<String>,
rel_path: Option<String>,
}
impl LatestToml {
fn validate(&self) -> Result<(&str, &str)> {
let latest_stable = self
.latest_stable
.as_ref()
.ok_or_else(|| Error::custom("Missing 'latest_stable' section in latest.toml".to_string()))?;
let version = latest_stable
.version
.as_deref()
.ok_or_else(|| Error::custom("Missing 'version' in latest_stable section of latest.toml".to_string()))?;
let rel_path = latest_stable
.rel_path
.as_deref()
.ok_or_else(|| Error::custom("Missing 'rel_path' in latest_stable section of latest.toml".to_string()))?;
Ok((version, rel_path))
}
}
pub struct InstalledPack {
pub pack_toml: PackToml,
pub path: SPath,
#[allow(unused)]
pub size: usize,
pub zip_size: usize,
}
pub async fn install_pack(dir_context: &DirContext, pack_uri: &str) -> Result<InstalledPack> {
let pack_uri = PackUri::parse(pack_uri);
let (aipack_zipped_file, pack_uri) = match pack_uri {
pack_uri @ PackUri::RepoPack(_) => download_from_repo(dir_context, pack_uri).await?,
pack_uri @ PackUri::LocalPath(_) => resolve_local_path(dir_context, pack_uri)?,
pack_uri @ PackUri::HttpLink(_) => download_pack(dir_context, pack_uri).await?,
};
support::validate_aipack_file(&aipack_zipped_file, &pack_uri.to_string())?;
let zip_size = support::get_file_size(&aipack_zipped_file, &pack_uri.to_string())?;
let mut installed_pack = install_aipack_file(dir_context, &aipack_zipped_file, &pack_uri)?;
installed_pack.zip_size = zip_size;
Ok(installed_pack)
}
async fn download_from_repo(dir_context: &DirContext, pack_uri: PackUri) -> Result<(SPath, PackUri)> {
if let PackUri::RepoPack(ref pack_identity) = pack_uri {
let latest_toml_url = format!(
"https://repo.aipack.ai/pack/{}/{}/stable/latest.toml",
pack_identity.namespace, pack_identity.name
);
let client = Client::new();
let response = client.get(&latest_toml_url).send().await.map_err(|e| Error::FailToInstall {
aipack_ref: pack_uri.to_string(),
cause: format!("Failed to download latest.toml: {}", e),
})?;
if !response.status().is_success() {
return Err(Error::FailToInstall {
aipack_ref: pack_uri.to_string(),
cause: format!("HTTP error when fetching latest.toml: {}", response.status()),
});
}
let latest_toml_content = response.text().await.map_err(|e| Error::FailToInstall {
aipack_ref: pack_uri.to_string(),
cause: format!("Failed to read latest.toml content: {}", e),
})?;
let latest_toml: LatestToml = toml::from_str(&latest_toml_content).map_err(|e| Error::FailToInstall {
aipack_ref: pack_uri.to_string(),
cause: format!("Failed to parse latest.toml: {}", e),
})?;
let (_version, rel_path) = latest_toml.validate()?;
let base_url = format!(
"https://repo.aipack.ai/pack/{}/{}/stable/",
pack_identity.namespace, pack_identity.name
);
let aipack_url = format!("{}{}", base_url, rel_path);
let http_uri = PackUri::HttpLink(aipack_url);
let (aipack_file, _) = download_pack(dir_context, http_uri).await?;
return Ok((aipack_file, pack_uri));
}
Err(Error::custom(
"Expected RepoPack variant but got a different one".to_string(),
))
}
fn resolve_local_path(dir_context: &DirContext, pack_uri: PackUri) -> Result<(SPath, PackUri)> {
if let PackUri::LocalPath(ref path) = pack_uri {
let aipack_zipped_file = SPath::from(path);
if aipack_zipped_file.path().is_absolute() {
Ok((aipack_zipped_file, pack_uri))
} else {
let absolute_path = dir_context.current_dir().join(aipack_zipped_file.as_str());
Ok((absolute_path, pack_uri))
}
} else {
Err(Error::custom(
"Expected LocalPath variant but got a different one".to_string(),
))
}
}
async fn download_pack(dir_context: &DirContext, pack_uri: PackUri) -> Result<(SPath, PackUri)> {
if let PackUri::HttpLink(ref url) = pack_uri {
let download_dir = dir_context.aipack_paths().get_base_pack_download_dir()?;
if !download_dir.exists() {
ensure_dir(&download_dir)?;
}
let url_path = url.split('/').next_back().unwrap_or("unknown.aipack");
let filename = url_path.replace(' ', "-");
let now = OffsetDateTime::now_utc();
let now = if let Ok(local) = time_tz::system::get_timezone() {
now.to_timezone(local)
} else {
now
};
let timestamp =
now.format(&time::format_description::well_known::Rfc3339)
.map_err(|e| Error::FailToInstall {
aipack_ref: pack_uri.to_string(),
cause: format!("Failed to format timestamp: {}", e),
})?;
let file_timestamp = timestamp.replace([':', 'T'], "-");
let file_timestamp = file_timestamp.split('.').next().unwrap_or(timestamp.as_str());
let timestamped_filename = format!("{}-{}", file_timestamp, filename);
let download_path = download_dir.join(×tamped_filename);
let client = Client::new();
let response = client.get(url).send().await.map_err(|e| Error::FailToInstall {
aipack_ref: pack_uri.to_string(),
cause: format!("Failed to download file: {}", e),
})?;
if !response.status().is_success() {
return Err(Error::FailToInstall {
aipack_ref: pack_uri.to_string(),
cause: format!("HTTP error: {}", response.status()),
});
}
let mut stream = response.bytes_stream();
use tokio::fs::File as TokioFile;
use tokio::io::AsyncWriteExt;
let mut file = TokioFile::create(download_path.path())
.await
.map_err(|e| Error::FailToInstall {
aipack_ref: pack_uri.to_string(),
cause: format!("Failed to create file: {}", e),
})?;
while let Some(chunk_result) = tokio_stream::StreamExt::next(&mut stream).await {
let chunk = chunk_result.map_err(|e| Error::FailToInstall {
aipack_ref: pack_uri.to_string(),
cause: format!("Failed to download chunk: {}", e),
})?;
file.write_all(&chunk).await.map_err(|e| Error::FailToInstall {
aipack_ref: pack_uri.to_string(),
cause: format!("Failed to write chunk to file: {}", e),
})?;
}
file.flush().await.map_err(|e| Error::FailToInstall {
aipack_ref: pack_uri.to_string(),
cause: format!("Failed to flush file: {}", e),
})?;
return Ok((download_path, pack_uri));
}
Err(Error::custom(
"Expected HttpLink variant but got a different one".to_string(),
))
}
fn install_aipack_file(
dir_context: &DirContext,
aipack_zipped_file: &SPath,
pack_uri: &PackUri,
) -> Result<InstalledPack> {
let pack_installed_dir = dir_context.aipack_paths().get_base_pack_installed_dir()?;
ensure_dir(&pack_installed_dir)?;
if !pack_installed_dir.exists() {
return Err(Error::FailToInstall {
aipack_ref: pack_uri.to_string(),
cause: format!(
"aipack base directory '{pack_installed_dir}' not found.\n recommendation: Run 'aip init'"
),
});
}
let new_pack_toml = support::extract_pack_toml_from_pack_file(aipack_zipped_file)?;
support::validate_version_for_install(&new_pack_toml.version)?;
let potential_existing_path = pack_installed_dir.join(&new_pack_toml.namespace).join(&new_pack_toml.name);
if potential_existing_path.exists() {
let existing_pack_toml_path = potential_existing_path.join("pack.toml");
if existing_pack_toml_path.exists() {
let existing_toml_content =
std::fs::read_to_string(existing_pack_toml_path.path()).map_err(|e| Error::FailToInstall {
aipack_ref: pack_uri.to_string(),
cause: format!("Failed to read existing pack.toml: {}", e),
})?;
let existing_pack_toml =
parse_validate_pack_toml(&existing_toml_content, existing_pack_toml_path.as_str())?;
support::validate_version_update(&existing_pack_toml.version, &new_pack_toml.version)?;
}
}
let pack_target_dir = pack_installed_dir.join(&new_pack_toml.namespace).join(&new_pack_toml.name);
if pack_target_dir.exists() {
std::fs::remove_dir_all(pack_target_dir.path()).map_err(|e| Error::FailToInstall {
aipack_ref: pack_uri.to_string(),
cause: format!("Failed to remove existing pack directory: {}", e),
})?;
}
zip::unzip_file(aipack_zipped_file, &pack_target_dir).map_err(|e| Error::FailToInstall {
aipack_ref: pack_uri.to_string(),
cause: format!("Failed to unzip pack: {}", e),
})?;
let size = support::calculate_directory_size(&pack_target_dir)?;
Ok(InstalledPack {
pack_toml: new_pack_toml,
path: pack_target_dir,
size,
zip_size: 0, })
}
#[cfg(test)]
#[path = "../_tests/tests_installer_impl.rs"]
mod tests_installer_impl;