use std::path::PathBuf;
use parking_lot::RwLock;
use dprint_core::plugins::PluginInfo;
use dprint_core::types::ErrBox;
use dprint_cli_core::checksums::verify_sha256_checksum;
use crate::environment::Environment;
use crate::plugins::{PluginSourceReference};
use crate::utils::{PathSource, get_bytes_hash};
use super::implementations::{setup_plugin, cleanup_plugin, get_file_path_from_plugin_info};
use super::{PluginCacheManifest, PluginCacheManifestItem, read_manifest, write_manifest};
pub struct PluginCacheItem {
pub file_path: PathBuf,
pub info: PluginInfo,
}
pub struct PluginCache<TEnvironment : Environment> {
environment: TEnvironment,
manifest: RwLock<PluginCacheManifest>,
}
impl<TEnvironment> PluginCache<TEnvironment> where TEnvironment : Environment {
pub fn new(environment: TEnvironment) -> Self {
let manifest = RwLock::new(read_manifest(&environment));
PluginCache {
environment,
manifest,
}
}
pub fn forget(&self, source_reference: &PluginSourceReference) -> Result<(), ErrBox> {
let cache_key = self.get_cache_key(&source_reference.path_source)?;
let mut manifest = self.manifest.write();
let cache_item = manifest.remove_item(&cache_key);
write_manifest(&manifest, &self.environment)?;
if let Some(cache_item) = cache_item {
match cleanup_plugin(&source_reference.path_source, &cache_item.info, &self.environment) {
Err(err) => self.environment.log_error(&format!("Error forgetting plugin: {}", err.to_string())),
_ => {},
}
}
Ok(())
}
pub fn get_plugin_cache_item(&self, source_reference: &PluginSourceReference) -> Result<PluginCacheItem, ErrBox> {
match &source_reference.path_source {
PathSource::Remote(_) => {
self.get_plugin(
source_reference.clone(),
false,
download_url,
)
},
PathSource::Local(_) => {
self.get_plugin(
source_reference.clone(),
true,
get_file_bytes,
)
},
}
}
fn get_plugin(
&self,
source_reference: PluginSourceReference,
check_file_hash: bool,
read_bytes: impl Fn(PathSource, TEnvironment) -> Result<Vec<u8>, ErrBox>,
) -> Result<PluginCacheItem, ErrBox> {
let cache_key = self.get_cache_key(&source_reference.path_source)?;
let cache_item = self.manifest.read().get_item(&cache_key).map(|x| x.to_owned()); if let Some(cache_item) = cache_item {
let file_path = get_file_path_from_plugin_info(
&source_reference.path_source,
&cache_item.info,
&self.environment,
)?;
if check_file_hash {
let file_bytes = read_bytes(source_reference.path_source.clone(), self.environment.clone())?;
let file_hash = get_bytes_hash(&file_bytes);
let cache_file_hash = match &cache_item.file_hash {
Some(file_hash) => *file_hash,
None => return err!("Expected to have the plugin file hash stored in the cache."),
};
if file_hash == cache_file_hash {
return Ok(PluginCacheItem {
file_path,
info: cache_item.info,
});
} else {
self.forget(&source_reference)?;
}
} else {
return Ok(PluginCacheItem {
file_path,
info: cache_item.info,
});
}
}
let file_bytes = read_bytes(source_reference.path_source.clone(), self.environment.clone())?;
if let Some(checksum) = &source_reference.checksum {
verify_sha256_checksum(&file_bytes, checksum)?;
}
let setup_result = setup_plugin(&source_reference.path_source, &file_bytes, &self.environment)?;
let cache_item = PluginCacheManifestItem {
info: setup_result.plugin_info.clone(),
file_hash: if check_file_hash {
Some(get_bytes_hash(&file_bytes))
} else {
None
},
created_time: self.environment.get_time_secs(),
};
let mut manifest = self.manifest.write();
manifest.add_item(cache_key, cache_item);
write_manifest(&manifest, &self.environment)?;
Ok(PluginCacheItem {
file_path: setup_result.file_path,
info: setup_result.plugin_info,
})
}
fn get_cache_key(&self, path_source: &PathSource) -> Result<String, ErrBox> {
Ok(match path_source {
PathSource::Remote(remote_source) => format!("remote:{}", remote_source.url.as_str()),
PathSource::Local(local_source) => {
let absolute_path = self.environment.canonicalize(&local_source.path)?;
format!("local:{}", absolute_path.to_string_lossy())
}
})
}
}
fn download_url<TEnvironment: Environment>(path_source: PathSource, environment: TEnvironment) -> Result<Vec<u8>, ErrBox> {
environment.download_file(path_source.unwrap_remote().url.as_str())
}
fn get_file_bytes<TEnvironment: Environment>(path_source: PathSource, environment: TEnvironment) -> Result<Vec<u8>, ErrBox> {
environment.read_file_bytes(&path_source.unwrap_local().path)
}
#[cfg(test)]
mod test {
use std::path::PathBuf;
use pretty_assertions::assert_eq;
use dprint_core::plugins::PluginInfo;
use crate::environment::TestEnvironment;
use crate::plugins::{CompilationResult, PluginSourceReference};
use dprint_core::types::ErrBox;
use super::*;
#[test]
fn it_should_download_remote_file() -> Result<(), ErrBox> {
let environment = TestEnvironment::new();
environment.add_remote_file("https://plugins.dprint.dev/test.wasm", "t".as_bytes());
environment.set_wasm_compile_result(create_compilation_result("t".as_bytes()));
let plugin_cache = PluginCache::new(environment.clone());
let plugin_source = PluginSourceReference::new_remote_from_str("https://plugins.dprint.dev/test.wasm");
let file_path = plugin_cache.get_plugin_cache_item(&plugin_source)?.file_path;
let expected_file_path = PathBuf::from("/cache").join("plugins").join("test-plugin").join("test-plugin-0.1.0.cached");
assert_eq!(file_path, expected_file_path);
assert_eq!(environment.take_logged_errors(), vec!["Compiling https://plugins.dprint.dev/test.wasm"]);
let file_path = plugin_cache.get_plugin_cache_item(&plugin_source)?.file_path;
assert_eq!(file_path, expected_file_path);
assert_eq!(
environment.read_file(&environment.get_cache_dir().join("plugin-cache-manifest.json")).unwrap(),
r#"{"schemaVersion":2,"plugins":{"remote:https://plugins.dprint.dev/test.wasm":{"createdTime":123456,"info":{"name":"test-plugin","version":"0.1.0","configKey":"test-plugin","fileExtensions":["txt","dat"],"helpUrl":"test-url","configSchemaUrl":"schema-url"}}}}"#,
);
plugin_cache.forget(&plugin_source).unwrap();
assert_eq!(environment.path_exists(&file_path), false);
assert_eq!(
environment.read_file(&environment.get_cache_dir().join("plugin-cache-manifest.json")).unwrap(),
r#"{"schemaVersion":2,"plugins":{}}"#,
);
Ok(())
}
#[test]
fn it_should_cache_local_file() -> Result<(), ErrBox> {
let environment = TestEnvironment::new();
let original_file_path = PathBuf::from("/test.wasm");
let file_bytes = "t".as_bytes();
environment.write_file_bytes(&original_file_path, file_bytes).unwrap();
environment.set_wasm_compile_result(create_compilation_result("t".as_bytes()));
let plugin_cache = PluginCache::new(environment.clone());
let plugin_source = PluginSourceReference::new_local(original_file_path.clone());
let file_path = plugin_cache.get_plugin_cache_item(&plugin_source)?.file_path;
let expected_file_path = PathBuf::from("/cache").join("plugins").join("test-plugin").join("test-plugin-0.1.0.cached");
assert_eq!(file_path, expected_file_path);
assert_eq!(environment.take_logged_errors(), vec!["Compiling /test.wasm"]);
let file_path = plugin_cache.get_plugin_cache_item(&plugin_source)?.file_path;
assert_eq!(file_path, expected_file_path);
assert_eq!(
environment.read_file(&environment.get_cache_dir().join("plugin-cache-manifest.json")).unwrap(),
concat!(
r#"{"schemaVersion":2,"plugins":{"local:/test.wasm":{"createdTime":123456,"fileHash":10632242795325663332,"info":{"#,
r#""name":"test-plugin","version":"0.1.0","configKey":"test-plugin","#,
r#""fileExtensions":["txt","dat"],"helpUrl":"test-url","configSchemaUrl":"schema-url"}}}}"#,
)
);
assert_eq!(environment.take_logged_errors().len(), 0);
let file_bytes = "u".as_bytes();
environment.write_file_bytes(&original_file_path, file_bytes).unwrap();
let file_path = plugin_cache.get_plugin_cache_item(&PluginSourceReference::new_local(original_file_path.clone()))?.file_path;
assert_eq!(file_path, expected_file_path);
assert_eq!(
environment.read_file(&environment.get_cache_dir().join("plugin-cache-manifest.json")).unwrap(),
concat!(
r#"{"schemaVersion":2,"plugins":{"local:/test.wasm":{"createdTime":123456,"fileHash":6989588595861227504,"info":{"#,
r#""name":"test-plugin","version":"0.1.0","configKey":"test-plugin","#,
r#""fileExtensions":["txt","dat"],"helpUrl":"test-url","configSchemaUrl":"schema-url"}}}}"#,
)
);
assert_eq!(environment.take_logged_errors(), vec!["Compiling /test.wasm"]);
plugin_cache.forget(&plugin_source).unwrap();
assert_eq!(environment.path_exists(&file_path), false);
assert_eq!(
environment.read_file(&environment.get_cache_dir().join("plugin-cache-manifest.json")).unwrap(),
r#"{"schemaVersion":2,"plugins":{}}"#,
);
Ok(())
}
fn create_compilation_result(bytes: &[u8]) -> CompilationResult {
CompilationResult {
bytes: bytes.to_vec(),
plugin_info: get_test_plugin_info(),
}
}
fn get_test_plugin_info() -> PluginInfo {
PluginInfo {
name: String::from("test-plugin"),
version: String::from("0.1.0"),
config_key: String::from("test-plugin"),
file_extensions: vec![String::from("txt"), String::from("dat")],
help_url: String::from("test-url"),
config_schema_url: String::from("schema-url"),
}
}
}