Compare commits

...

7 Commits

@ -20,6 +20,10 @@ pub enum Command {
#[command(short_flag = 'v', aliases = &["--version"])]
Version,
/// Initializes nenv directories and installs a default node version
#[command()]
Init,
/// Installs the given node version
#[command()]
Install(InstallArgs),
@ -28,10 +32,12 @@ pub enum Command {
#[command()]
Default(DefaultArgs),
/// Refreshes the node environment mappings and cache.
/// This will erase all binary mappings not relevant to the current node version.
/// Creates wrapper scripts for node binaries
/// so they can be found in the path and are executed
/// with the correct node version. This will delete
/// all binary wrappers that don't apply to the active node version.
#[command()]
Refresh,
RemapBinaries,
/// Lists all available versions
#[command(name = "list-versions")]
@ -40,6 +46,10 @@ pub enum Command {
/// Executes the given version specific node executable
#[command()]
Exec(ExecArgs),
/// Clears the download cache
#[command()]
ClearCache,
}
#[derive(Clone, Debug, Parser)]

@ -15,7 +15,6 @@ pub struct Config {
pub struct NodeConfig {
/// The default version if no version is specified
/// in the `package.json` file or `NODE_VERSION` environment variable
#[serde(with = "NodeVersion")]
pub default_version: NodeVersion,
}

@ -14,7 +14,7 @@ lazy_static! {
.unwrap_or_else(|| PathBuf::from(".cache"))
.join(PathBuf::from("nenv"));
pub static ref CFG_FILE_PATH: PathBuf = CFG_DIR.join("config.toml");
pub static ref VERSION_FILE_PATH: PathBuf = DATA_DIR.join("versions.cache");
pub static ref VERSION_FILE_PATH: PathBuf = CACHE_DIR.join("versions.cache");
pub static ref BIN_DIR: PathBuf = DATA_DIR.join("bin");
pub static ref NODE_VERSIONS_DIR: PathBuf = DATA_DIR.join("versions");
pub static ref NODE_ARCHIVE_SUFFIX: String = format!("-{OS}-{ARCH}.{ARCHIVE_TYPE}");

@ -4,19 +4,6 @@ use miette::{Diagnostic, NamedSource, SourceSpan};
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)]
#[error("{detail}")]
#[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 repository;
mod utils;
mod web_api;
use miette::Result;
use tracing::metadata::LevelFilter;
use tracing_subscriber::fmt::format::FmtSpan;
@ -20,6 +19,7 @@ mod args;
mod config;
mod nenv;
mod version_detection;
mod versioning;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
@ -45,8 +45,10 @@ async fn main() -> Result<()> {
process::exit(exit_code);
}
args::Command::Refresh => nenv.refresh().await,
args::Command::RemapBinaries => nenv.remap().await,
args::Command::ListVersions => nenv.list_versions().await,
args::Command::Init => nenv.init_nenv().await,
args::Command::ClearCache => nenv.clear_cache().await,
_ => xkcd_unreachable!(),
}?;

@ -1,8 +1,8 @@
use std::ffi::OsString;
use std::{ffi::OsString, str::FromStr};
use crate::{
config::ConfigAccess,
consts::VERSION_FILE_PATH,
consts::{BIN_DIR, CACHE_DIR, VERSION_FILE_PATH},
error::VersionError,
mapper::Mapper,
repository::{NodeVersion, Repository},
@ -10,7 +10,8 @@ use crate::{
version_detection::{self, VersionDetector},
};
use crossterm::style::Stylize;
use miette::{IntoDiagnostic, Result};
use dialoguer::{theme::ColorfulTheme, Input, Select};
use miette::{Context, IntoDiagnostic, Result};
use tokio::fs;
pub struct Nenv {
@ -40,7 +41,7 @@ impl Nenv {
pub async fn install(&mut self, version: NodeVersion) -> Result<()> {
Self::clear_version_cache().await?;
if self.repo.is_installed(&version)?
if self.repo.is_installed(&version).await?
&& !prompt(
false,
format!(
@ -54,7 +55,7 @@ impl Nenv {
} else {
self.repo.install_version(&version).await?;
self.active_version = version.to_owned();
self.get_mapper()?.remap_additive().await?;
self.get_mapper().await?.remap_additive().await?;
println!("Installed {}", version.to_string().bold());
Ok(())
@ -66,20 +67,20 @@ impl Nenv {
pub async fn set_system_default(&mut self, version: NodeVersion) -> Result<()> {
self.active_version = version.to_owned();
if !self.repo.is_installed(&version)? {
if !self.repo.is_installed(&version).await? {
if prompt(
false,
format!("The version {version} is not installed. Do you want to install it?"),
) {
self.repo.install_version(&version).await?;
self.config.get_mut().await.node.default_version = version.to_owned();
self.get_mapper()?.remap_additive().await?;
self.get_mapper().await?.remap_additive().await?;
println!("Now using {}", version.to_string().bold());
}
Ok(())
} else {
self.get_mapper()?.remap_additive().await?;
self.get_mapper().await?.remap_additive().await?;
self.config.get_mut().await.node.default_version = version.to_owned();
println!("Now using {}", version.to_string().bold());
@ -89,27 +90,26 @@ impl Nenv {
/// Executes a given node executable for the currently active version
#[tracing::instrument(skip(self))]
pub async fn exec(&self, command: String, args: Vec<OsString>) -> Result<i32> {
if !self.repo.is_installed(&self.active_version)? {
pub async fn exec(&mut self, command: String, args: Vec<OsString>) -> Result<i32> {
if !self.repo.is_installed(&self.active_version).await? {
self.repo.install_version(&self.active_version).await?;
}
let exit_status = self.get_mapper()?.exec(command, args).await?;
let exit_status = self.get_mapper().await?.exec(command, args).await?;
Ok(exit_status.code().unwrap_or(0))
}
/// Clears the version cache and remaps all executables
#[tracing::instrument(skip(self))]
pub async fn refresh(&self) -> Result<()> {
Self::clear_version_cache().await?;
self.get_mapper()?.remap().await
pub async fn remap(&mut self) -> Result<()> {
self.get_mapper().await?.remap().await
}
/// Lists the currently installed versions
#[tracing::instrument(skip(self))]
pub async fn list_versions(&self) -> Result<()> {
pub async fn list_versions(&mut self) -> Result<()> {
let versions = self.repo.installed_versions().await?;
let active_version = self.repo.lookup_version(&self.active_version)?;
let active_version = self.repo.lookup_version(&self.active_version).await?;
let active_version = active_version.version.into();
println!("{}", "Installed versions:".bold());
@ -118,6 +118,7 @@ impl Nenv {
let info = self
.repo
.all_versions()
.await?
.get(&version)
.ok_or_else(|| VersionError::unknown_version(version.to_string()))?;
let lts = info
@ -136,6 +137,56 @@ impl Nenv {
Ok(())
}
/// Initializes nenv and prompts for a default version.
#[tracing::instrument(skip(self))]
pub async fn init_nenv(&mut self) -> Result<()> {
let items = vec!["latest", "lts", "custom"];
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Select a default node version")
.items(&items)
.default(0)
.interact()
.into_diagnostic()?;
let version = if items[selection] == "custom" {
let version_string: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter a version number: ")
.interact_text()
.into_diagnostic()?;
NodeVersion::from_str(&version_string).unwrap()
} else {
NodeVersion::from_str(items[selection]).unwrap()
};
self.repo.install_version(&version).await?;
println!("{}", "Initialized!".green());
println!(
"{}\n {}\n{}",
"Make sure to add".bold(),
BIN_DIR.to_string_lossy().yellow(),
"to your PATH environment variables.".bold()
);
Ok(())
}
/// Clears the download cache
#[tracing::instrument(skip(self))]
pub async fn clear_cache(&self) -> Result<()> {
fs::remove_dir_all(&*CACHE_DIR)
.await
.into_diagnostic()
.context("Removing cache directory")?;
fs::create_dir_all(&*CACHE_DIR)
.await
.into_diagnostic()
.context("Creating cache directory")?;
println!("Cleared download cache.");
Ok(())
}
/// Persits all changes made that aren't written to the disk yet
#[tracing::instrument(level = "debug", skip(self))]
pub async fn persist(&self) -> Result<()> {
@ -162,10 +213,11 @@ impl Nenv {
}
#[tracing::instrument(level = "debug", skip(self))]
fn get_mapper(&self) -> Result<Mapper> {
async fn get_mapper(&mut self) -> Result<Mapper> {
let node_path = self
.repo
.get_version_path(&self.active_version)?
.get_version_path(&self.active_version)
.await?
.ok_or_else(|| VersionError::not_installed(self.active_version.to_owned()))?;
Ok(Mapper::new(node_path))
}

@ -0,0 +1,150 @@
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,
versioning::SimpleVersion,
};
use self::versions::Versions;
use futures::StreamExt;
use miette::{miette, Context, IntoDiagnostic, Result};
use tokio::{
fs::File,
io::{AsyncWrite, AsyncWriteExt, BufWriter},
};
mod extract;
mod version_info;
pub mod versions;
pub use version_info::VersionInfo;
#[derive(Clone)]
pub struct NodeDownloader {
config: ConfigAccess,
versions: Option<Versions>,
}
impl NodeDownloader {
pub fn new(config: ConfigAccess) -> Self {
Self {
config,
versions: None,
}
}
/// Returns the list of available node versions
#[tracing::instrument(level = "debug", skip(self))]
pub async fn versions(&mut self) -> Result<&Versions> {
if self.versions.is_none() {
self.versions = Some(self.load_versions().await?);
}
Ok(self.versions.as_ref().unwrap())
}
async fn load_versions(&self) -> Result<Versions> {
let versions = if let Some(v) = Versions::load().await {
v
} else {
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")?;
let v = Versions::new(versions);
v.save().await?;
v
};
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,72 +1,29 @@
use std::{collections::HashMap, fmt::Display};
use std::collections::HashMap;
use semver::{Version, VersionReq};
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use tokio::fs;
use crate::{consts::VERSION_FILE_PATH, error::SerializeBincodeError, web_api::VersionInfo};
use crate::{
consts::VERSION_FILE_PATH,
error::SerializeBincodeError,
versioning::{SimpleVersion, VersionMetadata},
};
use miette::{Context, IntoDiagnostic, Result};
use super::VersionInfo;
#[derive(Clone, Serialize, Deserialize)]
pub struct Versions {
lts_versions: HashMap<String, u16>,
versions: HashMap<SimpleVersion, SimpleVersionInfo>,
versions: HashMap<SimpleVersion, VersionMetadata>,
// as this field is not serialized
// it needs to be calculated after serialization
#[serde(skip)]
sorted_versions: Vec<SimpleVersion>,
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize, Hash)]
pub struct SimpleVersion {
pub major: u16,
pub minor: u16,
pub patch: u32,
}
impl From<semver::Version> for SimpleVersion {
fn from(value: semver::Version) -> Self {
Self {
major: value.major as u16,
minor: value.minor as u16,
patch: value.patch as u32,
}
}
}
impl From<SimpleVersion> for semver::Version {
fn from(value: SimpleVersion) -> Self {
Self::new(value.major as u64, value.minor as u64, value.patch as u64)
}
}
impl Display for SimpleVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
major,
minor,
patch,
} = self;
write!(f, "{major}.{minor}.{patch}")
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct SimpleVersionInfo {
pub version: SimpleVersion,
pub lts: Option<String>,
}
impl From<VersionInfo> for SimpleVersionInfo {
fn from(value: VersionInfo) -> Self {
Self {
version: value.version.into(),
lts: value.lts.lts(),
}
}
}
impl Versions {
/// Loads the versions from the cached versions.json file
pub(crate) async fn load() -> Option<Self> {
@ -129,7 +86,7 @@ impl Versions {
/// Returns the latest known node version
#[tracing::instrument(level = "debug", skip_all)]
pub fn latest(&self) -> &SimpleVersionInfo {
pub fn latest(&self) -> &VersionMetadata {
self.versions
.get(self.sorted_versions.last().expect("No known node versions"))
.unwrap()
@ -137,7 +94,7 @@ impl Versions {
/// Returns the latest node lts version
#[tracing::instrument(level = "debug", skip_all)]
pub fn latest_lts(&self) -> &SimpleVersionInfo {
pub fn latest_lts(&self) -> &VersionMetadata {
let mut versions = self
.lts_versions
.values()
@ -149,14 +106,14 @@ impl Versions {
/// Returns a lts version by name
#[tracing::instrument(level = "debug", skip(self))]
pub fn get_lts<S: AsRef<str> + Debug>(&self, lts_name: S) -> Option<&SimpleVersionInfo> {
pub fn get_lts<S: AsRef<str> + Debug>(&self, lts_name: S) -> Option<&VersionMetadata> {
let lts_version = self.lts_versions.get(lts_name.as_ref())?;
self.get_latest_for_major(*lts_version)
}
/// Returns any version that fulfills the given requirement
#[tracing::instrument(level = "debug", skip(self))]
pub fn get_fulfilling(&self, req: &VersionReq) -> Option<&SimpleVersionInfo> {
pub fn get_fulfilling(&self, req: &VersionReq) -> Option<&VersionMetadata> {
let fulfilling_versions = self
.sorted_versions
.iter()
@ -165,18 +122,18 @@ impl Versions {
.collect::<Vec<_>>();
let version = fulfilling_versions.last()?.clone().into();
self.versions.get(&version).into()
self.versions.get(&version)
}
/// Returns the info for the given version
#[tracing::instrument(level = "debug", skip(self))]
pub fn get(&self, version: &Version) -> Option<&SimpleVersionInfo> {
pub fn get(&self, version: &Version) -> Option<&VersionMetadata> {
self.versions.get(&version.clone().into())
}
/// Returns any version that fulfills the given requirement
#[tracing::instrument(level = "debug", skip(self))]
fn get_latest_for_major(&self, major: u16) -> Option<&SimpleVersionInfo> {
fn get_latest_for_major(&self, major: u16) -> Option<&VersionMetadata> {
let fulfilling_versions = self
.sorted_versions
.iter()
@ -184,7 +141,7 @@ impl Versions {
.collect::<Vec<_>>();
let version = fulfilling_versions.last()?;
self.versions.get(&version).into()
self.versions.get(version)
}
/// Creates the list of sorted versions

@ -1,36 +1,28 @@
use core::fmt;
use std::{
path::{Path, PathBuf},
str::FromStr,
};
use std::{path::PathBuf, str::FromStr};
use futures::future;
use semver::{Version, VersionReq};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use tokio::{
fs::{self, File},
io::BufWriter,
};
use tokio::fs;
use crate::{
config::ConfigAccess,
consts::{
ARCH, BIN_DIR, CACHE_DIR, CFG_DIR, DATA_DIR, NODE_ARCHIVE_SUFFIX, NODE_VERSIONS_DIR, OS,
},
consts::{ARCH, BIN_DIR, CACHE_DIR, CFG_DIR, DATA_DIR, NODE_VERSIONS_DIR, OS},
error::VersionError,
web_api::WebApi,
versioning::{SimpleVersion, VersionMetadata},
};
use miette::{IntoDiagnostic, Result};
use self::{
downloader::{versions::Versions, NodeDownloader},
node_path::NodePath,
versions::{SimpleVersion, SimpleVersionInfo, Versions},
};
pub(crate) mod extract;
pub mod downloader;
mod local_versions;
pub(crate) mod node_path;
pub mod versions;
#[derive(Clone, Debug)]
pub enum NodeVersion {
@ -64,13 +56,23 @@ impl FromStr for NodeVersion {
}
}
impl NodeVersion {
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
impl<'de> Deserialize<'de> for NodeVersion {
#[inline]
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let string = String::deserialize(deserializer)?;
Self::from_str(&string).map_err(serde::de::Error::custom)
}
}
pub fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
impl Serialize for NodeVersion {
#[inline]
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
self.to_string().serialize(serializer)
}
}
@ -88,8 +90,7 @@ impl fmt::Display for NodeVersion {
}
pub struct Repository {
versions: Versions,
web_api: WebApi,
downloader: NodeDownloader,
}
impl Repository {
@ -97,10 +98,9 @@ impl Repository {
#[tracing::instrument(level = "debug", skip_all)]
pub async fn init(config: ConfigAccess) -> Result<Self> {
Self::create_folders().await?;
let web_api = WebApi::new(&config.get().await.download.dist_base_url);
let versions = load_versions(&web_api).await?;
let downloader = NodeDownloader::new(config.clone());
Ok(Self { web_api, versions })
Ok(Self { downloader })
}
#[tracing::instrument(level = "debug")]
@ -121,6 +121,7 @@ impl Repository {
}))
.await
{
#[allow(clippy::question_mark)]
if let Err(e) = result {
return Err(e);
}
@ -131,8 +132,8 @@ impl Repository {
/// Returns the path for the given node version
#[tracing::instrument(level = "debug", skip(self))]
pub fn get_version_path(&self, version: &NodeVersion) -> Result<Option<NodePath>> {
let info = self.lookup_version(version)?;
pub async fn get_version_path(&mut self, version: &NodeVersion) -> Result<Option<NodePath>> {
let info = self.lookup_version(version).await?;
let path = build_version_path(&info.version);
Ok(if path.exists() {
@ -159,27 +160,33 @@ impl Repository {
/// Returns if the given version is installed
#[tracing::instrument(level = "debug", skip(self))]
pub fn is_installed(&self, version: &NodeVersion) -> Result<bool> {
let info = self.lookup_version(version)?;
pub async fn is_installed(&mut self, version: &NodeVersion) -> Result<bool> {
let info = self.lookup_version(version).await?;
Ok(build_version_path(&info.version).exists())
}
/// Installs the given node version
#[tracing::instrument(level = "debug", skip(self))]
pub async fn install_version(&mut self, version: &NodeVersion) -> Result<()> {
let info = self.lookup_version(version).await?.to_owned();
self.downloader.download(&info.version).await?;
Ok(())
}
/// Performs a lookup for the given node version
#[tracing::instrument(level = "debug", skip(self))]
pub fn lookup_version(
&self,
version_req: &NodeVersion,
) -> Result<&SimpleVersionInfo, VersionError> {
pub async fn lookup_version(&mut self, version_req: &NodeVersion) -> Result<&VersionMetadata> {
let versions = self.downloader.versions().await?;
let version = match version_req {
NodeVersion::Latest => self.versions.latest(),
NodeVersion::LatestLts => self.versions.latest_lts(),
NodeVersion::Lts(lts) => self
.versions
NodeVersion::Latest => versions.latest(),
NodeVersion::LatestLts => versions.latest_lts(),
NodeVersion::Lts(lts) => versions
.get_lts(lts)
.ok_or_else(|| VersionError::unknown_version(lts.to_owned()))?,
NodeVersion::Req(req) => self
.versions
NodeVersion::Req(req) => versions
.get_fulfilling(req)
.ok_or_else(|| VersionError::unfulfillable_version(req.to_owned()))?,
};
@ -189,57 +196,9 @@ impl Repository {
/// Returns the reference to all known versions
#[tracing::instrument(level = "debug", skip(self))]
pub fn all_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(())
pub async fn all_versions(&mut self) -> Result<&Versions> {
self.downloader.versions().await
}
#[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]
#[tracing::instrument(level = "debug", skip_all)]
async fn load_versions(web_api: &WebApi) -> Result<Versions> {
let versions = if let Some(v) = Versions::load().await {
v
} else {
let all_versions = web_api.get_versions().await?;
let v = Versions::new(all_versions);
v.save().await?;
v
};
Ok(versions)
}
fn build_version_path(version: &SimpleVersion) -> PathBuf {

@ -0,0 +1,56 @@
use std::fmt::Display;
use serde::{Deserialize, Serialize};
use crate::repository::downloader::VersionInfo;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize, Hash)]
pub struct SimpleVersion {
pub major: u16,
pub minor: u16,
pub patch: u32,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct VersionMetadata {
/// The semver version
pub version: SimpleVersion,
/// The lts name of this version if it is an lts version
pub lts: Option<String>,
}
impl From<semver::Version> for SimpleVersion {
fn from(value: semver::Version) -> Self {
Self {
major: value.major as u16,
minor: value.minor as u16,
patch: value.patch as u32,
}
}
}
impl From<SimpleVersion> for semver::Version {
fn from(value: SimpleVersion) -> Self {
Self::new(value.major as u64, value.minor as u64, value.patch as u64)
}
}
impl Display for SimpleVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
major,
minor,
patch,
} = self;
write!(f, "{major}.{minor}.{patch}")
}
}
impl From<VersionInfo> for VersionMetadata {
fn from(value: VersionInfo) -> Self {
Self {
version: value.version.into(),
lts: value.lts.lts(),
}
}
}

@ -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