From 97a6ee5a604c516766a632d717c5f349c1fda72a Mon Sep 17 00:00:00 2001 From: trivernis Date: Tue, 24 Jan 2023 21:26:02 +0100 Subject: [PATCH] Add lookup table for locally installed versions --- src/consts.rs | 1 + src/nenv.rs | 10 +-- src/repository/local_versions.rs | 106 +++++++++++++++++++++++++++++++ src/repository/mod.rs | 97 ++++++++++++++++++++++------ 4 files changed, 190 insertions(+), 24 deletions(-) diff --git a/src/consts.rs b/src/consts.rs index f4dab5a..7ab03d4 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -15,6 +15,7 @@ lazy_static! { .join(PathBuf::from("nenv")); pub static ref CFG_FILE_PATH: PathBuf = CFG_DIR.join("config.toml"); pub static ref VERSION_FILE_PATH: PathBuf = CACHE_DIR.join("versions.cache"); + pub static ref INSTALLED_VERSION_FILE: PathBuf = DATA_DIR.join("installed_versions"); 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}"); diff --git a/src/nenv.rs b/src/nenv.rs index 2bbb683..c842973 100644 --- a/src/nenv.rs +++ b/src/nenv.rs @@ -126,8 +126,11 @@ impl Nenv { /// Lists the currently installed versions #[tracing::instrument(skip(self))] 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).await?; + let versions = self.repo.installed_versions(); + let active_version = self + .repo + .lookup_remote_version(&self.active_version) + .await?; let active_version = active_version.version.into(); println!("{}", "Installed versions:".bold()); @@ -234,8 +237,7 @@ impl Nenv { async fn get_mapper(&mut self) -> Result { let node_path = self .repo - .get_version_path(&self.active_version) - .await? + .get_version_path(&self.active_version)? .ok_or_else(|| VersionError::not_installed(self.active_version.to_owned()))?; Ok(Mapper::new(node_path)) } diff --git a/src/repository/local_versions.rs b/src/repository/local_versions.rs index e69de29..6261337 100644 --- a/src/repository/local_versions.rs +++ b/src/repository/local_versions.rs @@ -0,0 +1,106 @@ +use std::{fs::File, io::Write}; + +use semver::VersionReq; +use serde::{Deserialize, Serialize}; + +use crate::{ + consts::INSTALLED_VERSION_FILE, + versioning::{SimpleVersion, VersionMetadata}, +}; +use miette::{Context, IntoDiagnostic, Result}; + +#[derive(Serialize, Deserialize, Default)] +pub struct InstalledVersions { + ordered_versions: Vec<(SimpleVersion, VersionMetadata)>, +} + +impl InstalledVersions { + pub fn new(mut versions: Vec<(SimpleVersion, VersionMetadata)>) -> Self { + versions.sort_by_key(|e| e.0); + Self { + ordered_versions: versions, + } + } + + /// Loads the local versions + pub fn load() -> Result { + let reader = File::open(&*INSTALLED_VERSION_FILE) + .into_diagnostic() + .context("Opening local versions file")?; + let versions = bincode::deserialize_from(reader) + .into_diagnostic() + .context("Deserializing local versions")?; + + Ok(versions) + } + + /// Saves the local versions + pub fn save(&self) -> Result<()> { + let mut file = File::create(&*INSTALLED_VERSION_FILE) + .into_diagnostic() + .context("Opening local versions file")?; + bincode::serialize_into(&mut file, &self) + .into_diagnostic() + .context("Serializing local versions")?; + file.flush() + .into_diagnostic() + .context("Flushing local versions to file")?; + + Ok(()) + } + + /// Inserts a new version. This requires reordering the list + pub fn insert(&mut self, version: (SimpleVersion, VersionMetadata)) { + self.ordered_versions.push(version); + self.ordered_versions.sort_by_key(|e| e.0); + } + + /// Removes a version. This keeps the order intact + pub fn remove(&mut self, version: &SimpleVersion) { + self.ordered_versions.retain(|(v, _)| v != version) + } + + pub fn all(&self) -> Vec<&SimpleVersion> { + self.ordered_versions.iter().map(|(v, _)| v).collect() + } + + pub fn latest(&self) -> Option<&VersionMetadata> { + self.ordered_versions.last().map(|(_, m)| m) + } + + pub fn latest_lts(&self) -> Option<&VersionMetadata> { + self.ordered_versions + .iter() + .filter(|(_, m)| m.lts.is_some()) + .last() + .map(|(_, m)| m) + } + + pub fn lts>(&self, lts: S) -> Option<&VersionMetadata> { + self.ordered_versions + .iter() + .filter_map(|(v, m)| Some((v, m.lts.clone()?, m))) + .filter(|(_, n, _)| n == lts.as_ref()) + .last() + .map(|(_, _, m)| m) + } + + pub fn fulfilling(&self, req: &VersionReq) -> Option<&VersionMetadata> { + self.ordered_versions + .iter() + .filter(|(v, _)| req.matches(&v.to_owned().into())) + .last() + .map(|(_, m)| m) + } +} + +impl From> for InstalledVersions { + fn from(versions: Vec) -> Self { + let versions = versions + .into_iter() + .map(|v| (v.version.to_owned(), v)) + .collect::>(); + + Self::new(versions) + } +} diff --git a/src/repository/mod.rs b/src/repository/mod.rs index b809dff..3fc0e91 100644 --- a/src/repository/mod.rs +++ b/src/repository/mod.rs @@ -17,6 +17,7 @@ use miette::{Context, IntoDiagnostic, Result}; use self::{ downloader::{versions::Versions, NodeDownloader}, + local_versions::InstalledVersions, node_path::NodePath, }; @@ -91,6 +92,7 @@ impl fmt::Display for NodeVersion { pub struct Repository { downloader: NodeDownloader, + installed_versions: InstalledVersions, } impl Repository { @@ -98,9 +100,19 @@ impl Repository { #[tracing::instrument(level = "debug", skip_all)] pub async fn init(config: ConfigAccess) -> Result { Self::create_folders().await?; - let downloader = NodeDownloader::new(config.clone()); + let mut downloader = NodeDownloader::new(config.clone()); - Ok(Self { downloader }) + let installed_versions = match InstalledVersions::load() { + Ok(v) => v, + Err(_) => load_installed_versions_info(downloader.versions().await?) + .await? + .into(), + }; + + Ok(Self { + downloader, + installed_versions, + }) } #[tracing::instrument(level = "debug")] @@ -132,8 +144,8 @@ impl Repository { /// Returns the path for the given node version #[tracing::instrument(level = "debug", skip(self))] - pub async fn get_version_path(&mut self, version: &NodeVersion) -> Result> { - let info = self.lookup_version(version).await?; + pub fn get_version_path(&mut self, version: &NodeVersion) -> Result> { + let info = self.lookup_local_version(version)?; let path = build_version_path(&info.version); Ok(if path.exists() { @@ -144,24 +156,22 @@ impl Repository { } /// Returns a list of installed versions - #[tracing::instrument(level = "debug", skip(self))] - pub async fn installed_versions(&self) -> Result> { - let mut versions = Vec::new(); - let mut iter = fs::read_dir(&*NODE_VERSIONS_DIR).await.into_diagnostic()?; - - while let Some(entry) = iter.next_entry().await.into_diagnostic()? { - if let Ok(version) = Version::parse(entry.file_name().to_string_lossy().as_ref()) { - versions.push(version); - }; - } - - Ok(versions) + pub fn installed_versions(&self) -> Vec { + self.installed_versions + .all() + .into_iter() + .map(|v| v.clone().into()) + .collect() } /// Returns if the given version is installed #[tracing::instrument(level = "debug", skip(self))] pub async fn is_installed(&mut self, version: &NodeVersion) -> Result { - let info = self.lookup_version(version).await?; + let info = if let Ok(v) = self.lookup_local_version(version) { + v + } else { + self.lookup_remote_version(version).await? + }; Ok(build_version_path(&info.version).exists()) } @@ -169,8 +179,10 @@ impl Repository { /// 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(); + let info = self.lookup_remote_version(version).await?.to_owned(); self.downloader.download(&info.version).await?; + self.installed_versions.insert((info.version, info)); + self.installed_versions.save()?; Ok(()) } @@ -178,7 +190,7 @@ impl Repository { /// Uninstalls the given node version by deleting the versions directory #[tracing::instrument(level = "debug", skip(self))] pub async fn uninstall(&mut self, version: &NodeVersion) -> Result<()> { - let info = self.lookup_version(version).await?; + let info = self.lookup_local_version(version)?.clone(); let version_dir = NODE_VERSIONS_DIR.join(info.version.to_string()); if !version_dir.exists() { @@ -189,13 +201,18 @@ impl Repository { .await .into_diagnostic() .context("Deleting node version")?; + self.installed_versions.remove(&info.version); + self.installed_versions.save()?; Ok(()) } /// Performs a lookup for the given node version #[tracing::instrument(level = "debug", skip(self))] - pub async fn lookup_version(&mut self, version_req: &NodeVersion) -> Result<&VersionMetadata> { + pub async fn lookup_remote_version( + &mut self, + version_req: &NodeVersion, + ) -> Result<&VersionMetadata> { let versions = self.downloader.versions().await?; let version = match version_req { @@ -212,6 +229,28 @@ impl Repository { Ok(version) } + /// Performs a lookup for the given node version + #[tracing::instrument(level = "debug", skip(self))] + pub fn lookup_local_version(&self, version_req: &NodeVersion) -> Result<&VersionMetadata> { + let versions = &self.installed_versions; + let version = match version_req { + NodeVersion::Latest => versions + .latest() + .ok_or_else(|| VersionError::not_installed("latest"))?, + NodeVersion::LatestLts => versions + .latest_lts() + .ok_or_else(|| VersionError::not_installed("lts"))?, + NodeVersion::Lts(lts) => versions + .lts(lts) + .ok_or_else(|| VersionError::unknown_version(lts.to_owned()))?, + NodeVersion::Req(req) => versions + .fulfilling(req) + .ok_or_else(|| VersionError::unfulfillable_version(req.to_owned()))?, + }; + + Ok(version) + } + /// Returns the reference to all known versions #[tracing::instrument(level = "debug", skip(self))] pub async fn all_versions(&mut self) -> Result<&Versions> { @@ -224,3 +263,21 @@ fn build_version_path(version: &SimpleVersion) -> PathBuf { .join(version.to_string()) .join(format!("node-v{}-{}-{}", version, OS, ARCH)) } + +async fn load_installed_versions_info(versions: &Versions) -> Result> { + let mut installed_versions = Vec::new(); + let mut iter = fs::read_dir(&*NODE_VERSIONS_DIR).await.into_diagnostic()?; + + while let Some(entry) = iter.next_entry().await.into_diagnostic()? { + if let Ok(version) = Version::parse(entry.file_name().to_string_lossy().as_ref()) { + installed_versions.push(version); + }; + } + let versions = installed_versions + .into_iter() + .filter_map(|v| versions.get(&v)) + .cloned() + .collect(); + + Ok(versions) +}