git.delta.rocks / jrsonnet / refs/commits / 80f6128c09ce

difftreelog

source

crates/jrsonnet-pkg/src/install/github.rs5.6 KiBsourcehistory
1#![allow(clippy::result_large_err)]23use std::{4	collections::HashSet,5	fs::{self, File},6	io::Write as _,7	path::{Path, PathBuf},8};910use camino::Utf8PathBuf;11use reqwest::{blocking::Response, header};12use tracing::{debug, info, warn};1314use super::{15	Error, LocalExtraction, ResolveResult, Result, VendorSource,16	accessor::{AccessorEntry, ZipFileAccessor},17	make_symlink,18};19use crate::{20	install::{PKG_USER_AGENT, cache_dir},21	jsonnet_bundler::{Dependency, GitSource, JsonnetFile, Source, SubDir},22};2324fn is_sha(s: &str) -> bool {25	s.len() == 40 && s.bytes().all(|b| b.is_ascii_hexdigit())26}2728fn commit_cache_path(source: &GitSource, sha: &str) -> Result<PathBuf> {29	Ok(cache_dir("github")?30		.join(source.plain_repo_name())31		.join(format!("{sha}.zip")))32}3334fn resolve_sha(source: &GitSource, version: &str) -> Result<String> {35	let url = format!(36		"https://api.github.com/repos/{}/commits/{}",37		source.plain_repo_name(),38		version39	);40	let response = reqwest::blocking::Client::new()41		.get(&url)42		.header(header::ACCEPT, "application/vnd.github.sha")43		.header(header::USER_AGENT, PKG_USER_AGENT)44		.send()45		.and_then(Response::error_for_status)?;46	let sha = response.text()?;47	Ok(sha.trim().to_owned())48}4950fn fetch_zip(source: &GitSource, sha: &str) -> Result<ZipFileAccessor> {51	let cached = commit_cache_path(source, sha)?;52	if cached.exists() {53		debug!("using cached archive {}", cached.display());54		return Ok(ZipFileAccessor::new_prefixed(55			File::open(&cached).map_err(|e| Error::Io(cached.clone(), e))?,56		)?);57	}5859	let url = format!(60		"https://github.com/{}/archive/{}.zip",61		source.plain_repo_name(),62		sha63	);64	info!("downloading {url}");6566	let bytes = reqwest::blocking::Client::new()67		.get(&url)68		.header(header::USER_AGENT, PKG_USER_AGENT)69		.send()70		.and_then(Response::error_for_status)?71		.bytes()?;7273	if let Some(parent) = cached.parent() {74		fs::create_dir_all(parent).map_err(|e| Error::Io(parent.to_owned(), e))?;75	}76	let mut downloaded = File::create_new(&cached).map_err(|e| Error::Io(cached.clone(), e))?;77	downloaded78		.write_all(&bytes)79		.map_err(|e| Error::Io(cached.clone(), e))?;8081	Ok(ZipFileAccessor::new_prefixed(downloaded)?)82}8384fn open_cached_zip(zip_path: &Path) -> Result<ZipFileAccessor> {85	Ok(ZipFileAccessor::new_prefixed(86		File::open(zip_path).map_err(|e| Error::Io(zip_path.to_owned(), e))?,87	)?)88}8990fn extract_subdir(archive: &ZipFileAccessor, subdir: &SubDir, dest: &Path) -> Result<()> {91	archive.iter(subdir, &mut |name, entry| {92		let target = dest.join(&name);93		match entry {94			AccessorEntry::Dir => {95				fs::create_dir_all(&target).map_err(|e| Error::Io(target, e))?;96			}97			AccessorEntry::File(data) => {98				if let Some(parent) = target.parent() {99					fs::create_dir_all(parent).map_err(|e| Error::Io(parent.to_owned(), e))?;100				}101				fs::write(&target, &data).map_err(|e| Error::Io(target, e))?;102			}103			AccessorEntry::Symlink(link_target) => {104				let symlink_parent = name105					.as_path()106					.parent()107					.map(|p| SubDir::try_from(Utf8PathBuf::from(p)))108					.transpose()109					.expect("parent of a SubDir is a SubDir")110					.unwrap_or_else(SubDir::empty);111				if link_target.resolve_under(&symlink_parent).is_err() {112					warn!("symlink {name} -> {link_target} escapes extraction; skipping");113					return Ok(());114				}115				if let Some(parent) = target.parent() {116					fs::create_dir_all(parent).map_err(|e| Error::Io(parent.to_owned(), e))?;117				}118				make_symlink(&link_target.to_string(), &target)119					.map_err(|e| Error::Io(target, e))?;120			}121		}122		Ok(())123	})124}125126fn collect_archive_deps(127	archive: &ZipFileAccessor,128	dir: &SubDir,129	git_deps: &mut Vec<Dependency>,130	local_extractions: &mut Vec<LocalExtraction>,131	visited: &mut HashSet<SubDir>,132) -> Result<()> {133	if !visited.insert(dir.clone()) {134		return Ok(());135	}136137	let manifest_path = dir138		.join("jsonnetfile.json")139		.expect("appending a literal filename keeps it within parent");140141	let Some(data) = archive.read(&manifest_path)? else {142		return Ok(());143	};144	let Ok(manifest) = serde_json::from_slice::<JsonnetFile>(&data) else {145		return Ok(());146	};147148	for dep in manifest.dependencies {149		match &dep.source {150			Source::Git(_) => git_deps.push(dep),151			Source::Local(local) => {152				let Ok(child_dir) = local.resolve_under(dir) else {153					tracing::info!("local source {local} escapes its package; skipping");154					continue;155				};156				let name = child_dir157					.file_name()158					.map_or_else(|| local.to_string(), str::to_owned);159				local_extractions.push(LocalExtraction {160					tree_path: child_dir.clone(),161					name,162				});163				collect_archive_deps(archive, &child_dir, git_deps, local_extractions, visited)?;164			}165		}166	}167	Ok(())168}169170pub(super) fn resolve(source: &GitSource, version: Option<&str>) -> Result<ResolveResult> {171	let version_str = version.unwrap_or("HEAD");172	let sha = if is_sha(version_str) {173		version_str.to_owned()174	} else {175		let resolved = resolve_sha(source, version_str)?;176		info!("resolved {version_str} to {resolved}");177		resolved178	};179180	let archive = fetch_zip(source, &sha)?;181182	let mut transitive_git_deps = Vec::new();183	let mut local_extractions = Vec::new();184	let mut visited = HashSet::new();185	collect_archive_deps(186		&archive,187		&source.subdir,188		&mut transitive_git_deps,189		&mut local_extractions,190		&mut visited,191	)?;192193	let zip_path = commit_cache_path(source, &sha)?;194195	Ok(ResolveResult {196		version: sha.clone(),197		transitive_git_deps,198		local_extractions,199		source: VendorSource::GithubZip {200			zip_path,201			commit_sha: sha,202			subdir: source.subdir.clone(),203		},204	})205}206207pub(super) fn extract(zip_path: &Path, subdir: &SubDir, dest: &Path) -> Result<()> {208	let archive = open_cached_zip(zip_path)?;209	extract_subdir(&archive, subdir, dest)210}