Move downloading part to downloader subdir in repository

feature/lookup-installed
trivernis 2 years ago
parent a10fa5c45e
commit 3ad9790bc7
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

@ -4,19 +4,6 @@ use miette::{Diagnostic, NamedSource, SourceSpan};
use thiserror::Error; use thiserror::Error;
use crate::repository::extract::ExtractError;
#[derive(Debug, Error, Diagnostic)]
pub enum Error {
#[diagnostic(code(nenv::extract))]
#[error("The node archive could not be extracted")]
Extract(#[from] ExtractError),
#[diagnostic(code(nenv::version))]
#[error("The passed version is invalid")]
Version(#[from] VersionError),
}
#[derive(Debug, Error, Diagnostic)] #[derive(Debug, Error, Diagnostic)]
#[error("{detail}")] #[error("{detail}")]
#[diagnostic(code(nenv::version), help("Make sure there's no typo in the version."))] #[diagnostic(code(nenv::version), help("Make sure there's no typo in the version."))]

@ -10,7 +10,6 @@ pub mod error;
pub mod mapper; pub mod mapper;
pub mod repository; pub mod repository;
mod utils; mod utils;
mod web_api;
use miette::Result; use miette::Result;
use tracing::metadata::LevelFilter; use tracing::metadata::LevelFilter;
use tracing_subscriber::fmt::format::FmtSpan; use tracing_subscriber::fmt::format::FmtSpan;

@ -0,0 +1,128 @@
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()
}
}

@ -1,34 +1,26 @@
use core::fmt; use core::fmt;
use std::{ use std::{path::PathBuf, str::FromStr};
path::{Path, PathBuf},
str::FromStr,
};
use futures::future; use futures::future;
use semver::{Version, VersionReq}; use semver::{Version, VersionReq};
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use tokio::{ use tokio::fs;
fs::{self, File},
io::BufWriter,
};
use crate::{ use crate::{
config::ConfigAccess, config::ConfigAccess,
consts::{ consts::{ARCH, BIN_DIR, CACHE_DIR, CFG_DIR, DATA_DIR, NODE_VERSIONS_DIR, OS},
ARCH, BIN_DIR, CACHE_DIR, CFG_DIR, DATA_DIR, NODE_ARCHIVE_SUFFIX, NODE_VERSIONS_DIR, OS,
},
error::VersionError, error::VersionError,
web_api::WebApi,
}; };
use miette::{IntoDiagnostic, Result}; use miette::{IntoDiagnostic, Result};
use self::{ use self::{
downloader::NodeDownloader,
node_path::NodePath, node_path::NodePath,
versions::{SimpleVersion, SimpleVersionInfo, Versions}, versions::{SimpleVersion, SimpleVersionInfo, Versions},
}; };
pub(crate) mod extract; pub mod downloader;
pub(crate) mod node_path; pub(crate) mod node_path;
pub mod versions; pub mod versions;
@ -89,7 +81,7 @@ impl fmt::Display for NodeVersion {
pub struct Repository { pub struct Repository {
versions: Versions, versions: Versions,
web_api: WebApi, downloader: NodeDownloader,
} }
impl Repository { impl Repository {
@ -97,10 +89,13 @@ impl Repository {
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
pub async fn init(config: ConfigAccess) -> Result<Self> { pub async fn init(config: ConfigAccess) -> Result<Self> {
Self::create_folders().await?; Self::create_folders().await?;
let web_api = WebApi::new(&config.get().await.download.dist_base_url); let downloader = NodeDownloader::new(config.clone());
let versions = load_versions(&web_api).await?; let versions = load_versions(&downloader).await?;
Ok(Self { web_api, versions }) Ok(Self {
downloader,
versions,
})
} }
#[tracing::instrument(level = "debug")] #[tracing::instrument(level = "debug")]
@ -165,6 +160,13 @@ impl Repository {
Ok(build_version_path(&info.version).exists()) Ok(build_version_path(&info.version).exists())
} }
/// Installs the given node version
#[tracing::instrument(level = "debug", skip(self))]
pub async fn install_version(&self, version: &NodeVersion) -> Result<()> {
let info = self.lookup_version(version)?;
self.downloader.download(&info.version).await
}
/// Performs a lookup for the given node version /// Performs a lookup for the given node version
#[tracing::instrument(level = "debug", skip(self))] #[tracing::instrument(level = "debug", skip(self))]
pub fn lookup_version( pub fn lookup_version(
@ -192,49 +194,15 @@ impl Repository {
pub fn all_versions(&self) -> &Versions { pub fn all_versions(&self) -> &Versions {
&self.versions &self.versions
} }
/// Installs a specified node version
#[tracing::instrument(level = "debug", skip(self))]
pub async fn install_version(&self, version_req: &NodeVersion) -> Result<()> {
let info = self.lookup_version(version_req)?;
let archive_path = self.download_version(&info.version).await?;
self.extract_archive(&info.version, &archive_path)?;
Ok(())
}
#[tracing::instrument(level = "debug", skip(self))]
async fn download_version(&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.web_api
.download_version(version.to_string(), &mut download_writer)
.await?;
Ok(download_path)
}
#[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(())
}
} }
#[inline] #[inline]
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
async fn load_versions(web_api: &WebApi) -> Result<Versions> { async fn load_versions(downloader: &NodeDownloader) -> Result<Versions> {
let versions = if let Some(v) = Versions::load().await { let versions = if let Some(v) = Versions::load().await {
v v
} else { } else {
let all_versions = web_api.get_versions().await?; let all_versions = downloader.get_versions().await?;
let v = Versions::new(all_versions); let v = Versions::new(all_versions);
v.save().await?; v.save().await?;
v v

@ -5,9 +5,11 @@ use serde::{Deserialize, Serialize};
use std::fmt::Debug; use std::fmt::Debug;
use tokio::fs; use tokio::fs;
use crate::{consts::VERSION_FILE_PATH, error::SerializeBincodeError, web_api::VersionInfo}; use crate::{consts::VERSION_FILE_PATH, error::SerializeBincodeError};
use miette::{Context, IntoDiagnostic, Result}; use miette::{Context, IntoDiagnostic, Result};
use super::downloader::VersionInfo;
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
pub struct Versions { pub struct Versions {
lts_versions: HashMap<String, u16>, lts_versions: HashMap<String, u16>,

@ -1,96 +0,0 @@
use std::{
cmp::min,
fmt::{Debug, Display},
};
use crate::{
consts::{NODE_ARCHIVE_SUFFIX, NODE_DIST_URL},
error::ReqwestError,
utils::progress_bar,
};
mod model;
use futures_util::StreamExt;
use miette::{miette, Context, IntoDiagnostic, Result};
pub use model::*;
use tokio::io::{AsyncWrite, AsyncWriteExt};
#[cfg(test)]
mod test;
#[derive(Clone, Debug)]
pub struct WebApi {
base_url: String,
}
impl Default for WebApi {
fn default() -> Self {
Self::new(NODE_DIST_URL)
}
}
impl WebApi {
/// Creates a new instance to access the nodejs website
pub fn new<S: ToString>(base_url: S) -> Self {
Self {
base_url: base_url.to_string(),
}
}
/// Returns the list of available node versions
#[tracing::instrument(level = "debug")]
pub async fn get_versions(&self) -> Result<Vec<VersionInfo>> {
let versions = reqwest::get(format!("{}/index.json", self.base_url))
.await
.map_err(ReqwestError::from)
.context("Fetching versions")?
.json()
.await
.map_err(ReqwestError::from)
.context("Parsing versions response")?;
Ok(versions)
}
/// Downloads a specific node version
/// and writes it to the given writer
#[tracing::instrument(level = "debug", skip(writer))]
pub async fn download_version<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, *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)
}
}

@ -1,19 +0,0 @@
use tokio::io::sink;
use super::WebApi;
#[tokio::test]
async fn it_fetches_all_versions() {
let versions = WebApi::default().get_versions().await.unwrap();
assert!(!versions.is_empty());
}
#[tokio::test]
async fn it_downloads_a_specific_version() {
let mut writer = sink();
let bytes_written = WebApi::default()
.download_version("15.0.0", &mut writer)
.await
.unwrap();
assert!(bytes_written > 0);
}
Loading…
Cancel
Save