use std::cmp::min;
use std::env;
use std::path::Path;
use anyhow::anyhow;
use anyhow::Error;
use anyhow::Result;
use clap::Parser;
use futures_util::stream::StreamExt;
use indicatif::ProgressBar;
use reqwest::Client;
use tokio::io::AsyncWriteExt;
use tracing::info;
use super::PostDownloadVersionType;
use crate::fs::copy_package_proxy;
use crate::fs::get_downloads_directory;
use crate::fs::get_file_type;
use crate::fs::unarchive;
use crate::helpers::version::is_version_installed;
use crate::helpers::version::LocalVersion;
use crate::helpers::version::ParsedVersion;
use crate::helpers::version::VersionType;
use crate::packages::Package;
use crate::packages::PackageType;
#[derive(Parser)]
pub struct Args {
#[command(subcommand)]
command: Commands,
}
#[derive(Parser)]
pub enum Commands {
Reth { version: String },
Oura { version: String },
Aiken { version: String },
Dolos { version: String },
Zellij { version: String },
NeoVim { version: String },
Jujutsu { version: String },
Mithril { version: String },
Scrolls { version: String },
CardanoCli { version: String },
CardanoNode { version: String },
SidechainCli { version: String },
PartnerChainCli { version: String },
PartnerChainNode { version: String },
CardanoSubmitApi { version: String },
}
macro_rules! execute {
($command:expr, $client:expr, $(($variant:ident, $package_type:expr)),*) => {
match $command {
$(
Commands::$variant { version } => {
let package = Package::new($package_type, version, $client).await;
install($client, package).await.expect("Failed to install");
}
)*
}
}
}
pub async fn run(
args: Args,
_ctx: &crate::Context,
client: Option<&reqwest::Client>,
) -> miette::Result<()> {
execute!(
args.command,
client,
(Reth, PackageType::Reth),
(Oura, PackageType::Oura),
(Aiken, PackageType::Aiken),
(Dolos, PackageType::Dolos),
(Zellij, PackageType::Zellij),
(NeoVim, PackageType::Neovim),
(Jujutsu, PackageType::Jujutsu),
(Mithril, PackageType::Mithril),
(Scrolls, PackageType::Scrolls),
(CardanoCli, PackageType::CardanoCli),
(CardanoNode, PackageType::CardanoNode),
(SidechainCli, PackageType::SidechainCli),
(PartnerChainCli, PackageType::PartnerChainCli),
(PartnerChainNode, PackageType::PartnerChainNode),
(CardanoSubmitApi, PackageType::CardanoSubmitApi)
);
Ok(())
}
pub async fn install(client: Option<&Client>, package: Package) -> Result<(), Error> {
let version = package.version().map_or_else(|| Err(anyhow!("No version specified")), Ok)?;
let root = get_downloads_directory(package.clone()).await?;
env::set_current_dir(&root)?;
let root = root.as_path();
let is_version_installed = is_version_installed(&version.tag_name, package.clone()).await?;
copy_package_proxy(package.clone()).await?;
if is_version_installed {
info!("{} is already installed.", version.tag_name);
return Ok(());
}
let downloaded_file = match version.version_type {
VersionType::Normal | VersionType::Latest => {
download_version(client, &version, root, package.clone()).await?
}
};
match downloaded_file {
PostDownloadVersionType::Standard(local_version) => {
unarchive(package, local_version).await?;
}
}
info!("Successfully installed {}", version.tag_name);
Ok(())
}
async fn download_version(
client: Option<&Client>,
version: &ParsedVersion,
root: &Path,
package: Package,
) -> Result<PostDownloadVersionType> {
let response = send_request(client, package.clone()).await?;
if response.status() != reqwest::StatusCode::OK {
return Err(anyhow!("Failed to send request to download version"));
}
let mut downloaded: u64 = 0;
let content_length = get_content_length(&response).await?;
let pb = ProgressBar::new(content_length);
let mut response_bytes = response.bytes_stream();
let package_type = package.package_type();
let file_type = get_file_type(package_type);
let file_path = create_file_path(version, root, file_type);
let mut file = create_file(&file_path).await?;
while let Some(item) = response_bytes.next().await {
let chunk = item.map_err(|_| anyhow!("Failed to get chunk"))?;
file.write_all(&chunk).await?;
let new = min(downloaded + (chunk.len() as u64), content_length);
downloaded = new;
pb.set_position(new);
}
pb.finish_with_message(format!(
"Downloaded version {} to {}",
version.tag_name, file_path
));
let local_version = LocalVersion {
file_name: version.tag_name.to_owned(),
file_format: file_type.to_string(),
path: root.display().to_string(),
semver: version.semver.clone(),
};
Ok(PostDownloadVersionType::Standard(local_version))
}
async fn get_content_length(response: &reqwest::Response) -> Result<u64> {
let content_length = response.content_length();
content_length.ok_or(anyhow!("Failed to get content length of the response"))
}
async fn create_file(file_path: &str) -> Result<tokio::fs::File> {
Ok(tokio::fs::File::create(&file_path).await?)
}
fn create_file_path(version: &ParsedVersion, root: &Path, file_type: &str) -> String {
format!("{}/{}.{}", root.display(), version.tag_name, file_type)
}
async fn send_request(
client: Option<&Client>,
package: Package,
) -> Result<reqwest::Response, reqwest::Error> {
let package_url = package.download_url();
info!("Downloading: {}", package_url);
client
.expect("Client is not set")
.get(package_url.to_string())
.header("user-agent", "hyper-jump")
.send()
.await
}