use std::{fs, io};
use std::cmp::min;
use std::fs::File;
use std::io::{Read, Write};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use anyhow::{anyhow, bail, Context};
use flate2::read::GzDecoder;
use indicatif::{ProgressBar, ProgressStyle};
use reqwest::Client;
use serde_json::Value;
use sha2::{Digest, Sha256};
use tracing::{debug, info};
use futures_util::StreamExt;
use crate::plugin_models::PactPluginManifest;
use crate::utils::os_and_arch;
use tar::Archive;
pub(crate) async fn fetch_json_from_url(source: &str, http_client: &Client) -> anyhow::Result<Value> {
info!(%source, "Fetching root document for source");
let response: Value = http_client.get(source)
.header("accept", "application/json")
.send()
.await.context("Fetching root document for source")?
.json()
.await.context("Parsing root JSON document for source")?;
debug!(?response, "Got response");
Ok(response)
}
pub async fn download_json_from_github(
http_client: &Client,
base_url: &str,
tag: &String,
filename: &str
) -> anyhow::Result<Value> {
let url = format!("{}/download/{}/{}", base_url, tag, filename);
debug!("Downloading JSON file from {}", url);
Ok(http_client.get(url)
.send()
.await?
.json()
.await?)
}
pub async fn download_plugin_executable(
manifest: &PactPluginManifest,
plugin_dir: &PathBuf,
http_client: &Client,
base_url: &str,
tag: &String,
display_progress: bool
) -> anyhow::Result<PathBuf> {
let (os, arch) = os_and_arch()?;
let ext = if os == "windows" { ".exe" } else { "" };
let gz_file = format!("pact-{}-plugin-{}-{}{}.gz", manifest.name, os, arch, ext);
let sha_file = format!("pact-{}-plugin-{}-{}{}.gz.sha256", manifest.name, os, arch, ext);
if github_file_exists(http_client, base_url, tag, gz_file.as_str()).await? {
debug!(file = %gz_file, "Found a GZipped file");
let file = download_file_from_github(http_client, base_url, tag, gz_file.as_str(), plugin_dir, display_progress).await?;
if github_file_exists(http_client, base_url, tag, sha_file.as_str()).await? {
let sha_file = download_file_from_github(http_client, base_url, tag, sha_file.as_str(), plugin_dir, display_progress).await?;
check_sha(&file, &sha_file)?;
fs::remove_file(sha_file)?;
}
let file = gunzip_file(&file, plugin_dir, manifest, ext)?;
#[cfg(unix)]
{
let mut perms = fs::metadata(&file)?.permissions();
perms.set_mode(0o775);
fs::set_permissions(&file, perms)?;
}
return Ok(file);
}
let zip_file = format!("pact-{}-plugin-{}-{}.zip", manifest.name, os, arch);
let zip_sha_file = format!("pact-{}-plugin-{}-{}.zip.sha256", manifest.name, os, arch);
if github_file_exists(http_client, base_url, tag, zip_file.as_str()).await? {
return download_zip_file(plugin_dir, http_client, base_url, tag, zip_file, zip_sha_file, display_progress).await;
}
let zip_file = format!("pact-{}-plugin.zip", manifest.name);
let zip_sha_file = format!("pact-{}-plugin.zip.sha256", manifest.name);
if github_file_exists(http_client, base_url, tag, zip_file.as_str()).await? {
return download_zip_file(plugin_dir, http_client, base_url, tag, zip_file, zip_sha_file, display_progress).await;
}
let tar_gz_file = format!("pact-{}-plugin.tar.gz", manifest.name);
let tar_gz_sha_file = format!("pact-{}-plugin.tar.gz.sha256", manifest.name);
if github_file_exists(http_client, base_url, tag, tar_gz_file.as_str()).await? {
return download_tar_gz_file(plugin_dir, http_client, base_url, tag, tar_gz_file, tar_gz_sha_file, display_progress).await;
}
let tar_gz_file = format!("pact-{}-plugin-{}-{}.tar.gz", manifest.name, os, arch);
let tar_gz_sha_file = format!("pact-{}-plugin-{}-{}.tar.gz.sha256", manifest.name, os, arch);
if github_file_exists(http_client, base_url, tag, tar_gz_file.as_str()).await? {
return download_tar_gz_file(plugin_dir, http_client, base_url, tag, tar_gz_file, tar_gz_sha_file, display_progress).await;
}
let tgz_file = format!("pact-{}-plugin-{}-{}.tgz", manifest.name, os, arch);
let tgz_sha_file = format!("pact-{}-plugin-{}-{}.tgz.sha256", manifest.name, os, arch);
if github_file_exists(http_client, base_url, tag, tgz_file.as_str()).await? {
return download_tar_gz_file(plugin_dir, http_client, base_url, tag, tgz_file, tgz_sha_file, display_progress).await;
}
let tgz_file = format!("pact-{}-plugin.tgz", manifest.name);
let tgz_sha_file = format!("pact-{}-plugin.tgz.sha256", manifest.name);
if github_file_exists(http_client, base_url, tag, tgz_file.as_str()).await? {
return download_tar_gz_file(plugin_dir, http_client, base_url, tag, tgz_file, tgz_sha_file, display_progress).await;
}
bail!("Did not find a matching file pattern on GitHub to install")
}
async fn github_file_exists(http_client: &Client, base_url: &str, tag: &String, filename: &str) -> anyhow::Result<bool> {
let url = format!("{}/download/{}/{}", base_url, tag, filename);
debug!("Checking existence of file from {}", url);
Ok(http_client.head(url)
.send()
.await?
.status().is_success())
}
pub async fn download_zip_file(
plugin_dir: &PathBuf,
http_client: &Client,
base_url: &str,
tag: &String,
zip_file: String,
zip_sha_file: String,
display_progress: bool
) -> anyhow::Result<PathBuf> {
debug!(file = %zip_file, "Found a Zip file");
let file = download_file_from_github(http_client, base_url, tag, zip_file.as_str(), plugin_dir, display_progress).await?;
if github_file_exists(http_client, base_url, tag, zip_sha_file.as_str()).await? {
let sha_file = download_file_from_github(http_client, base_url, tag, zip_sha_file.as_str(), plugin_dir, display_progress).await?;
check_sha(&file, &sha_file)?;
fs::remove_file(sha_file)?;
}
unzip_file(&file, plugin_dir)
}
pub async fn download_tar_gz_file(
plugin_dir: &PathBuf,
http_client: &Client,
base_url: &str,
tag: &String,
tar_gz_file: String,
tar_gz_sha_file: String,
display_progress: bool
) -> anyhow::Result<PathBuf> {
debug!(file = %tar_gz_file, "Found a tar gz file");
let file = download_file_from_github(http_client, base_url, tag, tar_gz_file.as_str(), plugin_dir, display_progress).await?;
if github_file_exists(http_client, base_url, tag, tar_gz_sha_file.as_str()).await? {
let sha_file = download_file_from_github(http_client, base_url, tag, tar_gz_sha_file.as_str(), plugin_dir, display_progress).await?;
check_sha(&file, &sha_file)?;
fs::remove_file(sha_file)?;
}
extract_tar_gz(&file, plugin_dir)
}
fn unzip_file(zip_file: &PathBuf, plugin_dir: &PathBuf) -> anyhow::Result<PathBuf> {
let mut archive = zip::ZipArchive::new(File::open(zip_file)?)?;
for i in 0..archive.len() {
let mut file = archive.by_index(i).unwrap();
let outpath = match file.enclosed_name() {
Some(path) => plugin_dir.join(path),
None => continue
};
if (*file.name()).ends_with('/') {
debug!("Dir {} extracted to \"{}\"", i, outpath.display());
fs::create_dir_all(&outpath)?;
} else {
debug!("File {} extracted to \"{}\" ({} bytes)", i, outpath.display(), file.size());
if let Some(p) = outpath.parent() {
if !p.exists() {
fs::create_dir_all(&p)?;
}
}
let mut outfile = File::create(&outpath)?;
io::copy(&mut file, &mut outfile)?;
}
#[cfg(unix)]
{
if let Some(mode) = file.unix_mode() {
fs::set_permissions(&outpath, fs::Permissions::from_mode(mode))?;
}
}
}
Ok(plugin_dir.clone())
}
fn gunzip_file(
gz_file: &PathBuf,
plugin_dir: &PathBuf,
manifest: &PactPluginManifest,
ext: &str
) -> anyhow::Result<PathBuf> {
let file = if ext.is_empty() {
plugin_dir.join(&manifest.entry_point)
} else {
plugin_dir.join(&manifest.entry_point)
.with_extension(ext.strip_prefix('.').unwrap_or(ext))
};
let mut f = File::create(file.clone())?;
let mut gz = GzDecoder::new(File::open(gz_file)?);
let bytes = io::copy(&mut gz, &mut f)?;
debug!(file = %file.display(), "Wrote {} bytes", bytes);
fs::remove_file(gz_file)?;
Ok(file)
}
fn extract_tar_gz(tar_gz_file: &PathBuf, plugin_dir: &PathBuf) -> anyhow::Result<PathBuf> {
let file = File::open(tar_gz_file)?;
let gz_decoder = GzDecoder::new(file);
let mut archive = Archive::new(gz_decoder);
archive.unpack(plugin_dir)?;
debug!("Unpacked {:?} plugin", tar_gz_file);
fs::remove_file(tar_gz_file)?;
Ok(tar_gz_file.clone())
}
pub async fn download_file_from_github(
http_client: &Client,
base_url: &str,
tag: &String,
filename: &str,
plugin_dir: &PathBuf,
display_progress: bool
) -> anyhow::Result<PathBuf> {
let url = format!("{}/download/{}/{}", base_url, tag, filename);
debug!("Downloading file from {}", url);
let res = http_client.get(url.as_str()).send().await?;
let total_size = res.content_length()
.ok_or(anyhow!("Failed to get content length from '{}'", url))?;
let pb = ProgressBar::new(total_size);
if display_progress {
pb.set_style(
ProgressStyle::with_template("{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})")
.unwrap()
.progress_chars("#>-"));
pb.set_message(format!("Downloading {}", url));
}
let path = plugin_dir.join(filename);
let mut file = File::create(path.clone())?;
let mut downloaded: u64 = 0;
let mut stream = res.bytes_stream();
while let Some(item) = stream.next().await {
let chunk = item?;
file.write_all(&chunk)?;
let new = min(downloaded + (chunk.len() as u64), total_size);
downloaded = new;
if display_progress {
pb.set_position(new);
}
}
if display_progress {
pb.finish_with_message(format!("Downloaded {} to {}", url, path.display()));
}
debug!(url, downloaded_bytes = downloaded, "File downloaded OK");
Ok(path.clone())
}
pub fn check_sha(file: &PathBuf, sha_file: &PathBuf) -> anyhow::Result<()> {
debug!(file = %file.display(), sha_file = %sha_file.display(), "Checking SHA of downloaded file");
let sha = fs::read_to_string(sha_file).context("Could not read SHA file")?;
let sha = sha.split(' ').next().ok_or(anyhow!("SHA file is not correctly formatted"))?;
debug!("Downloaded SHA {}", sha);
let mut hasher = Sha256::new();
let mut f = File::open(file.clone())?;
let mut buffer = [0_u8; 256];
let mut done = false;
while !done {
let amount = f.read(&mut buffer)?;
if amount == 0 {
done = true;
} else if amount == 256 {
hasher.update(&buffer);
} else {
let b = &buffer[0..amount];
hasher.update(b);
}
}
let result = hasher.finalize();
let calculated = format!("{:x}", result);
debug!("Calculated SHA {}", calculated);
if calculated == sha {
Ok(())
} else {
Err(anyhow!("Downloaded file {} has a checksum mismatch: {} != {}",
file.display(), sha, calculated))
}
}