You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nenv/src/repository/downloader/mod.rs

129 lines
3.9 KiB
Rust

use std::{
cmp::min,
fmt::Debug,
fmt::Display,
path::{Path, PathBuf},
};
use crate::{
config::ConfigAccess,
consts::{CACHE_DIR, NODE_ARCHIVE_SUFFIX, NODE_VERSIONS_DIR},
error::ReqwestError,
utils::progress_bar,
};
use super::versions::SimpleVersion;
use futures::StreamExt;
use miette::{miette, Context, IntoDiagnostic, Result};
use tokio::{
fs::File,
io::{AsyncWrite, AsyncWriteExt, BufWriter},
};
mod extract;
mod version_info;
pub use version_info::VersionInfo;
#[derive(Clone)]
pub struct NodeDownloader {
config: ConfigAccess,
}
impl NodeDownloader {
pub fn new(config: ConfigAccess) -> Self {
Self { config }
}
/// Returns the list of available node versions
#[tracing::instrument(level = "debug", skip(self))]
pub async fn get_versions(&self) -> Result<Vec<VersionInfo>> {
let versions = reqwest::get(format!("{}/index.json", self.base_url().await))
.await
.map_err(ReqwestError::from)
.context("Fetching versions")?
.json()
.await
.map_err(ReqwestError::from)
.context("Parsing versions response")?;
Ok(versions)
}
/// Downloads a specified node version to the repository
#[tracing::instrument(level = "debug", skip(self))]
pub async fn download(&self, version: &SimpleVersion) -> Result<()> {
let archive_path = self.download_archive_to_cache(version).await?;
self.extract_archive(version, &archive_path)?;
Ok(())
}
#[tracing::instrument(level = "debug", skip(self))]
fn extract_archive(&self, version: &SimpleVersion, archive_path: &Path) -> Result<()> {
let dst_path = NODE_VERSIONS_DIR.join(version.to_string());
extract::extract_file(archive_path, &dst_path)?;
Ok(())
}
#[tracing::instrument(level = "debug", skip(self))]
async fn download_archive_to_cache(&self, version: &SimpleVersion) -> Result<PathBuf> {
let download_path = CACHE_DIR.join(format!("node-v{}{}", version, *NODE_ARCHIVE_SUFFIX));
if download_path.exists() {
return Ok(download_path);
}
let mut download_writer =
BufWriter::new(File::create(&download_path).await.into_diagnostic()?);
self.download_archive(version.to_string(), &mut download_writer)
.await?;
Ok(download_path)
}
/// Downloads a specific node version
/// and writes it to the given writer
#[tracing::instrument(level = "debug", skip(self, writer))]
pub async fn download_archive<W: AsyncWrite + Unpin, S: Display + Debug>(
&self,
version: S,
writer: &mut W,
) -> Result<u64> {
let res = reqwest::get(format!(
"{}/v{version}/node-v{version}{}",
self.base_url().await,
*NODE_ARCHIVE_SUFFIX
))
.await
.map_err(ReqwestError::from)
.context("Downloading nodejs")?;
let total_size = res
.content_length()
.ok_or_else(|| miette!("Missing content_length header"))?;
let pb = progress_bar(total_size);
pb.set_message(format!("Downloading node v{version}"));
let mut stream = res.bytes_stream();
let mut total_downloaded = 0;
while let Some(item) = stream.next().await {
let chunk = item.map_err(ReqwestError::from)?;
writer
.write_all(&chunk)
.await
.into_diagnostic()
.context("Writing download chunk to file")?;
total_downloaded = min(chunk.len() as u64 + total_downloaded, total_size);
pb.set_position(total_downloaded);
}
writer.flush().await.into_diagnostic()?;
pb.finish_with_message(format!("Downloaded node v{version}."));
Ok(total_downloaded)
}
async fn base_url(&self) -> String {
self.config.get().await.download.dist_base_url.to_owned()
}
}