Improve error handling by using miette everywhere

feature/lookup-installed
trivernis 1 year ago
parent bf750af4cf
commit cb24de8a19
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

@ -3,8 +3,6 @@ name = "nenv"
version = "0.2.0"
edition = "2021"
[lib]
name = "nenv"
[[bin]]
name = "nenv"
@ -14,7 +12,6 @@ path = "src/main.rs"
[dependencies]
clap = { version = "4.1.1", features = ["derive"] }
color-eyre = "0.6.2"
crossterm = "0.25.0"
dialoguer = "0.10.3"
dirs = "4.0.0"

@ -1,7 +1,7 @@
use std::ffi::OsString;
use crate::repository::NodeVersion;
use clap::{Parser, Subcommand};
use nenv::repository::NodeVersion;
#[derive(Clone, Debug, Parser)]
#[clap(infer_subcommands = true)]

@ -1,84 +1,128 @@
use std::io;
use miette::{Diagnostic, NamedSource, SourceSpan};
use miette::Diagnostic;
use semver::VersionReq;
use thiserror::Error;
use crate::{
mapper::error::MapperError,
repository::{config::ConfigError, extract::ExtractError},
web_api::error::ApiError,
};
pub(crate) type LibResult<T> = Result<T>;
pub type Result<T> = std::result::Result<T, Error>;
use crate::{mapper::error::MapperError, repository::extract::ExtractError};
#[derive(Debug, Error, Diagnostic)]
pub enum Error {
#[diagnostic(code(nenv::web))]
#[error("Failed to call nodejs.com api.")]
Web(
#[from]
#[source]
#[diagnostic_source]
ApiError,
),
#[diagnostic(code(nenv::extract))]
#[error("The node archive could not be extracted")]
Extract(
#[from]
#[source]
#[diagnostic_source]
ExtractError,
),
#[diagnostic(code(nenv::config))]
#[error("The config file could not be loaded")]
Config(
#[from]
#[diagnostic_source]
ConfigError,
),
Extract(#[from] ExtractError),
#[diagnostic(code(nenv::mapper))]
#[error("Mapping failed")]
Mapper(
#[from]
#[source]
#[diagnostic_source]
MapperError,
),
Mapper(#[from] MapperError),
#[diagnostic(code(nenv::version))]
#[error("The passed version is invalid")]
Version(
#[from]
#[diagnostic_source]
VersionError,
),
#[diagnostic(code(nenv::json))]
#[error("Failed to work with json")]
Json(#[from] serde_json::Error),
#[diagnostic(code(nenv::io))]
#[error("Error during IO operation")]
Io(#[from] io::Error),
Version(#[from] VersionError),
}
#[derive(Debug, Error, Diagnostic)]
#[error("{detail}")]
#[diagnostic(code(nenv::version), help("Make sure there's no typo in the version."))]
pub struct VersionError {
#[source_code]
src: String,
#[label("this version")]
pos: SourceSpan,
detail: String,
}
impl VersionError {
pub fn new<S1: ToString, S2: ToString>(src: S1, detail: S2) -> Self {
let src = src.to_string();
let pos = (0, src.len()).into();
Self {
src,
detail: detail.to_string(),
pos,
}
}
pub fn unknown_version<S: ToString>(src: S) -> Self {
Self::new(src, "unknown version")
}
pub fn unfulfillable_version<S: ToString>(src: S) -> Self {
Self::new(src, "the version requirement cannot be fulfilled")
}
pub fn not_installed<S: ToString>(src: S) -> Self {
Self::new(src, "the version is not installed")
}
}
#[derive(Debug, Error, Diagnostic)]
pub enum VersionError {
#[error("Invalid version string `{0}`")]
ParseVersion(#[source_code] String),
#[error("failed to parse json")]
#[diagnostic(code(nenv::json::deserialize))]
pub struct ParseJsonError {
#[source_code]
pub src: NamedSource,
#[error("Unknown Version `{0}`")]
UnkownVersion(#[source_code] String),
#[label]
pub pos: SourceSpan,
#[error("The version `{0}` is not installed")]
NotInstalled(#[source_code] String),
#[source]
pub caused_by: serde_json::Error,
}
#[derive(Debug, Error, Diagnostic)]
#[diagnostic(code(nenv::json::serialize))]
#[error("failed to serialize value to json string")]
pub struct SerializeJsonError {
#[from]
caused_by: serde_json::Error,
}
#[derive(Debug, Error, Diagnostic)]
#[diagnostic(code(nenv::toml::deserialize))]
#[error("failed to parse toml value")]
pub struct ParseTomlError {
#[source_code]
src: NamedSource,
#[label]
pos: Option<SourceSpan>,
#[error("The version requirement `{0}` cannot be fulfilled")]
Unfulfillable(VersionReq),
#[source]
caused_by: toml::de::Error,
}
impl ParseTomlError {
pub fn new(file_name: &str, src: String, caused_by: toml::de::Error) -> Self {
let abs_pos = caused_by
.line_col()
.map(|(l, c)| {
src.lines()
.into_iter()
.take(l)
.map(|line| line.len() + 1)
.sum::<usize>()
+ c
})
.map(|p| SourceSpan::new(p.into(), 0.into()));
Self {
src: NamedSource::new(file_name, src),
pos: abs_pos.into(),
caused_by,
}
}
}
#[derive(Debug, Error, Diagnostic)]
#[diagnostic(code(nenv::toml::serialize))]
#[error("failed to serialize value to toml string")]
pub struct SerializeTomlError {
#[from]
caused_by: toml::ser::Error,
}
#[derive(Debug, Error, Diagnostic)]
#[diagnostic(code(nenv::http))]
#[error("http request failed")]
pub struct ReqwestError(#[from] reqwest::Error);

@ -1,120 +0,0 @@
use std::ffi::OsString;
use consts::VERSION_FILE_PATH;
use crossterm::style::Stylize;
use mapper::Mapper;
use repository::{config::Config, NodeVersion, Repository};
mod consts;
pub mod error;
pub mod mapper;
pub mod repository;
mod utils;
mod web_api;
use dialoguer::Confirm;
use error::Result;
use tokio::fs;
use crate::error::VersionError;
pub async fn install_version(version: NodeVersion) -> Result<()> {
if VERSION_FILE_PATH.exists() {
fs::remove_file(&*VERSION_FILE_PATH).await?;
}
let repo = get_repository().await?;
if repo.is_installed(&version)? {
if !Confirm::new()
.with_prompt("The version {version} is already installed. Reinstall?")
.default(false)
.interact()
.unwrap()
{
return Ok(());
}
}
repo.install_version(&version).await?;
println!("Installed {}", version.to_string().bold());
Ok(())
}
pub async fn set_default_version(version: NodeVersion) -> Result<()> {
let mut mapper = get_mapper().await?;
if !mapper.repository().is_installed(&version)?
&& Confirm::new()
.with_prompt(format!(
"The version {version} is not installed. Do you want to install it?"
))
.default(false)
.interact()
.unwrap()
{
mapper.repository().install_version(&version).await?;
}
mapper.set_default_version(&version).await?;
println!("Now using {}", version.to_string().bold());
Ok(())
}
#[inline]
pub async fn exec(command: String, args: Vec<OsString>) -> Result<i32> {
let mapper = get_mapper().await?;
let active_version = mapper.active_version();
if !mapper.repository().is_installed(active_version)? {
mapper.repository().install_version(&active_version).await?;
}
let exit_status = mapper.exec(command, args).await?;
Ok(exit_status.code().unwrap_or(0))
}
pub async fn refresh() -> Result<()> {
get_mapper().await?.remap().await?;
fs::remove_file(&*VERSION_FILE_PATH).await?;
Ok(())
}
pub async fn list_versions() -> Result<()> {
let mapper = get_mapper().await?;
let versions = mapper.repository().installed_versions().await?;
let active_version = mapper
.repository()
.lookup_version(mapper.active_version())?;
println!("{}", "Installed versions:".bold());
for version in versions {
let info = mapper
.repository()
.all_versions()
.get(&version)
.ok_or_else(|| VersionError::UnkownVersion(version.to_string()))?;
let lts = info
.lts
.as_ref()
.map(|l| format!(" ({})", l.to_owned().green()))
.unwrap_or_default();
if version == active_version.version {
println!(" {}{} [current]", version.to_string().blue().bold(), lts)
} else {
println!(" {}{}", version.to_string().blue(), lts)
}
}
Ok(())
}
async fn get_repository() -> Result<Repository> {
Repository::init(Config::load().await?).await
}
async fn get_mapper() -> Result<Mapper> {
Ok(Mapper::load(get_repository().await?).await)
}

@ -2,25 +2,43 @@ use std::process;
use args::Args;
use clap::Parser;
use std::ffi::OsString;
use consts::VERSION_FILE_PATH;
use crossterm::style::Stylize;
use mapper::Mapper;
use repository::{config::Config, NodeVersion, Repository};
mod consts;
pub mod error;
pub mod mapper;
pub mod repository;
mod utils;
mod web_api;
use dialoguer::Confirm;
use miette::{IntoDiagnostic, Result};
use tokio::fs;
use crate::error::VersionError;
mod args;
#[tokio::main(flavor = "current_thread")]
async fn main() -> miette::Result<()> {
async fn main() -> Result<()> {
miette::set_panic_hook();
let args: Args = Args::parse();
match args.commmand {
args::Command::Version => Ok(print_version()),
args::Command::Install(v) => nenv::install_version(v.version).await,
args::Command::Default(v) => nenv::set_default_version(v.version).await,
args::Command::Install(v) => install_version(v.version).await,
args::Command::Default(v) => set_default_version(v.version).await,
args::Command::Exec(args) => {
let exit_code = nenv::exec(args.command, args.args).await?;
let exit_code = exec(args.command, args.args).await?;
process::exit(exit_code);
}
args::Command::Refresh => nenv::refresh().await,
args::Command::ListVersions => nenv::list_versions().await,
args::Command::Refresh => refresh().await,
args::Command::ListVersions => list_versions().await,
}?;
Ok(())
@ -29,3 +47,113 @@ async fn main() -> miette::Result<()> {
fn print_version() {
println!("{} v{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
}
pub async fn install_version(version: NodeVersion) -> Result<()> {
if VERSION_FILE_PATH.exists() {
fs::remove_file(&*VERSION_FILE_PATH)
.await
.into_diagnostic()?;
}
let repo = get_repository().await?;
if repo.is_installed(&version)? {
if !Confirm::new()
.with_prompt(format!(
"The version {} is already installed. Reinstall?",
version.to_string().bold()
))
.default(false)
.interact()
.unwrap()
{
return Ok(());
}
}
repo.install_version(&version).await?;
println!("Installed {}", version.to_string().bold());
Ok(())
}
pub async fn set_default_version(version: NodeVersion) -> Result<()> {
let mut mapper = get_mapper().await?;
if !mapper.repository().is_installed(&version)?
&& Confirm::new()
.with_prompt(format!(
"The version {version} is not installed. Do you want to install it?"
))
.default(false)
.interact()
.unwrap()
{
mapper.repository().install_version(&version).await?;
}
mapper.set_default_version(&version).await?;
println!("Now using {}", version.to_string().bold());
Ok(())
}
#[inline]
pub async fn exec(command: String, args: Vec<OsString>) -> Result<i32> {
let mapper = get_mapper().await?;
let active_version = mapper.active_version();
if !mapper.repository().is_installed(active_version)? {
mapper.repository().install_version(&active_version).await?;
}
let exit_status = mapper.exec(command, args).await?;
Ok(exit_status.code().unwrap_or(0))
}
pub async fn refresh() -> Result<()> {
get_mapper().await?.remap().await?;
fs::remove_file(&*VERSION_FILE_PATH)
.await
.into_diagnostic()?;
println!("Remapped binaries and cleared version cache");
Ok(())
}
pub async fn list_versions() -> Result<()> {
let mapper = get_mapper().await?;
let versions = mapper.repository().installed_versions().await?;
let active_version = mapper
.repository()
.lookup_version(mapper.active_version())?;
println!("{}", "Installed versions:".bold());
for version in versions {
let info = mapper
.repository()
.all_versions()
.get(&version)
.ok_or_else(|| VersionError::unknown_version(version.to_string()))?;
let lts = info
.lts
.as_ref()
.map(|l| format!(" ({})", l.to_owned().green()))
.unwrap_or_default();
if version == active_version.version {
println!(" {}{} [current]", version.to_string().blue().bold(), lts)
} else {
println!(" {}{}", version.to_string().blue(), lts)
}
}
Ok(())
}
async fn get_repository() -> Result<Repository> {
Repository::init(Config::load().await?).await
}
async fn get_mapper() -> Result<Mapper> {
Ok(Mapper::load(get_repository().await?).await)
}

@ -3,22 +3,12 @@ use std::{io, path::PathBuf};
use miette::Diagnostic;
use thiserror::Error;
use crate::repository::config::ConfigError;
use super::mapped_command::CommandError;
pub type MapperResult<T> = Result<T, MapperError>;
#[derive(Error, Diagnostic, Debug)]
pub enum MapperError {
#[error("Config error: {0}")]
Config(
#[from]
#[source]
#[diagnostic_source]
ConfigError,
),
#[error("Failed to execute mapped command")]
Command(#[from] CommandError),

@ -4,7 +4,7 @@ use tokio::fs;
use crate::{
consts::BIN_DIR,
error::{LibResult, VersionError},
error::VersionError,
repository::{NodeVersion, Repository},
};
@ -12,6 +12,7 @@ use self::{
error::MapperError, mapped_command::MappedCommand, mapped_dir::map_node_bin,
package_info::PackageInfo,
};
use miette::{IntoDiagnostic, Result};
pub mod error;
mod mapped_command;
@ -41,7 +42,7 @@ impl Mapper {
}
/// Sets the given version as the default one
pub async fn set_default_version(&mut self, version: &NodeVersion) -> LibResult<()> {
pub async fn set_default_version(&mut self, version: &NodeVersion) -> Result<()> {
self.repo
.config
.set_default_version(version.clone())
@ -57,11 +58,11 @@ impl Mapper {
}
/// Executes a mapped command with the given node environment
pub async fn exec(&self, command: String, args: Vec<OsString>) -> LibResult<ExitStatus> {
pub async fn exec(&self, command: String, args: Vec<OsString>) -> Result<ExitStatus> {
let node_path = self
.repo
.get_version_path(&self.active_version)?
.expect("version not installed");
.ok_or_else(|| VersionError::not_installed(&self.active_version))?;
let executable = node_path.bin().join(command);
let exit_status = MappedCommand::new(executable, args)
.run()
@ -73,9 +74,9 @@ impl Mapper {
}
/// Recreates all environment mappings
pub async fn remap(&self) -> LibResult<()> {
fs::remove_dir_all(&*BIN_DIR).await?;
fs::create_dir_all(&*BIN_DIR).await?;
pub async fn remap(&self) -> Result<()> {
fs::remove_dir_all(&*BIN_DIR).await.into_diagnostic()?;
fs::create_dir_all(&*BIN_DIR).await.into_diagnostic()?;
self.map_active_version().await?;
Ok(())
@ -98,11 +99,11 @@ impl Mapper {
}
/// creates wrapper scripts for the current version
async fn map_active_version(&self) -> LibResult<()> {
async fn map_active_version(&self) -> Result<()> {
let dir = self
.repo
.get_version_path(&self.active_version)?
.ok_or_else(|| VersionError::NotInstalled(self.active_version.to_string()))?;
.ok_or_else(|| VersionError::not_installed(self.active_version.to_string()))?;
map_node_bin(dir).await?;
Ok(())

@ -1,11 +1,12 @@
use std::{collections::HashMap, path::Path};
use miette::{IntoDiagnostic, NamedSource, Result};
use semver::VersionReq;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::fs;
use crate::error::LibResult;
use crate::error::ParseJsonError;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PackageInfo {
@ -24,8 +25,8 @@ pub struct EngineInfo {
}
impl PackageInfo {
pub async fn find() -> LibResult<Option<Self>> {
let mut dir = std::env::current_dir()?;
pub async fn find() -> Result<Option<Self>> {
let mut dir = std::env::current_dir().into_diagnostic()?;
let file_path = dir.join("package.json");
if file_path.exists() {
@ -47,9 +48,14 @@ impl PackageInfo {
}
/// Loads the package.json config file
pub async fn load(path: &Path) -> LibResult<Self> {
let file_content = fs::read_to_string(&path).await?;
let cfg = serde_json::from_str(&file_content)?;
pub async fn load(path: &Path) -> Result<Self> {
let file_content = fs::read_to_string(&path).await.into_diagnostic()?;
let cfg = serde_json::from_str(&file_content).map_err(|e| ParseJsonError {
src: NamedSource::new(path.file_name().unwrap().to_string_lossy(), file_content),
pos: (e.column(), e.column()).into(),
caused_by: e,
})?;
Ok(cfg)
}

@ -1,11 +1,13 @@
use std::io;
use miette::{Diagnostic, NamedSource, SourceSpan};
use miette::Context;
use miette::{IntoDiagnostic, Result};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio::fs;
use crate::consts::{CFG_DIR, CFG_FILE_PATH, NODE_DIST_URL};
use crate::error::SerializeTomlError;
use crate::{
consts::{CFG_DIR, CFG_FILE_PATH, NODE_DIST_URL},
error::ParseTomlError,
};
use super::NodeVersion;
@ -16,39 +18,6 @@ pub struct Config {
pub default_version: NodeVersion,
}
pub type ConfigResult<T> = Result<T, ConfigError>;
#[derive(Error, Diagnostic, Debug)]
pub enum ConfigError {
#[diagnostic(code(nenv::config::io))]
#[error("IO Error: {0}")]
Io(
#[from]
#[source]
io::Error,
),
#[diagnostic(code(nenv::config::parse))]
#[error("Failed to parse config file")]
#[diagnostic_source]
Parse {
#[source_code]
src: NamedSource,
#[label]
pos: Option<(usize, usize)>,
#[source]
e: toml::de::Error,
},
#[diagnostic(code(nenv::config::write))]
#[error("Failed to serialize config file: {0}")]
Serialize(
#[from]
#[source]
toml::ser::Error,
),
}
impl Default for Config {
fn default() -> Self {
Self {
@ -60,34 +29,44 @@ impl Default for Config {
impl Config {
/// Loads the config file from the default config path
pub async fn load() -> ConfigResult<Self> {
pub async fn load() -> Result<Self> {
if !CFG_FILE_PATH.exists() {
if !CFG_DIR.exists() {
fs::create_dir_all(&*CFG_DIR).await?;
fs::create_dir_all(&*CFG_DIR)
.await
.into_diagnostic()
.context("creating config dir")?;
}
let cfg = Config::default();
cfg.save().await?;
Ok(cfg)
} else {
let cfg_string = fs::read_to_string(&*CFG_FILE_PATH).await?;
let cfg = toml::from_str(&cfg_string).map_err(|e| ConfigError::Parse {
src: NamedSource::new("config.toml", cfg_string),
pos: e.line_col(),
e,
})?;
let cfg_string = fs::read_to_string(&*CFG_FILE_PATH)
.await
.into_diagnostic()
.context("reading config file")?;
let cfg = toml::from_str(&cfg_string)
.map_err(|e| ParseTomlError::new("config.toml", cfg_string, e))?;
Ok(cfg)
}
}
pub async fn save(&self) -> ConfigResult<()> {
fs::write(&*CFG_FILE_PATH, toml::to_string_pretty(&self)?).await?;
pub async fn save(&self) -> Result<()> {
fs::write(
&*CFG_FILE_PATH,
toml::to_string_pretty(&self).map_err(SerializeTomlError::from)?,
)
.await
.into_diagnostic()
.context("writing config file")?;
Ok(())
}
pub async fn set_default_version(&mut self, default_version: NodeVersion) -> ConfigResult<()> {
pub async fn set_default_version(&mut self, default_version: NodeVersion) -> Result<()> {
self.default_version = default_version;
self.save().await
}

@ -5,25 +5,22 @@ use std::{
};
use miette::Diagnostic;
use miette::Result;
use thiserror::Error;
use crate::utils::progress_spinner;
type ExtractResult<T> = Result<T, ExtractError>;
/// An error that can occur during extraction
#[derive(Error, Debug, Diagnostic)]
pub enum ExtractError {
#[diagnostic(code(nenv::extract::io))]
#[error("IO error when extracting: {0}")]
Io(
#[from]
#[source]
io::Error,
),
Io(#[from] io::Error),
#[diagnostic(code(nenv::extract::zip))]
#[error("Failed to extract zip: {0}")]
Zip(
#[from]
#[source]
zip::result::ZipError,
),
Zip(#[from] zip::result::ZipError),
}
pub fn extract_file(src: &Path, dst: &Path) -> ExtractResult<()> {

@ -15,10 +15,12 @@ use crate::{
consts::{
ARCH, BIN_DIR, CACHE_DIR, CFG_DIR, DATA_DIR, NODE_ARCHIVE_SUFFIX, NODE_VERSIONS_DIR, OS,
},
error::{LibResult, VersionError},
error::VersionError,
web_api::{VersionInfo, WebApi},
};
use miette::{IntoDiagnostic, Result};
use self::{config::Config, node_path::NodePath, versions::Versions};
pub mod config;
@ -87,7 +89,7 @@ pub struct Repository {
impl Repository {
/// Initializes a new repository with the given confi
pub async fn init(config: Config) -> LibResult<Self> {
pub async fn init(config: Config) -> Result<Self> {
Self::create_folders().await?;
let web_api = WebApi::new(&config.dist_base_url);
let versions = load_versions(&web_api).await?;
@ -99,7 +101,7 @@ impl Repository {
})
}
async fn create_folders() -> LibResult<()> {
async fn create_folders() -> Result<()> {
let dirs = vec![
&*CFG_DIR,
&*DATA_DIR,
@ -109,7 +111,7 @@ impl Repository {
];
for dir in dirs {
if !dir.exists() {
fs::create_dir_all(dir).await?;
fs::create_dir_all(dir).await.into_diagnostic()?;
}
}
@ -117,7 +119,7 @@ impl Repository {
}
/// Returns the path for the given node version
pub fn get_version_path(&self, version: &NodeVersion) -> LibResult<Option<NodePath>> {
pub fn get_version_path(&self, version: &NodeVersion) -> Result<Option<NodePath>> {
let info = self.lookup_version(&version)?;
let path = build_version_path(&info.version);
@ -129,11 +131,11 @@ impl Repository {
}
/// Returns a list of installed versions
pub async fn installed_versions(&self) -> LibResult<Vec<Version>> {
pub async fn installed_versions(&self) -> Result<Vec<Version>> {
let mut versions = Vec::new();
let mut iter = fs::read_dir(&*NODE_VERSIONS_DIR).await?;
let mut iter = fs::read_dir(&*NODE_VERSIONS_DIR).await.into_diagnostic()?;
while let Some(entry) = iter.next_entry().await? {
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);
};
@ -143,7 +145,7 @@ impl Repository {
}
/// Returns if the given version is installed
pub fn is_installed(&self, version: &NodeVersion) -> LibResult<bool> {
pub fn is_installed(&self, version: &NodeVersion) -> Result<bool> {
let info = self.lookup_version(version)?;
Ok(build_version_path(&info.version).exists())
@ -157,11 +159,11 @@ impl Repository {
NodeVersion::Lts(lts) => self
.versions
.get_lts(&lts)
.ok_or_else(|| VersionError::UnkownVersion(lts.to_owned()))?,
.ok_or_else(|| VersionError::unknown_version(lts.to_owned()))?,
NodeVersion::Req(req) => self
.versions
.get_fulfilling(&req)
.ok_or_else(|| VersionError::Unfulfillable(req.to_owned()))?,
.ok_or_else(|| VersionError::unfulfillable_version(req.to_owned()))?,
};
Ok(version)
@ -173,7 +175,7 @@ impl Repository {
}
/// Installs a specified node version
pub async fn install_version(&self, version_req: &NodeVersion) -> LibResult<()> {
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, &archive_path)?;
@ -181,13 +183,14 @@ impl Repository {
Ok(())
}
async fn download_version(&self, version: &Version) -> LibResult<PathBuf> {
async fn download_version(&self, version: &Version) -> 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?);
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?;
@ -195,7 +198,7 @@ impl Repository {
Ok(download_path)
}
fn extract_archive(&self, info: &VersionInfo, archive_path: &Path) -> LibResult<()> {
fn extract_archive(&self, info: &VersionInfo, archive_path: &Path) -> Result<()> {
let dst_path = NODE_VERSIONS_DIR.join(info.version.to_string());
extract::extract_file(archive_path, &dst_path)?;
@ -204,7 +207,7 @@ impl Repository {
}
#[inline]
async fn load_versions(web_api: &WebApi) -> Result<Versions, crate::error::Error> {
async fn load_versions(web_api: &WebApi) -> Result<Versions> {
let versions = if let Some(v) = Versions::load().await {
v
} else {

@ -4,7 +4,8 @@ use semver::{Version, VersionReq};
use serde::{Deserialize, Serialize};
use tokio::fs;
use crate::{consts::VERSION_FILE_PATH, error::LibResult, web_api::VersionInfo};
use crate::{consts::VERSION_FILE_PATH, error::SerializeJsonError, web_api::VersionInfo};
use miette::{IntoDiagnostic, Result};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Versions {
@ -54,9 +55,11 @@ impl Versions {
}
}
pub(crate) async fn save(&self) -> LibResult<()> {
let json_string = serde_json::to_string(&self)?;
fs::write(&*VERSION_FILE_PATH, json_string).await?;
pub(crate) async fn save(&self) -> Result<()> {
let json_string = serde_json::to_string(&self).map_err(SerializeJsonError::from)?;
fs::write(&*VERSION_FILE_PATH, json_string)
.await
.into_diagnostic()?;
Ok(())
}

@ -1,24 +0,0 @@
use std::io;
use miette::Diagnostic;
use thiserror::Error;
pub type ApiResult<T> = Result<T, ApiError>;
#[derive(Debug, Error, Diagnostic)]
pub enum ApiError {
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
#[error(transparent)]
Io(#[from] io::Error),
#[error("{0}")]
Other(#[help] String),
}
impl ApiError {
pub fn other<S: ToString>(error: S) -> Self {
Self::Other(error.to_string())
}
}

@ -5,16 +5,15 @@ use std::{
use crate::{
consts::{NODE_ARCHIVE_SUFFIX, NODE_DIST_URL},
error::ReqwestError,
utils::progress_bar,
};
use self::error::{ApiError, ApiResult};
use reqwest::Client;
pub mod error;
mod model;
use futures_util::StreamExt;
use miette::{miette, Context, IntoDiagnostic, Result};
pub use model::*;
use tokio::io::{AsyncWrite, AsyncWriteExt};
@ -44,14 +43,18 @@ impl WebApi {
/// Returns the list of available node versions
#[tracing::instrument(level = "trace")]
pub async fn get_versions(&self) -> ApiResult<Vec<VersionInfo>> {
pub async fn get_versions(&self) -> Result<Vec<VersionInfo>> {
let versions = self
.client
.get(format!("{}/index.json", self.base_url))
.send()
.await?
.await
.map_err(ReqwestError::from)
.context("fetching versions")?
.json()
.await?;
.await
.map_err(ReqwestError::from)
.context("fetching versions")?;
Ok(versions)
}
@ -63,7 +66,7 @@ impl WebApi {
&self,
version: S,
writer: &mut W,
) -> ApiResult<u64> {
) -> Result<u64> {
let res = self
.client
.get(format!(
@ -71,23 +74,31 @@ impl WebApi {
self.base_url, *NODE_ARCHIVE_SUFFIX
))
.send()
.await?;
.await
.map_err(ReqwestError::from)
.context("downloading nodejs")?;
let total_size = res
.content_length()
.ok_or_else(|| ApiError::other("Missing 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?;
writer.write_all(&chunk).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?;
writer.flush().await.into_diagnostic()?;
pb.finish_with_message(format!("Downloaded node v{version}."));
Ok(total_downloaded)

Loading…
Cancel
Save