Compare commits

...

7 Commits

@ -20,6 +20,10 @@ pub enum Command {
#[command(short_flag = 'v', aliases = &["--version"])] #[command(short_flag = 'v', aliases = &["--version"])]
Version, Version,
/// Initializes nenv directories and installs a default node version
#[command()]
Init,
/// Installs the given node version /// Installs the given node version
#[command()] #[command()]
Install(InstallArgs), Install(InstallArgs),
@ -28,10 +32,12 @@ pub enum Command {
#[command()] #[command()]
Default(DefaultArgs), Default(DefaultArgs),
/// Refreshes the node environment mappings and cache. /// Creates wrapper scripts for node binaries
/// This will erase all binary mappings not relevant to the current node version. /// 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()] #[command()]
Refresh, RemapBinaries,
/// Lists all available versions /// Lists all available versions
#[command(name = "list-versions")] #[command(name = "list-versions")]
@ -40,6 +46,10 @@ pub enum Command {
/// Executes the given version specific node executable /// Executes the given version specific node executable
#[command()] #[command()]
Exec(ExecArgs), Exec(ExecArgs),
/// Clears the download cache
#[command()]
ClearCache,
} }
#[derive(Clone, Debug, Parser)] #[derive(Clone, Debug, Parser)]

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

@ -14,7 +14,7 @@ lazy_static! {
.unwrap_or_else(|| PathBuf::from(".cache")) .unwrap_or_else(|| PathBuf::from(".cache"))
.join(PathBuf::from("nenv")); .join(PathBuf::from("nenv"));
pub static ref CFG_FILE_PATH: PathBuf = CFG_DIR.join("config.toml"); 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 BIN_DIR: PathBuf = DATA_DIR.join("bin");
pub static ref NODE_VERSIONS_DIR: PathBuf = DATA_DIR.join("versions"); pub static ref NODE_VERSIONS_DIR: PathBuf = DATA_DIR.join("versions");
pub static ref NODE_ARCHIVE_SUFFIX: String = format!("-{OS}-{ARCH}.{ARCHIVE_TYPE}"); 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 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;
@ -20,6 +19,7 @@ mod args;
mod config; mod config;
mod nenv; mod nenv;
mod version_detection; mod version_detection;
mod versioning;
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> { async fn main() -> Result<()> {
@ -45,8 +45,10 @@ async fn main() -> Result<()> {
process::exit(exit_code); 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::ListVersions => nenv.list_versions().await,
args::Command::Init => nenv.init_nenv().await,
args::Command::ClearCache => nenv.clear_cache().await,
_ => xkcd_unreachable!(), _ => xkcd_unreachable!(),
}?; }?;

@ -1,8 +1,8 @@
use std::ffi::OsString; use std::{ffi::OsString, str::FromStr};
use crate::{ use crate::{
config::ConfigAccess, config::ConfigAccess,
consts::VERSION_FILE_PATH, consts::{BIN_DIR, CACHE_DIR, VERSION_FILE_PATH},
error::VersionError, error::VersionError,
mapper::Mapper, mapper::Mapper,
repository::{NodeVersion, Repository}, repository::{NodeVersion, Repository},
@ -10,7 +10,8 @@ use crate::{
version_detection::{self, VersionDetector}, version_detection::{self, VersionDetector},
}; };
use crossterm::style::Stylize; use crossterm::style::Stylize;
use miette::{IntoDiagnostic, Result}; use dialoguer::{theme::ColorfulTheme, Input, Select};
use miette::{Context, IntoDiagnostic, Result};
use tokio::fs; use tokio::fs;
pub struct Nenv { pub struct Nenv {
@ -40,7 +41,7 @@ impl Nenv {
pub async fn install(&mut self, version: NodeVersion) -> Result<()> { pub async fn install(&mut self, version: NodeVersion) -> Result<()> {
Self::clear_version_cache().await?; Self::clear_version_cache().await?;
if self.repo.is_installed(&version)? if self.repo.is_installed(&version).await?
&& !prompt( && !prompt(
false, false,
format!( format!(
@ -54,7 +55,7 @@ impl Nenv {
} else { } else {
self.repo.install_version(&version).await?; self.repo.install_version(&version).await?;
self.active_version = version.to_owned(); 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()); println!("Installed {}", version.to_string().bold());
Ok(()) Ok(())
@ -66,20 +67,20 @@ impl Nenv {
pub async fn set_system_default(&mut self, version: NodeVersion) -> Result<()> { pub async fn set_system_default(&mut self, version: NodeVersion) -> Result<()> {
self.active_version = version.to_owned(); self.active_version = version.to_owned();
if !self.repo.is_installed(&version)? { if !self.repo.is_installed(&version).await? {
if prompt( if prompt(
false, false,
format!("The version {version} is not installed. Do you want to install it?"), format!("The version {version} is not installed. Do you want to install it?"),
) { ) {
self.repo.install_version(&version).await?; self.repo.install_version(&version).await?;
self.config.get_mut().await.node.default_version = version.to_owned(); 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()); println!("Now using {}", version.to_string().bold());
} }
Ok(()) Ok(())
} else { } 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(); self.config.get_mut().await.node.default_version = version.to_owned();
println!("Now using {}", version.to_string().bold()); println!("Now using {}", version.to_string().bold());
@ -89,27 +90,26 @@ impl Nenv {
/// Executes a given node executable for the currently active version /// Executes a given node executable for the currently active version
#[tracing::instrument(skip(self))] #[tracing::instrument(skip(self))]
pub async fn exec(&self, command: String, args: Vec<OsString>) -> Result<i32> { pub async fn exec(&mut self, command: String, args: Vec<OsString>) -> Result<i32> {
if !self.repo.is_installed(&self.active_version)? { if !self.repo.is_installed(&self.active_version).await? {
self.repo.install_version(&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)) Ok(exit_status.code().unwrap_or(0))
} }
/// Clears the version cache and remaps all executables /// Clears the version cache and remaps all executables
#[tracing::instrument(skip(self))] #[tracing::instrument(skip(self))]
pub async fn refresh(&self) -> Result<()> { pub async fn remap(&mut self) -> Result<()> {
Self::clear_version_cache().await?; self.get_mapper().await?.remap().await
self.get_mapper()?.remap().await
} }
/// Lists the currently installed versions /// Lists the currently installed versions
#[tracing::instrument(skip(self))] #[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 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(); let active_version = active_version.version.into();
println!("{}", "Installed versions:".bold()); println!("{}", "Installed versions:".bold());
@ -118,6 +118,7 @@ impl Nenv {
let info = self let info = self
.repo .repo
.all_versions() .all_versions()
.await?
.get(&version) .get(&version)
.ok_or_else(|| VersionError::unknown_version(version.to_string()))?; .ok_or_else(|| VersionError::unknown_version(version.to_string()))?;
let lts = info let lts = info
@ -136,6 +137,56 @@ impl Nenv {
Ok(()) 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 /// Persits all changes made that aren't written to the disk yet
#[tracing::instrument(level = "debug", skip(self))] #[tracing::instrument(level = "debug", skip(self))]
pub async fn persist(&self) -> Result<()> { pub async fn persist(&self) -> Result<()> {
@ -162,10 +213,11 @@ impl Nenv {
} }
#[tracing::instrument(level = "debug", skip(self))] #[tracing::instrument(level = "debug", skip(self))]
fn get_mapper(&self) -> Result<Mapper> { async fn get_mapper(&mut self) -> Result<Mapper> {
let node_path = self let node_path = self
.repo .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_or_else(|| VersionError::not_installed(self.active_version.to_owned()))?;
Ok(Mapper::new(node_path)) 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 semver::{Version, VersionReq};
use serde::{Deserialize, Serialize}; 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,
versioning::{SimpleVersion, VersionMetadata},
};
use miette::{Context, IntoDiagnostic, Result}; use miette::{Context, IntoDiagnostic, Result};
use super::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>,
versions: HashMap<SimpleVersion, SimpleVersionInfo>, versions: HashMap<SimpleVersion, VersionMetadata>,
// as this field is not serialized // as this field is not serialized
// it needs to be calculated after serialization // it needs to be calculated after serialization
#[serde(skip)] #[serde(skip)]
sorted_versions: Vec<SimpleVersion>, 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 { impl Versions {
/// Loads the versions from the cached versions.json file /// Loads the versions from the cached versions.json file
pub(crate) async fn load() -> Option<Self> { pub(crate) async fn load() -> Option<Self> {
@ -129,7 +86,7 @@ impl Versions {
/// Returns the latest known node version /// Returns the latest known node version
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
pub fn latest(&self) -> &SimpleVersionInfo { pub fn latest(&self) -> &VersionMetadata {
self.versions self.versions
.get(self.sorted_versions.last().expect("No known node versions")) .get(self.sorted_versions.last().expect("No known node versions"))
.unwrap() .unwrap()
@ -137,7 +94,7 @@ impl Versions {
/// Returns the latest node lts version /// Returns the latest node lts version
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
pub fn latest_lts(&self) -> &SimpleVersionInfo { pub fn latest_lts(&self) -> &VersionMetadata {
let mut versions = self let mut versions = self
.lts_versions .lts_versions
.values() .values()
@ -149,14 +106,14 @@ impl Versions {
/// Returns a lts version by name /// Returns a lts version by name
#[tracing::instrument(level = "debug", skip(self))] #[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())?; let lts_version = self.lts_versions.get(lts_name.as_ref())?;
self.get_latest_for_major(*lts_version) self.get_latest_for_major(*lts_version)
} }
/// Returns any version that fulfills the given requirement /// Returns any version that fulfills the given requirement
#[tracing::instrument(level = "debug", skip(self))] #[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 let fulfilling_versions = self
.sorted_versions .sorted_versions
.iter() .iter()
@ -165,18 +122,18 @@ impl Versions {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let version = fulfilling_versions.last()?.clone().into(); let version = fulfilling_versions.last()?.clone().into();
self.versions.get(&version).into() self.versions.get(&version)
} }
/// Returns the info for the given version /// Returns the info for the given version
#[tracing::instrument(level = "debug", skip(self))] #[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()) self.versions.get(&version.clone().into())
} }
/// Returns any version that fulfills the given requirement /// Returns any version that fulfills the given requirement
#[tracing::instrument(level = "debug", skip(self))] #[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 let fulfilling_versions = self
.sorted_versions .sorted_versions
.iter() .iter()
@ -184,7 +141,7 @@ impl Versions {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let version = fulfilling_versions.last()?; let version = fulfilling_versions.last()?;
self.versions.get(&version).into() self.versions.get(version)
} }
/// Creates the list of sorted versions /// Creates the list of sorted versions

@ -1,36 +1,28 @@
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, versioning::{SimpleVersion, VersionMetadata},
}; };
use miette::{IntoDiagnostic, Result}; use miette::{IntoDiagnostic, Result};
use self::{ use self::{
downloader::{versions::Versions, NodeDownloader},
node_path::NodePath, node_path::NodePath,
versions::{SimpleVersion, SimpleVersionInfo, Versions},
}; };
pub(crate) mod extract; pub mod downloader;
mod local_versions;
pub(crate) mod node_path; pub(crate) mod node_path;
pub mod versions;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum NodeVersion { pub enum NodeVersion {
@ -64,13 +56,23 @@ impl FromStr for NodeVersion {
} }
} }
impl NodeVersion { impl<'de> Deserialize<'de> for NodeVersion {
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { #[inline]
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let string = String::deserialize(deserializer)?; let string = String::deserialize(deserializer)?;
Self::from_str(&string).map_err(serde::de::Error::custom) 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) self.to_string().serialize(serializer)
} }
} }
@ -88,8 +90,7 @@ impl fmt::Display for NodeVersion {
} }
pub struct Repository { pub struct Repository {
versions: Versions, downloader: NodeDownloader,
web_api: WebApi,
} }
impl Repository { impl Repository {
@ -97,10 +98,9 @@ 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?;
Ok(Self { web_api, versions }) Ok(Self { downloader })
} }
#[tracing::instrument(level = "debug")] #[tracing::instrument(level = "debug")]
@ -121,6 +121,7 @@ impl Repository {
})) }))
.await .await
{ {
#[allow(clippy::question_mark)]
if let Err(e) = result { if let Err(e) = result {
return Err(e); return Err(e);
} }
@ -131,8 +132,8 @@ impl Repository {
/// Returns the path for the given node version /// Returns the path for the given node version
#[tracing::instrument(level = "debug", skip(self))] #[tracing::instrument(level = "debug", skip(self))]
pub fn get_version_path(&self, version: &NodeVersion) -> Result<Option<NodePath>> { pub async fn get_version_path(&mut self, version: &NodeVersion) -> Result<Option<NodePath>> {
let info = self.lookup_version(version)?; let info = self.lookup_version(version).await?;
let path = build_version_path(&info.version); let path = build_version_path(&info.version);
Ok(if path.exists() { Ok(if path.exists() {
@ -159,27 +160,33 @@ impl Repository {
/// Returns if the given version is installed /// Returns if the given version is installed
#[tracing::instrument(level = "debug", skip(self))] #[tracing::instrument(level = "debug", skip(self))]
pub fn is_installed(&self, version: &NodeVersion) -> Result<bool> { pub async fn is_installed(&mut self, version: &NodeVersion) -> Result<bool> {
let info = self.lookup_version(version)?; let info = self.lookup_version(version).await?;
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(&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 /// 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 async fn lookup_version(&mut self, version_req: &NodeVersion) -> Result<&VersionMetadata> {
&self, let versions = self.downloader.versions().await?;
version_req: &NodeVersion,
) -> Result<&SimpleVersionInfo, VersionError> {
let version = match version_req { let version = match version_req {
NodeVersion::Latest => self.versions.latest(), NodeVersion::Latest => versions.latest(),
NodeVersion::LatestLts => self.versions.latest_lts(), NodeVersion::LatestLts => versions.latest_lts(),
NodeVersion::Lts(lts) => self NodeVersion::Lts(lts) => versions
.versions
.get_lts(lts) .get_lts(lts)
.ok_or_else(|| VersionError::unknown_version(lts.to_owned()))?, .ok_or_else(|| VersionError::unknown_version(lts.to_owned()))?,
NodeVersion::Req(req) => self NodeVersion::Req(req) => versions
.versions
.get_fulfilling(req) .get_fulfilling(req)
.ok_or_else(|| VersionError::unfulfillable_version(req.to_owned()))?, .ok_or_else(|| VersionError::unfulfillable_version(req.to_owned()))?,
}; };
@ -189,57 +196,9 @@ impl Repository {
/// Returns the reference to all known versions /// Returns the reference to all known versions
#[tracing::instrument(level = "debug", skip(self))] #[tracing::instrument(level = "debug", skip(self))]
pub fn all_versions(&self) -> &Versions { pub async fn all_versions(&mut self) -> Result<&Versions> {
&self.versions self.downloader.versions().await
}
/// 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]
#[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 { 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