use std::{
collections::HashMap,
error::Error,
path::{Path, PathBuf},
};
use serde::{Deserialize, Serialize};
use url::Url;
use crate::{
action::{ActionError, Context},
action_impl::ActionImpl,
runlog::RunLogSource,
util::{http_get_to_file, mkdir, UtilError},
};
const LOCK_FILES: &[&str] = &["npm-shrinkwrap.json", "package-lock.json"];
const SUBDIR: &str = "npm";
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NpmGet {}
impl NpmGet {
fn load_lock_file(&self, srcdir: &Path) -> Result<PackagesLock, NpmError> {
for filename in LOCK_FILES.iter() {
let filename = srcdir.join(filename);
if filename.exists() {
let data = std::fs::read(&filename)
.map_err(|err| NpmError::ReadLockFile(filename.to_path_buf(), err))?;
let lockfile: PackagesLock = serde_json::from_slice(&data)
.map_err(|err| NpmError::ParseLockFile(filename.to_path_buf(), err))?;
return Ok(lockfile);
}
}
Err(NpmError::NoLockFile(LOCK_FILES))
}
fn download(&self, npm_dir: PathBuf, lockfile: PackagesLock) -> Result<(), NpmError> {
mkdir(&npm_dir).map_err(|err| NpmError::Mkdir2(npm_dir.to_path_buf(), err))?;
for (name, p) in lockfile.packages.iter().filter(|(n, _)| !n.is_empty()) {
let filename = npm_dir.join(name);
let dir = filename
.parent()
.ok_or(NpmError::NoParent(filename.to_path_buf()))?;
if !dir.exists() {
mkdir(dir).map_err(|err| NpmError::Mkdir2(dir.to_path_buf(), err))?;
}
let url = Url::parse(&p.resolved)
.map_err(|err| NpmError::UrlParse(p.resolved.clone(), err))?;
let filename = npm_dir.join(format!("{}.tgz", name));
http_get_to_file(url.as_str(), &filename)
.map_err(|err| NpmError::HttpGet(url, filename.clone(), err))?;
}
Ok(())
}
}
impl ActionImpl for NpmGet {
fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
match self.load_lock_file(context.source_dir()) {
Ok(lockfile) => {
if let Err(err) = self.download(context.deps_dir().join(SUBDIR), lockfile) {
context.runlog().npm_get_failed(RunLogSource::PrePlan, &err);
Err(err)?
}
}
Err(err) => {
context.runlog().npm_get_failed(RunLogSource::PrePlan, &err);
Err(err)?
}
}
context.runlog().npm_get_succeeded(RunLogSource::PrePlan);
Ok(())
}
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct PackagesLock {
packages: HashMap<String, Package>,
}
#[derive(Debug, Default, Deserialize)]
#[allow(dead_code)]
#[serde(default)]
struct Package {
resolved: String,
}
#[derive(Debug, thiserror::Error)]
pub enum NpmError {
#[error("failed to find lock file, tried {0:?}")]
NoLockFile(&'static [&'static str]),
#[error("failed to read package lock file {0}")]
ReadLockFile(PathBuf, #[source] std::io::Error),
#[error("failed to parse package lock file {0}")]
ParseLockFile(PathBuf, #[source] serde_json::Error),
#[error("could not create artifacts directory {0}")]
Mkdir2(PathBuf, #[source] crate::util::UtilError),
#[error("failed to determine directory of path {0}")]
NoParent(PathBuf),
#[error("failed to parse URL {0:?}")]
UrlParse(String, #[source] url::ParseError),
#[error("failed to download {0} to {1}")]
HttpGet(Url, PathBuf, #[source] UtilError),
}
impl From<NpmError> for ActionError {
fn from(value: NpmError) -> Self {
Self::Npm(value)
}
}
impl From<NpmError> for String {
fn from(err: NpmError) -> Self {
let mut msg = format!("ERROR: {err}");
let mut source = err.source();
while let Some(src) = source {
msg.push_str(&format!("caused by: {src}"));
source = src.source();
}
msg
}
}