use std::{
fs::File,
io::{BufRead, BufReader, Write},
path::Path,
time::Duration,
};
use anyhow::{anyhow, Context, Result};
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
use reqwest::{blocking::Client, Url};
use sha2::{Digest, Sha256};
use crate::{
manifest::{Artifact, Manifest},
spec::InstallSpec,
};
const DEFAULT_BASE_URL: &str = "https://rialo-artifacts.s3.us-east-2.amazonaws.com/binaries/";
const BASE_URL_ENV: &str = "RIALO_BINARIES_DIST_BASE";
#[derive(Debug, Clone)]
pub struct RemoteClient {
http: Client,
base_url: Url,
}
impl RemoteClient {
pub fn new() -> Result<Self> {
let base = std::env::var(BASE_URL_ENV).unwrap_or_else(|_| DEFAULT_BASE_URL.to_owned());
let base_url = Url::parse(&base).context("invalid base distribution URL")?;
let http = Client::builder().timeout(Duration::from_secs(30)).build()?;
Ok(Self { http, base_url })
}
fn manifest_url(&self, spec: &InstallSpec) -> Result<Url> {
let path = match spec.version.as_ref() {
None => format!("{}/latest.json", spec.channel.as_str()),
Some(version) => format!("{}/{}/manifest.json", spec.channel.as_str(), version),
};
self.base_url
.join(&path)
.context("failed to construct manifest URL")
}
pub fn fetch_manifest(&self, spec: &InstallSpec) -> Result<Manifest> {
let url = self.manifest_url(spec)?;
let res = self
.http
.get(url.clone())
.send()
.with_context(|| format!("failed to fetch manifest from {url}"))?;
if !res.status().is_success() {
return Err(anyhow!(
"manifest fetch failed with status {}",
res.status()
));
}
let manifest: Manifest = res.json().context("failed to parse manifest JSON")?;
Ok(manifest)
}
pub fn download_artifact(&self, artifact: &Artifact, dest: &Path) -> Result<()> {
let url = self
.base_url
.join(&artifact.path)
.context("failed to build artifact URL")?;
let resp = self
.http
.get(url.clone())
.send()
.with_context(|| format!("failed to download artifact from {url}"))?;
if !resp.status().is_success() {
return Err(anyhow!(
"artifact download failed with status {}",
resp.status()
));
}
let total = resp.content_length();
let pb = ProgressBar::with_draw_target(total, ProgressDrawTarget::stderr());
pb.set_style(
ProgressStyle::with_template("{spinner:.green} {msg} {bytes}/{total_bytes} ({eta})")
.unwrap()
.progress_chars("=>-"),
);
pb.set_message(format!("Downloading {}", artifact.archive));
let mut file = File::create(dest).with_context(|| {
format!("failed to create temp artifact file at {}", dest.display())
})?;
let mut hasher = Sha256::new();
let mut reader = BufReader::with_capacity(1024 * 1024, resp);
loop {
let chunk = reader.fill_buf()?;
if chunk.is_empty() {
break;
}
file.write_all(chunk)?;
hasher.write_all(chunk)?;
let len = chunk.len();
reader.consume(len);
pb.inc(len as u64);
}
pb.finish_with_message(format!("Downloaded {}", artifact.archive));
let calculated = hex::encode(hasher.finalize());
if !calculated.eq_ignore_ascii_case(&artifact.sha256) {
return Err(anyhow!(
"sha256 mismatch for artifact {} (expected {}, got {})",
artifact.archive,
artifact.sha256,
calculated
));
}
Ok(())
}
}
pub fn extract_archive(archive_path: &Path, destination: &Path) -> Result<()> {
let file = File::open(archive_path)
.with_context(|| format!("failed to open archive at {}", archive_path.display()))?;
let decoder = flate2::read::GzDecoder::new(file);
let mut archive = tar::Archive::new(decoder);
archive.unpack(destination).with_context(|| {
format!(
"failed to extract archive {} into {}",
archive_path.display(),
destination.display()
)
})
}