139 lines
4.6 KiB
Rust
139 lines
4.6 KiB
Rust
use std::collections::HashSet;
|
|
|
|
use anyhow::{Context, Result, anyhow};
|
|
use futures::future::join_all;
|
|
use nix_compat::nixbase32;
|
|
use nix_compat::store_path::StorePath;
|
|
use object_store::{ObjectStore, aws::AmazonS3, path::Path as ObjectPath};
|
|
use regex::Regex;
|
|
use std::path::Path;
|
|
use tokio::process::Command;
|
|
use tracing::{debug, trace};
|
|
use url::Url;
|
|
|
|
use crate::store::Store;
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
pub struct PathInfo {
|
|
pub path: StorePath<String>,
|
|
pub signatures: Vec<String>,
|
|
pub references: Vec<StorePath<String>>,
|
|
pub nar_size: u64,
|
|
}
|
|
|
|
impl PathInfo {
|
|
pub async fn from_derivation(drv: &Path, store: &Store) -> Result<Self> {
|
|
debug!("query path info for {:?}", drv);
|
|
|
|
let derivation = match drv.extension() {
|
|
Some(ext) if ext == "drv" => drv.as_os_str().as_encoded_bytes(),
|
|
_ => {
|
|
let drv = {
|
|
// resolve symlink
|
|
if drv.is_symlink() {
|
|
&drv.canonicalize()?
|
|
} else {
|
|
drv
|
|
}
|
|
};
|
|
&Command::new("nix")
|
|
.arg("path-info")
|
|
.arg("--derivation")
|
|
.arg(drv)
|
|
.output()
|
|
.await
|
|
.context(format!("run command: nix path-info --derivaiton {drv:?}"))?
|
|
.stdout
|
|
}
|
|
};
|
|
let derivation = String::from_utf8_lossy(derivation);
|
|
debug!("derivation: {derivation}");
|
|
|
|
if derivation.is_empty() {
|
|
return Err(anyhow!(
|
|
"nix path-info did not return a derivation for {drv:#?}"
|
|
));
|
|
}
|
|
|
|
Self::from_path(derivation.trim(), store).await
|
|
}
|
|
|
|
pub async fn from_path(path: &str, store: &Store) -> Result<Self> {
|
|
let store_path =
|
|
StorePath::from_absolute_path(path.as_bytes()).context("storepath from path")?;
|
|
store
|
|
.query_path_info(store_path)
|
|
.await
|
|
.context("query pathinfo for path")
|
|
}
|
|
|
|
// TODO: skip call to query_path_info and return Vec<Path>?
|
|
pub async fn get_closure(&self, store: &Store) -> Result<Vec<Self>> {
|
|
let futs = store
|
|
.compute_fs_closure(self.path.clone())
|
|
.await?
|
|
.into_iter()
|
|
.map(|x| store.query_path_info(x));
|
|
join_all(futs).await.into_iter().collect()
|
|
}
|
|
|
|
/// checks if the path is signed by any upstream. if it is, we assume a cache hit.
|
|
/// the name of the cache in the signature does not have to be the domain of the cache.
|
|
/// in fact, it can be any random string. but, most often it is, and this saves us
|
|
/// a request.
|
|
pub fn check_upstream_signature(&self, upstreams: &[Url]) -> bool {
|
|
let upstreams: HashSet<_> = upstreams.iter().filter_map(|x| x.domain()).collect();
|
|
|
|
// some caches use names prefixed with -<some number>
|
|
// e.g. cache.nixos.org-1, nix-community.cachix.org-1
|
|
let re = Regex::new(r"-\d+$").expect("regex should be valid");
|
|
for signee in self.signees().iter().map(|&x| re.replace(x, "")) {
|
|
if upstreams.contains(signee.as_ref()) {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
fn signees(&self) -> Vec<&str> {
|
|
let signers: Vec<_> = self
|
|
.signatures
|
|
.iter()
|
|
.filter_map(|signature| Some(signature.split_once(":")?.0))
|
|
.collect();
|
|
trace!("signers for {}: {:?}", self.path, signers);
|
|
signers
|
|
}
|
|
|
|
pub async fn check_upstream_hit(&self, upstreams: &[Url]) -> bool {
|
|
for upstream in upstreams {
|
|
let upstream = upstream
|
|
.join(self.narinfo_path().as_ref())
|
|
.expect("adding <hash>.narinfo should make a valid url");
|
|
trace!("querying {}", upstream);
|
|
let res_status = reqwest::Client::new()
|
|
.head(upstream.as_str())
|
|
.send()
|
|
.await
|
|
.map(|x| x.status());
|
|
|
|
if res_status.map(|code| code.is_success()).unwrap_or_default() {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
pub fn absolute_path(&self) -> String {
|
|
self.path.to_absolute_path()
|
|
}
|
|
|
|
pub fn narinfo_path(&self) -> ObjectPath {
|
|
ObjectPath::parse(format!("{}.narinfo", nixbase32::encode(self.path.digest())))
|
|
.expect("must parse to a valid object_store path")
|
|
}
|
|
|
|
pub async fn check_if_already_exists(&self, s3: &AmazonS3) -> bool {
|
|
s3.head(&self.narinfo_path()).await.is_ok()
|
|
}
|
|
}
|