From 5c490486c1798237ee9f4367f738347ae278272d Mon Sep 17 00:00:00 2001 From: tuhana Date: Thu, 4 Jul 2024 13:15:28 +0300 Subject: [PATCH] Partly implement `install` command with a progress bar --- Cargo.lock | 159 +++++++++++++++++++++++++++++++++++++- Cargo.toml | 6 +- src/cli/install.rs | 85 +++++++------------- src/cli/list.rs | 23 ++++-- src/types/node/release.rs | 51 +++++++++++- 5 files changed, 258 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1205ead..be70916 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,6 +72,19 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "async-compression" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5" +dependencies = [ + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -228,6 +241,15 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossterm" version = "0.25.0" @@ -296,6 +318,28 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.4.1", + "windows-sys 0.52.0", +] + +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -326,6 +370,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.30" @@ -342,12 +401,34 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.30" @@ -366,8 +447,10 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -797,13 +880,17 @@ name = "nue" version = "0.0.0" dependencies = [ "anyhow", + "async-compression", "clap", + "futures", "indicatif", "inquire", "node-semver", "reqwest", "serde", "tokio", + "tokio-tar", + "tokio-util", ] [[package]] @@ -899,7 +986,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.2", "smallvec", "windows-targets 0.52.5", ] @@ -972,6 +1059,24 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.5.2" @@ -1017,10 +1122,12 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", + "tokio-util", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "winreg", ] @@ -1403,6 +1510,32 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tar" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5714c010ca3e5c27114c1cdeb9d14641ace49874aa5626d7149e47aedace75" +dependencies = [ + "filetime", + "futures-core", + "libc", + "redox_syscall 0.3.5", + "tokio", + "tokio-stream", + "xattr", +] + [[package]] name = "tokio-util" version = "0.7.11" @@ -1611,6 +1744,19 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "wasm-streams" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.69" @@ -1792,6 +1938,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] + [[package]] name = "zeroize" version = "1.8.1" diff --git a/Cargo.toml b/Cargo.toml index 7b1df2d..817c06e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,12 +12,16 @@ lto = "fat" [dependencies] anyhow = "1.0.86" +async-compression = { version = "0.4.11", features = ["gzip", "tokio"] } clap = { version = "4.5.8", features = ["derive"] } +futures = "0.3.30" indicatif = { version = "0.17.8", features = ["improved_unicode"] } inquire = "0.7.5" node-semver = { git = "https://github.com/catuhana/node-semver-rs", features = [ "serde", ] } -reqwest = { version = "0.12.5", features = ["blocking", "json"] } +reqwest = { version = "0.12.5", features = ["blocking", "json", "stream"] } serde = { version = "1.0.203", features = ["derive"] } tokio = { version = "1.38.0", features = ["rt-multi-thread", "sync", "macros"] } +tokio-tar = "0.3.1" +tokio-util = { version = "0.7.11", features = ["io"] } diff --git a/src/cli/install.rs b/src/cli/install.rs index e57c0f9..a903e6e 100644 --- a/src/cli/install.rs +++ b/src/cli/install.rs @@ -3,7 +3,7 @@ use clap::Args; use super::NueCommand; -use crate::{exts::HyperlinkExt, types}; +use crate::types; #[derive(Debug, Default, Clone)] enum VersionInputs { @@ -27,69 +27,44 @@ impl NueCommand for CommandArguments { let progress_bar = indicatif::ProgressBar::new_spinner(); progress_bar.enable_steady_tick(std::time::Duration::from_millis(120)); + // TODO: Deduplicate this, exact same code is also in `list.rs` progress_bar.set_message("Fetching releases..."); - let releases_json: Vec = reqwest::get( - "https://nodejs.org/download/release/index.json", - ) - .await - .context("Failed to fetch releases from `https://nodejs.org/download/release/index.json`")? - .json() - .await - .context("Failed to parse releases JSON")?; + let response = reqwest::get("https://nodejs.org/download/release/index.json") + .await + .context( + "Failed to fetch releases from `https://nodejs.org/download/release/index.json`", + )?; + if !response.status().is_success() { + anyhow::bail!("Failed to fetch releases: {}", response.status()); + } + + progress_bar.set_message("Parsing releases..."); + let releases_json: Vec = response + .json() + .await + .context("Failed to parse releases JSON")?; progress_bar.set_message("Filtering releases based on input..."); - let release_branch: &str; let latest_release = match &self.version { - VersionInputs::VersionString(version) => { - release_branch = version; - - releases_json - .iter() - .find(|release| format!("{}", release.version).starts_with(version)) - } - VersionInputs::Lts(Some(code_name)) => { - release_branch = code_name; - - releases_json.iter().find(|release| { - matches!( - &release.lts, - types::node::LTS::CodeName(name) if *name.to_lowercase() == *code_name - ) - }) - } - VersionInputs::Lts(None) => { - release_branch = "LTS"; - - releases_json - .iter() - .find(|release| release.lts.is_code_name()) - } - VersionInputs::Latest => { - release_branch = "latest"; - - releases_json.iter().max_by_key(|release| &release.version) - } + VersionInputs::VersionString(version) => releases_json + .iter() + .find(|release| format!("{}", release.version).starts_with(version)), + VersionInputs::Lts(Some(code_name)) => releases_json.iter().find(|release| { + matches!( + &release.lts, + types::node::LTS::CodeName(name) if *name.to_lowercase() == *code_name + ) + }), + VersionInputs::Lts(None) => releases_json + .iter() + .find(|release| release.lts.is_code_name()), + VersionInputs::Latest => releases_json.iter().max_by_key(|release| &release.version), }; progress_bar.finish_and_clear(); match latest_release { - Some(release) => { - let version_str = format!("v{}", release.version); - let branch_name = match release_branch { - "latest" => "current", - "LTS" => release_branch, - _ => release_branch, - }; - - println!( - "Installing version {} from `{}` branch", - version_str.hyperlink(format!( - "https://github.com/nodejs/node/releases/tag/{version_str}" - )), - branch_name - ) - } + Some(release) => release.install("test".into()).await?, None => { anyhow::bail!("No release found with given version or LTS code name."); } diff --git a/src/cli/list.rs b/src/cli/list.rs index d091099..8a9681e 100644 --- a/src/cli/list.rs +++ b/src/cli/list.rs @@ -33,14 +33,21 @@ impl NueCommand for CommandArguments { progress_bar.enable_steady_tick(std::time::Duration::from_millis(120)); progress_bar.set_message("Fetching releases..."); - let releases_json: Vec = reqwest::get( - "https://nodejs.org/download/release/index.json", - ) - .await - .context("Failed to fetch releases from `https://nodejs.org/download/release/index.json`")? - .json() - .await - .context("Failed to parse releases JSON")?; + let response = reqwest::get("https://nodejs.org/download/release/index.json") + .await + .context( + "Failed to fetch releases from `https://nodejs.org/download/release/index.json`", + )?; + + if !response.status().is_success() { + anyhow::bail!("Failed to fetch releases: {}", response.status()); + } + + progress_bar.set_message("Parsing releases..."); + let releases_json: Vec = response + .json() + .await + .context("Failed to parse releases JSON")?; progress_bar.set_message("Filtering releases..."); let releases: Vec<_> = releases_json.into_iter().filter(|release| { diff --git a/src/types/node/release.rs b/src/types/node/release.rs index 0e8a6c2..b2dd238 100644 --- a/src/types/node/release.rs +++ b/src/types/node/release.rs @@ -1,6 +1,12 @@ +use async_compression::tokio::bufread::GzipDecoder; +use futures::TryStreamExt; +use indicatif::ProgressBar; use serde::{de::Error as DeError, Deserialize, Deserializer}; +use tokio::io::BufReader; +use tokio_tar::Archive; +use tokio_util::io::StreamReader; -use crate::types; +use crate::{exts::HyperlinkExt, types}; use super::LTS; @@ -13,6 +19,49 @@ pub struct NodeRelease { } impl NodeRelease { + pub async fn install(&self, path: std::path::PathBuf) -> anyhow::Result<()> { + if !self.is_supported_by_current_platform() { + anyhow::bail!("This release is not supported by the current platform."); + } + + let response = reqwest::get(format!( + "https://nodejs.org/dist/v{}/node-v{}-{}.tar.gz", + self.version, + self.version, + types::platforms::Platform::get_system_platform() + )) + .await?; + if !response.status().is_success() { + anyhow::bail!("Failed to download release: {}", response.status()); + } + + let download_progress_bar = ProgressBar::new(0); + download_progress_bar.set_length(response.content_length().unwrap()); + download_progress_bar.set_style(indicatif::ProgressStyle::default_bar().template( + "{msg}\n{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})", + )?.progress_chars("#>-")); + download_progress_bar.set_message(format!( + "Downloading and unpacking version v{}", + self.version.to_string().hyperlink(format!( + "https://github.com/nodejs/node/releases/tag/{}", + self.version + )), + )); + + let data_stream = response + .bytes_stream() + .map_ok(|chunk| { + download_progress_bar.inc(chunk.len() as u64); + chunk + }) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err.to_string())); + + let decompressed = GzipDecoder::new(BufReader::new(StreamReader::new(data_stream))); + Archive::new(decompressed).unpack(path).await?; + + Ok(()) + } + pub fn is_supported_by_current_platform(&self) -> bool { self.files.iter().any(|file| { file.contains(&types::platforms::Platform::get_system_platform().to_string())