ambient_ci/action_impl/
npm.rs1use std::{
2 collections::HashMap,
3 error::Error,
4 path::{Path, PathBuf},
5};
6
7use serde::{Deserialize, Serialize};
8use url::Url;
9
10use crate::{
11 action::{ActionError, Context},
12 action_impl::ActionImpl,
13 runlog::RunLogSource,
14 util::{http_get_to_file, mkdir, UtilError},
15};
16
17const LOCK_FILES: &[&str] = &["npm-shrinkwrap.json", "package-lock.json"];
19
20const SUBDIR: &str = "npm";
21
22#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct NpmGet {}
28
29impl NpmGet {
30 fn load_lock_file(&self, srcdir: &Path) -> Result<PackagesLock, NpmError> {
31 for filename in LOCK_FILES.iter() {
32 let filename = srcdir.join(filename);
33 if filename.exists() {
34 let data = std::fs::read(&filename)
35 .map_err(|err| NpmError::ReadLockFile(filename.to_path_buf(), err))?;
36 let lockfile: PackagesLock = serde_json::from_slice(&data)
37 .map_err(|err| NpmError::ParseLockFile(filename.to_path_buf(), err))?;
38 return Ok(lockfile);
39 }
40 }
41
42 Err(NpmError::NoLockFile(LOCK_FILES))
43 }
44
45 fn download(&self, npm_dir: PathBuf, lockfile: PackagesLock) -> Result<(), NpmError> {
46 mkdir(&npm_dir).map_err(|err| NpmError::Mkdir2(npm_dir.to_path_buf(), err))?;
47
48 for (name, p) in lockfile.packages.iter().filter(|(n, _)| !n.is_empty()) {
49 let filename = npm_dir.join(name);
50 let dir = filename
51 .parent()
52 .ok_or(NpmError::NoParent(filename.to_path_buf()))?;
53 if !dir.exists() {
54 mkdir(dir).map_err(|err| NpmError::Mkdir2(dir.to_path_buf(), err))?;
55 }
56
57 let url = Url::parse(&p.resolved)
58 .map_err(|err| NpmError::UrlParse(p.resolved.clone(), err))?;
59 let filename = npm_dir.join(format!("{}.tgz", name));
60 http_get_to_file(url.as_str(), &filename)
61 .map_err(|err| NpmError::HttpGet(url, filename.clone(), err))?;
62 }
63
64 Ok(())
65 }
66}
67
68impl ActionImpl for NpmGet {
69 fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
70 match self.load_lock_file(context.source_dir()) {
71 Ok(lockfile) => {
72 if let Err(err) = self.download(context.deps_dir().join(SUBDIR), lockfile) {
73 context.runlog().npm_get_failed(RunLogSource::PrePlan, &err);
74 Err(err)?
75 }
76 }
77 Err(err) => {
78 context.runlog().npm_get_failed(RunLogSource::PrePlan, &err);
79 Err(err)?
80 }
81 }
82
83 context.runlog().npm_get_succeeded(RunLogSource::PrePlan);
84 Ok(())
85 }
86}
87
88#[derive(Debug, Deserialize)]
89#[allow(dead_code)]
90struct PackagesLock {
91 packages: HashMap<String, Package>,
92}
93
94#[derive(Debug, Default, Deserialize)]
95#[allow(dead_code)]
96#[serde(default)]
97struct Package {
98 resolved: String,
99}
100
101#[derive(Debug, thiserror::Error)]
103pub enum NpmError {
104 #[error("failed to find lock file, tried {0:?}")]
106 NoLockFile(&'static [&'static str]),
107
108 #[error("failed to read package lock file {0}")]
110 ReadLockFile(PathBuf, #[source] std::io::Error),
111
112 #[error("failed to parse package lock file {0}")]
114 ParseLockFile(PathBuf, #[source] serde_json::Error),
115
116 #[error("could not create artifacts directory {0}")]
118 Mkdir2(PathBuf, #[source] crate::util::UtilError),
119
120 #[error("failed to determine directory of path {0}")]
122 NoParent(PathBuf),
123
124 #[error("failed to parse URL {0:?}")]
126 UrlParse(String, #[source] url::ParseError),
127
128 #[error("failed to download {0} to {1}")]
130 HttpGet(Url, PathBuf, #[source] UtilError),
131}
132
133impl From<NpmError> for ActionError {
134 fn from(value: NpmError) -> Self {
135 Self::Npm(value)
136 }
137}
138
139impl From<NpmError> for String {
140 fn from(err: NpmError) -> Self {
141 let mut msg = format!("ERROR: {err}");
142 let mut source = err.source();
143 while let Some(src) = source {
144 msg.push_str(&format!("caused by: {src}"));
145 source = src.source();
146 }
147
148 msg
149 }
150}