use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{Result, anyhow, bail};
use tempfile::TempPath;
mod azblob;
mod fs;
mod gcs;
mod http;
mod oci;
mod s3;
pub use azblob::AzBlobResolver;
pub use fs::FsResolver;
pub use gcs::GcsResolver;
pub use http::HttpResolver;
pub use oci::OciResolver;
pub use s3::S3Resolver;
pub struct FetchResponse {
location: FetchLocation,
}
impl FetchResponse {
pub fn from_path(path: PathBuf) -> Self {
Self {
location: FetchLocation::Permanent(path),
}
}
pub fn from_temp(path: TempPath) -> Self {
Self {
location: FetchLocation::Temporary(path),
}
}
pub fn path(&self) -> &Path {
match &self.location {
FetchLocation::Permanent(path) => path,
FetchLocation::Temporary(path) => path.as_ref(),
}
}
}
enum FetchLocation {
Permanent(PathBuf),
Temporary(TempPath),
}
pub trait PackResolver: Send + Sync {
fn scheme(&self) -> &'static str;
fn fetch(&self, locator: &str) -> Result<FetchResponse>;
}
#[derive(Default)]
pub struct ResolverRegistry {
resolvers: HashMap<String, Arc<dyn PackResolver>>,
}
impl ResolverRegistry {
pub fn register(&mut self, resolver: impl PackResolver + 'static) {
self.resolvers
.insert(resolver.scheme().to_string(), Arc::new(resolver));
}
pub fn register_builtin(
&mut self,
fs_root: PathBuf,
network: Option<&greentic_config_types::NetworkConfig>,
) -> Result<()> {
self.register(FsResolver::new(fs_root));
self.register(HttpResolver::new("http", network)?);
self.register(HttpResolver::new("https", network)?);
self.register(OciResolver::new(network)?);
self.register(S3Resolver::new(network)?);
self.register(GcsResolver::new(network)?);
self.register(AzBlobResolver::new(network)?);
Ok(())
}
pub fn fetch(&self, reference: &str) -> Result<FetchResponse> {
let parsed = ParsedReference::parse(reference)?;
let resolver = self
.resolvers
.get(&parsed.scheme)
.ok_or_else(|| anyhow!("no resolver registered for scheme `{}`", parsed.scheme))?;
resolver.fetch(&parsed.locator)
}
}
struct ParsedReference {
scheme: String,
locator: String,
}
impl ParsedReference {
fn parse(input: &str) -> Result<Self> {
let (scheme_part, rest) = input
.split_once("://")
.ok_or_else(|| anyhow!("reference `{input}` is missing a URI scheme"))?;
if rest.is_empty() {
bail!("reference `{input}` is missing a locator");
}
if let Some((logical, actual)) = scheme_part.split_once('+') {
let locator = format!("{actual}://{rest}");
return Ok(Self {
scheme: logical.to_ascii_lowercase(),
locator,
});
}
Ok(Self {
scheme: scheme_part.to_ascii_lowercase(),
locator: input.to_string(),
})
}
}