#![allow(dead_code)] use std::path::Path; use bytes::Buf; use color_eyre::Result; use indexmap::IndexMap; use node_semver::Version; use reqwest::Client; use serde::Deserialize; use tokio::task::spawn_blocking; use tracing::{debug, info}; use crate::{ create_dir_if_not_exists, manifest::{Bugs, Human, Person, Repository}, PackageJson, WrapErr, }; #[derive(Debug, Deserialize)] pub struct Dist { pub shasum: String, pub tarball: String, pub integrity: Option, #[serde(rename = "fileCount")] pub file_count: Option, #[serde(rename = "unpackedSize")] pub unpacked_size: Option, #[serde(rename = "npm-signature")] pub npm_signature: Option, } #[derive(Debug, Deserialize)] pub struct VersionMeta { pub _from: Option, pub _id: String, #[serde(rename = "_nodeVersion")] pub _node_version: String, #[serde(rename = "_npmUser")] pub _npm_user: Person, #[serde(rename = "_npmVersion")] pub _npm_version: String, pub _shasum: Option, #[serde(rename = "_hasShrinkwrap")] pub _has_shrinkwrap: Option, pub dist: Dist, pub files: Vec, #[serde(flatten)] pub package_json: PackageJson, } #[derive(Debug, Deserialize)] pub struct PackageMeta { pub _id: String, pub _rev: String, #[serde(rename = "dist-tags")] pub dist_tags: IndexMap, pub name: String, pub time: IndexMap, pub users: IndexMap, pub versions: IndexMap, pub author: Human, pub bugs: Option, pub contributors: Option>, pub description: Option, pub homepage: Option, pub keywords: Option>, pub license: Option, pub maintainers: Option>, pub readme: Option, #[serde(rename = "readmeFilename")] pub readme_filename: Option, pub repository: Option, } #[derive(Default)] pub struct NpmClient { reqwest: Client, } const BASE_URL: &str = "https://registry.npmjs.org"; impl NpmClient { pub fn new() -> Self { let reqwest = Client::new(); Self { reqwest } } #[tracing::instrument(skip(self))] pub async fn fetch_package_meta(&self, name: &str) -> Result { let res = self .reqwest .get(format!("{BASE_URL}/{name}")) .send() .await?; let code = res.status(); let body = res.text().await?; let meta = serde_json::from_str::(&body)?; debug!(?code, ?meta, "Received response"); Ok(meta) } #[tracing::instrument(skip(self))] pub async fn download_package(&self, name: &str, url: &str) -> Result<()> { let module = Path::new("node_modules").join(name); create_dir_if_not_exists(&module).await?; let response = self .reqwest .get(url) .send() .await .wrap_err("getting response")?; let tarball = response.bytes().await.wrap_err("fetching body")?; spawn_blocking(move || -> Result<()> { let tar = flate2::read::GzDecoder::new(tarball.reader()); let mut archive = tar::Archive::new(tar); for entry in archive.entries()? { let mut entry = entry?; let path = entry.path()?; let path = path .strip_prefix("package") .wrap_err("file name must start with package/")?; let path = module.join(path); info!(?path, "Unpacking file"); entry .unpack(&path) .wrap_err(format!("unpacking file {}", path.display()))?; } Ok(()) }) .await??; info!("successfully downloaded package"); Ok(()) } }