use crate::clients::*;
use crate::helpers::{
determine_cache_extension, download_from_url_to_file, extract_file_name_from_url, hash_base64,
hash_sha256, move_or_unpack_download,
};
use crate::loader_error::WarpgateLoaderError;
use crate::protocols::{
DataLoader, FileLoader, GitHubLoader, HttpLoader, LoadFrom, LoaderProtocol, OciLoader,
};
use crate::registry::RegistryConfig;
use once_cell::sync::OnceCell;
use starbase_styles::color;
use starbase_utils::{fs, path};
use std::fmt::Debug;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use tracing::{instrument, trace, warn};
use warpgate_api::{Id, PluginLocator};
pub type OfflineChecker = Arc<fn() -> bool>;
#[derive(Clone)]
pub struct PluginLoader {
cache_duration: Duration,
data_loader: OnceCell<DataLoader>,
file_loader: OnceCell<FileLoader>,
github_loader: OnceCell<GitHubLoader>,
http_client: OnceCell<Arc<HttpClient>>,
http_loader: OnceCell<HttpLoader>,
http_options: HttpOptions,
offline_checker: Option<OfflineChecker>,
plugins_dir: PathBuf,
temp_dir: PathBuf,
registries: Vec<RegistryConfig>,
oci_client: OnceCell<Arc<OciClient>>,
oci_loader: OnceCell<OciLoader>,
}
impl PluginLoader {
pub fn new<P: AsRef<Path>, T: AsRef<Path>>(plugins_dir: P, temp_dir: T) -> Self {
let plugins_dir = plugins_dir.as_ref();
trace!(cache_dir = ?plugins_dir, "Creating plugin loader");
Self {
cache_duration: Duration::from_secs(86400 * 30), data_loader: OnceCell::new(),
file_loader: OnceCell::new(),
github_loader: OnceCell::new(),
http_client: OnceCell::new(),
http_loader: OnceCell::new(),
http_options: HttpOptions::default(),
oci_client: OnceCell::new(),
oci_loader: OnceCell::new(),
offline_checker: None,
plugins_dir: plugins_dir.to_owned(),
registries: vec![],
temp_dir: temp_dir.as_ref().to_owned(),
}
}
pub fn add_registry(&mut self, registry: RegistryConfig) {
self.registries.push(registry);
}
pub fn add_registries(&mut self, registries: Vec<RegistryConfig>) {
for registry in registries {
self.add_registry(registry);
}
}
pub fn get_data_loader(&self) -> Result<&DataLoader, WarpgateLoaderError> {
self.data_loader.get_or_try_init(|| Ok(DataLoader {}))
}
pub fn get_file_loader(&self) -> Result<&FileLoader, WarpgateLoaderError> {
self.file_loader.get_or_try_init(|| Ok(FileLoader {}))
}
pub fn get_github_loader(&self) -> Result<&GitHubLoader, WarpgateLoaderError> {
self.github_loader.get_or_try_init(|| {
Ok(GitHubLoader {
client: Arc::clone(self.get_http_client()?),
})
})
}
pub fn get_http_loader(&self) -> Result<&HttpLoader, WarpgateLoaderError> {
self.http_loader.get_or_try_init(|| Ok(HttpLoader {}))
}
pub fn get_oci_loader(&self) -> Result<&OciLoader, WarpgateLoaderError> {
self.oci_loader.get_or_try_init(|| {
Ok(OciLoader {
client: Arc::clone(self.get_oci_client()?),
})
})
}
pub fn get_http_client(&self) -> Result<&Arc<HttpClient>, WarpgateHttpClientError> {
self.http_client
.get_or_try_init(|| create_http_client_with_options(&self.http_options).map(Arc::new))
}
pub fn get_oci_client(&self) -> Result<&Arc<OciClient>, WarpgateHttpClientError> {
self.oci_client
.get_or_try_init(|| Ok(Arc::new(OciClient::default())))
}
#[instrument(skip(self))]
pub async fn load_plugin<I: AsRef<Id> + Debug, L: AsRef<PluginLocator> + Debug>(
&self,
id: I,
locator: L,
) -> Result<PathBuf, WarpgateLoaderError> {
let id = id.as_ref();
let locator = locator.as_ref();
trace!(
id = id.as_str(),
locator = locator.to_string(),
"Loading plugin {}",
color::id(id.as_str())
);
let (source, is_latest) = match locator {
PluginLocator::Data(data) => {
let loader = self.get_data_loader()?;
(loader.load(id, data, &()).await?, loader.is_latest(data))
}
PluginLocator::File(file) => {
let loader = self.get_file_loader()?;
(loader.load(id, file, &()).await?, loader.is_latest(file))
}
PluginLocator::GitHub(github) => {
let loader = self.get_github_loader()?;
(
loader.load(id, github, &()).await?,
loader.is_latest(github),
)
}
PluginLocator::Url(url) => {
let loader = self.get_http_loader()?;
(loader.load(id, url, &()).await?, loader.is_latest(url))
}
PluginLocator::Registry(registry) => {
let loader = self.get_oci_loader()?;
(
loader.load(id, registry, &self.registries).await?,
loader.is_latest(registry),
)
}
};
let cache_path = match source {
LoadFrom::Blob {
data, ext, hash, ..
} => {
let cache_path = self.create_cache_path(id, &hash, &ext, is_latest);
if !self.is_cached(id, &cache_path)? {
fs::write_file(&cache_path, data)?;
}
cache_path
}
LoadFrom::File(path) => path.to_path_buf(),
LoadFrom::Url(url) => {
let cache_path = self.create_cache_path(
id,
hash_sha256(&url).as_str(),
determine_cache_extension(&url).unwrap_or(".wasm"),
is_latest,
);
if !self.is_cached(id, &cache_path)? {
self.download_plugin(id, &url, &cache_path).await?;
}
cache_path
}
};
Ok(cache_path)
}
pub fn create_cache_path(&self, id: &Id, hash: &str, ext: &str, is_latest: bool) -> PathBuf {
self.plugins_dir.join(format!(
"{}-{}{}.{}",
path::encode_component(id.as_str()),
if is_latest { "latest-" } else { "" },
hash,
ext.trim_start_matches('.')
))
}
#[instrument(name = "is_plugin_cached", skip(self))]
pub fn is_cached(&self, id: &Id, path: &Path) -> Result<bool, WarpgateLoaderError> {
if !path.exists() {
trace!(id = id.as_str(), "Plugin not cached, acquiring");
return Ok(false);
}
if self.cache_duration.is_zero() {
trace!(
id = id.as_str(),
"Plugin caching has been disabled, acquiring"
);
return Ok(false);
}
let mut cached = !fs::is_stale(path, false, self.cache_duration)?;
if !cached && self.is_offline() {
cached = true;
}
if !cached && path.exists() {
fs::remove_file(path)?;
}
if cached {
trace!(id = id.as_str(), path = ?path, "Plugin already acquired and cached");
} else {
trace!(id = id.as_str(), path = ?path, "Plugin cached but stale, re-acquiring");
}
Ok(cached)
}
pub fn is_offline(&self) -> bool {
self.offline_checker
.as_ref()
.map(|op| op())
.unwrap_or_default()
}
pub fn set_cache_duration(&mut self, duration: Duration) {
self.cache_duration = duration;
}
pub fn set_http_client_options(&mut self, options: &HttpOptions) {
options.clone_into(&mut self.http_options);
}
pub fn set_offline_checker(&mut self, op: fn() -> bool) {
self.offline_checker = Some(Arc::new(op));
}
#[instrument(skip(self))]
async fn download_plugin(
&self,
id: &Id,
source_url: &str,
dest_file: &Path,
) -> Result<(), WarpgateLoaderError> {
if self.is_offline() {
return Err(WarpgateLoaderError::RequiredInternetConnection {
message: "Unable to download plugin.".into(),
url: source_url.to_owned(),
});
}
trace!(
id = id.as_str(),
from = source_url,
to = ?dest_file,
"Downloading plugin from URL"
);
let temp_hash = hash_base64(dest_file.to_str().unwrap_or(source_url));
let temp_file = self.temp_dir.join(format!(
"{}-{}",
&temp_hash[0..temp_hash.len().min(32)],
extract_file_name_from_url(source_url)
));
download_from_url_to_file(source_url, &temp_file, self.get_http_client()?).await?;
move_or_unpack_download(&temp_file, dest_file)?;
Ok(())
}
}