Skip to main content

nvm_windows/
lib.rs

1use std::{
2  env,
3  fs::{self, File},
4  io::{self, BufReader, Read},
5  path::{Path, PathBuf},
6};
7
8use anyhow::bail;
9use comfy_table::{Cell, CellAlignment, Table, presets};
10use indicatif::{ProgressBar, ProgressStyle};
11use path_clean::PathClean;
12use regex::Regex;
13use url::Url;
14use zip::ZipArchive;
15
16#[cfg(feature = "debug")]
17use std::sync::OnceLock;
18#[cfg(feature = "debug")]
19use tracing_appender::{
20  non_blocking::WorkerGuard,
21  rolling::{RollingFileAppender, Rotation},
22};
23#[cfg(feature = "debug")]
24use tracing_subscriber::{
25  EnvFilter, Layer, fmt, layer::SubscriberExt, util::SubscriberInitExt,
26};
27
28use crate::model::{ArchSpec, Config, ReleaseDatabase, VersionSpec};
29
30pub mod model;
31
32#[cfg(feature = "debug")]
33static LOG_GUARD: OnceLock<WorkerGuard> = OnceLock::new();
34
35#[cfg(feature = "debug")]
36const LOG_FILTER: &str = "info,nvm_rs=debug,d=debug";
37
38const LIST_COUNT: usize = 20;
39
40pub type Result<T = ()> = anyhow::Result<T>;
41
42#[cfg(feature = "debug")]
43pub fn log_init() {
44  // 1. 滚动文件 Appender(按天轮转,保留30天)
45  let file_appender = RollingFileAppender::builder()
46    .rotation(Rotation::DAILY)
47    .filename_prefix("app")
48    .filename_suffix("log")
49    .max_log_files(30)
50    .build("./logs")
51    .expect("failed to create rolling file appender");
52
53  // 非阻塞写入,避免日志I/O阻塞主线程
54  let (non_blocking_file, guard) =
55    tracing_appender::non_blocking(file_appender);
56
57  // ✅ 将 guard 存入全局静态变量,确保活到程序结束
58  LOG_GUARD.set(guard).expect("logging already initialized");
59
60  // 2. 构建 Layer
61  // 控制台:彩色、人类可读、INFO 级别以上
62  let console_layer = fmt::layer()
63    .with_writer(std::io::stderr)
64    .pretty()
65    .with_filter(EnvFilter::new(LOG_FILTER));
66
67  // 文件:无颜色、带时间戳、DEBUG 级别以上(记录更详细)
68  let file_layer = fmt::layer()
69    .with_writer(non_blocking_file)
70    .with_ansi(false)
71    .with_target(true)
72    .with_line_number(true)
73    .with_filter(EnvFilter::new(LOG_FILTER));
74
75  // 3. 组合并初始化
76  tracing_subscriber::registry()
77    .with(console_layer)
78    .with(file_layer)
79    .init();
80}
81
82pub fn download_file<U, D>(url: U, dest: D) -> Result<()>
83where
84  U: AsRef<str>,
85  D: AsRef<Path>,
86{
87  let url = url.as_ref();
88  let dest = dest.as_ref();
89  // 1. 发起请求
90  let mut resp = ureq::get(url).call()?;
91
92  // 2. 获取文件大小(如果服务器提供了 Content-Length)
93  let total_size = resp
94    .headers()
95    .get("Content-Length")
96    .and_then(|v| v.to_str().ok())
97    .and_then(|s| s.parse::<u64>().ok())
98    .unwrap_or(0);
99
100  // 3. 创建进度条
101  let pb = if total_size > 0 {
102    let pb = ProgressBar::new(total_size);
103    pb.set_style(
104            ProgressStyle::default_bar()
105                .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})")?
106                .progress_chars("#>-"),
107        );
108    pb
109  } else {
110    // 未知大小时使用 spinner
111    let pb = ProgressBar::new_spinner();
112    pb.set_style(
113      ProgressStyle::default_spinner()
114        .template("{spinner:.green} [{elapsed_precise}] {bytes} downloaded")?,
115    );
116    pb
117  };
118
119  // 4. 用 ProgressReader 包装 response body
120  let reader = resp.body_mut().as_reader();
121  let mut progress_reader = pb.wrap_read(reader);
122
123  // 5. 写入文件(每次 read/write 都会自动更新进度条)
124  let mut file = File::create(dest)?;
125  io::copy(&mut progress_reader, &mut file)?;
126
127  // 6. 完成
128  pb.finish_with_message("Download complete!");
129  Ok(())
130}
131
132pub fn list_local_versions(config: Config) -> Result<()> {
133  let (current_version, current_arch) = get_current_version_and_arch();
134  log::debug!("current version: {}({}bit)", current_version, current_arch);
135
136  let path = get_root(&config)?;
137  let versions = get_local_versions(path)?;
138
139  println!();
140  for version in versions {
141    // let version = version.strip_prefix('v').unwrap_or(version);
142    log::debug!("found version: {version}");
143
144    if version == current_version {
145      println!(
146        "  * {} (Currently using {}-bit executable)",
147        version, current_arch
148      );
149    } else {
150      println!("    {}", version);
151    }
152  }
153
154  println!();
155
156  Ok(())
157}
158
159/// Get all local installed versions.
160/// # Returns
161/// A vector of strings, each string is a version number. (e.g. "v24.2.2")
162fn get_local_versions<T>(root: T) -> Result<Vec<String>>
163where
164  T: AsRef<Path>,
165{
166  let mut versions = Vec::new();
167  let re = Regex::new(r"v\d+\.\d+\.\d+").unwrap();
168
169  for entry in fs::read_dir(root.as_ref())? {
170    let entry = entry?;
171    if !entry.file_type()?.is_dir() {
172      continue;
173    }
174
175    if let Some(name) = entry.file_name().to_str()
176      && re.is_match(name)
177    {
178      versions.push(name.to_string());
179    } else {
180      log::debug!("skipping invalid directory entry: {:?}", entry.path());
181    }
182  }
183
184  Ok(versions)
185}
186
187/// Get the current version and architecture of Node.js.
188/// # Returns
189/// A tuple of strings, the first string is the version number, the second string is the architecture.
190/// (e.g. "v24.2.2", "64")
191pub fn get_current_version_and_arch() -> (String, String) {
192  let Ok(output) = std::process::Command::new("node")
193    .arg("-p")
194    .arg("`${process.version},${process.arch}`") // print v24.2.2,x64
195    .output()
196  else {
197    return (String::new(), String::new());
198  };
199
200  let stdout = String::from_utf8_lossy(&output.stdout);
201  let mut parts = stdout.trim_end().split(',');
202
203  let Some(version) = parts.next() else {
204    return (String::new(), String::new());
205  };
206  let arch = match parts.next() {
207    Some("x64") => "64",
208    Some(_) => "32",
209    None => return (String::new(), String::new()),
210  };
211  log::debug!("version: {}", version);
212
213  (version.to_string(), arch.to_string())
214}
215
216pub fn get_processor_architecture() -> String {
217  // PROCESSOR_ARCHITEW6432 仅在 WOW64 子系统中存在
218  if env::var("PROCESSOR_ARCHITEW6432").is_ok() {
219    // 当前是 64 位系统上的 32 位进程
220    return "x64".to_string();
221  }
222
223  match env::var("PROCESSOR_ARCHITECTURE") {
224    // todo: 没有考虑 arm 架构
225    Ok(val) if val.eq_ignore_ascii_case("AMD64") => "x64".to_string(),
226    Ok(val) => val.to_ascii_lowercase(),
227    Err(_) => String::new(),
228  }
229}
230
231pub fn display_current() -> Result<()> {
232  let (current_version, _) = get_current_version_and_arch();
233  if !current_version.is_empty() {
234    println!("current version is {}", current_version);
235  } else {
236    println!("No current version. Run 'nvm use x.x.x' to set a version.");
237  }
238
239  Ok(())
240}
241
242pub fn display_or_update_proxy(
243  mut config: Config,
244  url: Option<String>,
245) -> Result<()> {
246  if let Some(url) = url {
247    config.proxy = if url.eq_ignore_ascii_case("none") {
248      None
249    } else {
250      Some(url)
251    };
252    config.save()?;
253  } else if let Some(proxy) = config.proxy {
254    println!("Current Proxy: {:?}", proxy);
255  } else {
256    println!("No Proxy set.");
257  }
258
259  Ok(())
260}
261
262pub fn display_or_update_root<T>(
263  mut config: Config,
264  path: Option<T>,
265) -> Result<()>
266where
267  T: AsRef<Path>,
268{
269  if let Some(path) = path {
270    let p = path.as_ref();
271    if !p.exists() {
272      bail!("path {:?} does not exist", p);
273    }
274    if !p.is_dir() {
275      bail!("path {:?} is not a directory", p);
276    }
277    config.root = Some(p.to_path_buf());
278    config.save()?;
279    println!("Current Root: {:?}", p);
280  } else if let Some(root) = config.root {
281    println!("Current Root: {:?}", root);
282  } else {
283    println!("No Root set.");
284  }
285
286  Ok(())
287}
288
289pub fn display_or_update_node_mirror(
290  mut config: Config,
291  url: Option<String>,
292) -> Result<()> {
293  if let Some(url) = url {
294    config.node_mirror = Some(url);
295    config.save()?;
296  } else if let Some(node_mirror) = config.node_mirror {
297    println!("Current NodeMirror: {:?}", node_mirror);
298  } else {
299    println!("No NodeMirror set.");
300  }
301
302  Ok(())
303}
304
305pub fn display_or_update_npm_mirror(
306  mut config: Config,
307  url: Option<String>,
308) -> Result<()> {
309  if let Some(url) = url {
310    config.npm_mirror = Some(url);
311    config.save()?;
312  } else if let Some(npm_mirror) = config.npm_mirror {
313    println!("Current NpmMirror: {:?}", npm_mirror);
314  } else {
315    println!("No NpmMirror set.");
316  }
317
318  Ok(())
319}
320
321pub fn activate_version(config: Config) -> Result<()> {
322  let symlink = get_nvm_symlink()?;
323  if let Ok(metadata) = fs::symlink_metadata(&symlink)
324    && metadata.is_symlink()
325  {
326    bail!("node is already activated");
327  }
328
329  let root = get_root(&config)?;
330  let versions = get_local_versions(&root)?;
331  let Some(last) = versions.last() else {
332    bail!("no node version installed");
333  };
334
335  create_junction_link(symlink, root.join(last))?;
336
337  println!("node is activated.");
338
339  Ok(())
340}
341
342pub fn deactivate_version() -> Result<()> {
343  let symlink = get_nvm_symlink()?;
344  let Ok(_) = fs::symlink_metadata(&symlink) else {
345    bail!("node is already deactivated");
346  };
347
348  delete_junction_link(symlink)?;
349
350  println!("node is deactivated.");
351
352  Ok(())
353}
354
355pub fn display_architecture(config: Config) -> Result<()> {
356  let arch = get_arch(&config.arch);
357  println!("current arch is {}", arch);
358
359  tips(arch);
360
361  Ok(())
362}
363
364pub fn uninstall_version(config: Config, version: VersionSpec) -> Result<()> {
365  let root = get_root(&config)?;
366  let versions = get_local_versions(&root)?;
367  log::debug!("local versions: {:?}", versions);
368
369  match version {
370    VersionSpec::Latest | VersionSpec::Lts => {
371      bail!(
372        "The version must be a specific version. Can not use 'latest' or 'lts' to uninstall."
373      );
374    }
375    VersionSpec::Exact(ver) => {
376      // let ver = format!("v{}", ver);
377      if !versions.contains(&ver) {
378        bail!("version {:?} not installed", ver);
379      }
380      if get_current_version_and_arch().0 == ver {
381        bail!("current version {} is in use, can not uninstall it", ver);
382      }
383
384      delete_version(&root, &ver)?;
385
386      println!("uninstall {} completed.", ver);
387    }
388  }
389
390  Ok(())
391}
392
393fn delete_version(root: &Path, ver: &str) -> Result<()> {
394  let path = root.join(ver);
395  log::debug!("delete: {:?}", path);
396  fs::remove_dir_all(&path)?;
397
398  Ok(())
399}
400
401fn delete_junction_link<T>(link: T) -> Result<()>
402where
403  T: AsRef<Path>,
404{
405  let path = link.as_ref();
406  log::debug!("delete link: {:?}", path);
407  // 链接可能本来就不存在,忽略删除失败
408  if let Err(e) = fs::remove_dir(path) {
409    log::debug!("remove_dir failed: {e}");
410  }
411
412  Ok(())
413}
414
415/// 创建符号链接
416/// # Params
417/// * `link` - 符号路径, 例如 c:\\program files\\nodejs
418/// * `target` - 目标路径, 例如 d:\\nodejs_root\\v18.16.0
419fn create_junction_link<T, L>(link: T, target: L) -> Result<()>
420where
421  T: AsRef<Path>,
422  L: AsRef<Path>,
423{
424  let target = target.as_ref();
425  let link = link.as_ref();
426  log::debug!("create link: {:?} -> {:?}", target, link);
427  #[cfg(target_os = "windows")]
428  junction::create(target, link)?;
429
430  Ok(())
431}
432
433/// 重置符号链接
434/// # Params
435/// * `link` - 符号路径, 例如 c:\\program files\\nodejs
436/// * `target` - 目标路径, 例如 d:\\nodejs_root\\v18.16.0
437fn reset_junction_link<T, L>(link: T, target: L) -> Result<()>
438where
439  T: AsRef<Path>,
440  L: AsRef<Path>,
441{
442  delete_junction_link(&link)?;
443  create_junction_link(link, target)?;
444
445  Ok(())
446}
447
448/// 提示<br>
449/// 如果使用的是 32 位版本, 则提示用户使用 64 位版本
450/// # Params
451/// * `arch` - 架构, 例如 x64, x86
452/// # Returns
453/// * `()`
454fn tips(arch: &str) {
455  if arch != "x64" {
456    println!(
457      "\n* Notice: Since version v23.0.0, 32-bit versions are no longer available. Please use the 64-bit version."
458    );
459  }
460}
461
462fn file_validate<T>(path: T, sha256_checksum: &str) -> Result<bool>
463where
464  T: AsRef<Path>,
465{
466  use sha2::Digest;
467
468  let file = std::fs::File::open(path)?;
469  let mut reader = BufReader::with_capacity(256 * 1024, file);
470  let mut hasher = sha2::Sha256::new();
471  let mut buf = [0u8; 64 * 1024];
472  // 读取错误必须向上传播,避免对损坏的半成品文件计算出错误摘要
473  loop {
474    let n = reader.read(&mut buf)?;
475    if n == 0 {
476      break;
477    }
478    hasher.update(&buf[..n]);
479  }
480
481  Ok(hex::encode(hasher.finalize()) == sha256_checksum)
482}
483
484/// 检查服务器上的版本是否存在, 如果不存在则报错<br>
485/// 检查本地是否存在该版本的目录<br>
486/// 如果存在则返回版本号, 例如 v1.1.0
487fn version_exists<T>(
488  db: &ReleaseDatabase,
489  version: &VersionSpec,
490  root: T,
491) -> Result<(bool, String)>
492where
493  T: AsRef<Path>,
494{
495  let ver = match version {
496    VersionSpec::Latest => {
497      if let Some(latest_version) = db.latest() {
498        latest_version
499      } else {
500        bail!("No latest version found.")
501      }
502    }
503    VersionSpec::Lts => {
504      if let Some(lts_version) = db.latest_lts() {
505        lts_version
506      } else {
507        bail!("No LTS version found.")
508      }
509    }
510    // todo: 修改为 v1.1.0 格式, 而不是 1.1.0
511    VersionSpec::Exact(s) => {
512      if db.version_exists(s) {
513        s.clone()
514      } else {
515        bail!("node v{} not installed.", s)
516      }
517    }
518  };
519
520  let root = root.as_ref();
521  let exists = fs::exists(root.join(&ver))?;
522
523  Ok((exists, ver))
524}
525
526fn get_node_file_url(version: &str, arch: &str, base_url: &str) -> String {
527  let url =
528    format!("{}/{}/node-{}-win-{}.zip", base_url, version, version, arch);
529  log::debug!("url: {:?}", url);
530  url
531}
532
533fn get_node_file_checksum_url(version: &str, base_url: &str) -> String {
534  let url = format!("{}/{}/SHASUMS256.txt", base_url, version);
535  log::debug!("url: {:?}", url);
536  url
537}
538
539fn zip_extract<T, R>(source: T, dest: R) -> Result<()>
540where
541  T: AsRef<Path>,
542  R: AsRef<Path>,
543{
544  let file = std::fs::File::open(source)?;
545  let reader = BufReader::new(file);
546  let mut archive = ZipArchive::new(reader)?;
547  let pb = ProgressBar::new(archive.len() as u64);
548  pb.set_style(
549    ProgressStyle::default_bar()
550      .template("extracting [{bar:40}] [{percent:.2}%]")
551      .expect("invalid template")
552      .progress_chars("=> "),
553  );
554  for i in 0..archive.len() {
555    let mut entry = archive.by_index(i)?;
556    safe_extract(&mut entry, &dest)?;
557    pb.inc(1);
558  }
559  pb.finish();
560  log::debug!("extract done");
561  Ok(())
562}
563
564fn safe_extract<T>(
565  entry: &mut zip::read::ZipFile<'_, BufReader<File>>,
566  dest: T,
567) -> Result<()>
568where
569  T: AsRef<Path>,
570{
571  let dest_dir = dest.as_ref();
572
573  // 防止 Zip Slip:解压后的路径必须仍在目标目录内
574  let out_path = dest_dir.join(entry.mangled_name());
575  if !out_path.starts_with(dest_dir) {
576    bail!("illegal path in archive: {}", entry.name());
577  }
578
579  if entry.is_dir() {
580    std::fs::create_dir_all(&out_path)?;
581  } else if let Some(parent) = out_path.parent() {
582    std::fs::create_dir_all(parent)?;
583    let mut outfile = std::fs::File::create(&out_path)?;
584    std::io::copy(&mut entry.take(100 * 1024 * 1024), &mut outfile)?; // 限制100MB
585  }
586  Ok(())
587}
588
589fn delete_zip_files<T>(root: T) -> Result<()>
590where
591  T: AsRef<Path>,
592{
593  let root = root.as_ref();
594  for entry in fs::read_dir(root)? {
595    let entry = entry?;
596    if entry.file_type()?.is_file()
597      && entry.file_name().to_string_lossy().ends_with(".zip")
598    {
599      let file_path = entry.path();
600      log::debug!("delete zip file: {}", file_path.display());
601      fs::remove_file(file_path)?;
602    }
603  }
604  Ok(())
605}
606
607fn get_arch(arch: &Option<ArchSpec>) -> &'static str {
608  match arch {
609    Some(ArchSpec::X64) => "x64",
610    Some(ArchSpec::X86) => "x86",
611    // todo: 没有考虑 arm 架构
612    None if get_processor_architecture().ends_with("64") => "x64",
613    None => "x86",
614  }
615}
616
617fn get_root(config: &Config) -> Result<PathBuf> {
618  Ok(match &config.root {
619    Some(root) => root.clone(),
620    None => env::current_dir()?,
621  })
622}
623
624fn get_node_mirror(config: &Config) -> String {
625  config
626    .node_mirror
627    .clone()
628    .unwrap_or_else(|| "https://nodejs.org/dist".to_string())
629}
630
631fn get_nvm_symlink() -> Result<PathBuf> {
632  Ok(env::var("NVM_SYMLINK")?.into())
633}
634
635pub fn list_remote_versions(config: Config) -> Result<()> {
636  // get_release_db appends "index.json" to the mirror base url itself.
637  let base_url = get_node_mirror(&config);
638  log::debug!("url: {}", base_url);
639
640  let release_database = get_release_db(&base_url)?;
641
642  let latest_version = release_database.latest_list(LIST_COUNT);
643
644  let lts_version = release_database.lts_list(LIST_COUNT);
645
646  log::debug!("latest_version: {:?}", latest_version);
647  log::debug!("lts_version: {:?}", lts_version);
648
649  let mut table = Table::new();
650  table.load_style(presets::UTF8_FULL_CONDENSED).set_header([
651    Cell::new("current").set_alignment(CellAlignment::Center),
652    Cell::new("lts").set_alignment(CellAlignment::Center),
653  ]);
654
655  for (c, l) in latest_version.iter().zip(lts_version.iter()) {
656    table.add_row(vec![
657      Cell::new(c).set_alignment(CellAlignment::Center),
658      Cell::new(l).set_alignment(CellAlignment::Center),
659    ]);
660  }
661
662  println!("{}", table);
663
664  //  You can use --full to show all versions.
665  println!(
666    "\n * Note: The list only shows the latest {} versions. Visit https://nodejs.org/en/ for more info.",
667    LIST_COUNT
668  );
669
670  Ok(())
671}
672
673pub fn list_versions(config: Config, is_remote_request: bool) -> Result<()> {
674  if is_remote_request {
675    list_remote_versions(config)
676  } else {
677    list_local_versions(config)
678  }
679}
680
681pub fn install_version(
682  config: Config,
683  version: VersionSpec,
684  arch: Option<ArchSpec>,
685  skip_checksum: bool,
686) -> Result<()> {
687  let root = get_root(&config)?;
688
689  let arch = get_arch(&arch);
690  log::debug!("install: {:?} {}", version, arch);
691  tips(arch);
692
693  let base_url = get_node_mirror(&config);
694
695  let db = get_release_db(&base_url)?;
696  let (exists, ver) = version_exists(&db, &version, &root)?;
697  if exists {
698    bail!("version {} already installed", ver);
699  }
700
701  let url = get_node_file_url(&ver, arch, &base_url);
702  log::debug!("download url: {}", url);
703
704  let root = root.as_path();
705  let Some(file_name) = Path::new(&url).file_name() else {
706    bail!("invalid download url: {url}");
707  };
708  let zip_path = root.join(file_name);
709
710  download_file(&url, &zip_path)?;
711
712  if !skip_checksum {
713    let url = get_node_file_checksum_url(&ver, &base_url);
714    let checksum = load_checksum(&url, &ver, arch)?;
715    let valid = file_validate(&zip_path, &checksum)?;
716    if !valid {
717      bail!("sha256 checksum failed: {:?}", file_name);
718    }
719
720    println!("checksum valid.");
721  }
722
723  zip_extract(zip_path, root)?;
724
725  let org_path = root.join(Path::new(file_name).with_extension(""));
726  let dist_path = root.join(&ver);
727  log::debug!("rename {:?} -> {:?}", org_path, dist_path);
728  fs::rename(org_path, dist_path)?;
729
730  delete_zip_files(root)?;
731
732  println!("install {} completed.", ver);
733
734  Ok(())
735}
736
737pub fn switch_version(
738  config: Config,
739  version: VersionSpec,
740  arch: Option<ArchSpec>,
741) -> Result<()> {
742  let root = get_root(&config)?;
743  let arch = get_arch(&arch);
744
745  let base_url = get_node_mirror(&config);
746
747  let db = get_release_db(&base_url)?;
748  let (exists, ver) = version_exists(&db, &version, &root)?;
749  let (current_ver, _) = get_current_version_and_arch();
750  if !exists {
751    bail!("version {:?} not installed", ver);
752  }
753  if current_ver == ver {
754    bail!("version {:?} is already used", ver);
755  }
756
757  tips(arch);
758
759  log::debug!("ready switch to {:?}({})", version, arch);
760
761  let node_path = root.join(&ver).clean();
762
763  // 创建符号链接, 仅在 Windows 上有效
764  reset_junction_link(get_nvm_symlink()?, node_path)?;
765
766  println!("switch to {} success.", ver);
767
768  // 提示用户使用 64 位版本
769  tips(arch);
770
771  Ok(())
772}
773
774fn get_release_db<T>(base_url: T) -> Result<ReleaseDatabase>
775where
776  T: AsRef<str>,
777{
778  let base_url = base_url.as_ref();
779  log::debug!("base url: {}", base_url);
780
781  let url: Url = base_url.parse()?;
782  let target_url = url.join("index.json")?;
783  log::debug!("target url: {}", target_url);
784
785  let json_data: ReleaseDatabase = ureq::get(target_url.as_str())
786    .call()?
787    .body_mut()
788    .read_json()?;
789  // log::debug!("body len: {}", body.len());
790
791  // let release_db: ReleaseDatabase = serde_json::from_str(&body)?;
792  log::debug!("node_release_info count: {}", json_data.len());
793
794  Ok(json_data)
795}
796
797// fn download_version<T>(url: &str, dest: T) -> Result<()>
798// where
799//   T: AsRef<Path>,
800// {
801//   let resp = reqwest::get(url).await?.error_for_status()?;
802//   let total = resp.content_length().unwrap_or(0);
803//   let mut stream = ProgressStream::new(resp.bytes_stream(), total);
804
805//   let mut file = tokio::fs::File::create(dest).await?;
806//   tokio::io::copy(&mut stream, &mut file).await?;
807//   stream.finish();
808
809//   Ok(())
810// }
811
812fn load_checksum<B, V, A>(base_url: B, version: V, arch: A) -> Result<String>
813where
814  B: AsRef<str>,
815  V: AsRef<str>,
816  A: AsRef<str>,
817{
818  let base_url = base_url.as_ref();
819  let version = version.as_ref();
820  let arch = arch.as_ref();
821
822  let text_data = ureq::get(base_url).call()?.body_mut().read_to_string()?;
823  log::debug!("body len: {}", text_data.len());
824
825  let package_name = format!("node-{}-win-{}.zip", version, arch);
826  for line in text_data.lines() {
827    // SHASUMS256.txt: "<64位哈希>  node-vX.Y.Z-win-x64.zip"
828    let mut parts = line.split_ascii_whitespace();
829    let (Some(checksum), Some(name)) = (parts.next(), parts.next()) else {
830      continue;
831    };
832    if name == package_name {
833      return Ok(checksum.to_string());
834    }
835  }
836
837  bail!("checksum not found: {:?}", package_name);
838}