use hashbrown::HashMap;
use std::path::{Path, PathBuf};
use tokio::fs;
use vtcode_commons::VtCodePaths;
use vtcode_commons::fs::write_private_file_atomic;
use super::{PluginError, PluginResult};
pub struct PluginCache {
cache_dir: PathBuf,
cached_plugins: HashMap<String, PathBuf>,
}
impl PluginCache {
pub fn new(cache_dir: PathBuf) -> Self {
Self { cache_dir, cached_plugins: HashMap::new() }
}
pub async fn cache_plugin(&mut self, plugin_id: &str, source_path: &Path) -> PluginResult<PathBuf> {
super::validate_plugin_component(plugin_id)?;
let source_metadata = fs::symlink_metadata(source_path).await.map_err(|error| {
PluginError::LoadingError(format!("Failed to inspect source path {}: {error}", source_path.display()))
})?;
if source_metadata.file_type().is_symlink() || !source_metadata.is_dir() {
return Err(PluginError::LoadingError(format!(
"Plugin source is not a directory: {}",
source_path.display()
)));
}
VtCodePaths::ensure_user_dir(&self.cache_dir)
.map_err(|e| PluginError::LoadingError(format!("Failed to create cache directory: {e}")))?;
let cache_path = self.cache_dir.join(plugin_id);
if let Ok(metadata) = fs::symlink_metadata(&cache_path).await {
if metadata.file_type().is_symlink() || !metadata.is_dir() {
return Err(PluginError::LoadingError(format!(
"Refusing to replace non-directory plugin cache path: {}",
cache_path.display()
)));
}
fs::remove_dir_all(&cache_path)
.await
.map_err(|e| PluginError::LoadingError(format!("Failed to remove existing cache: {e}")))?;
}
self.copy_plugin_to_cache(source_path, &cache_path).await?;
self.cached_plugins.insert(plugin_id.to_string(), cache_path.clone());
Ok(cache_path)
}
async fn copy_plugin_to_cache(&self, source: &Path, destination: &Path) -> PluginResult<()> {
Box::pin(async {
VtCodePaths::ensure_user_dir(destination)
.map_err(|e| PluginError::LoadingError(format!("Failed to create destination directory: {e}")))?;
let mut entries = fs::read_dir(source)
.await
.map_err(|e| PluginError::LoadingError(format!("Failed to read source directory: {e}")))?;
while let Some(entry) = entries
.next_entry()
.await
.map_err(|e| PluginError::LoadingError(format!("Failed to read directory entry: {e}")))?
{
let src_path = entry.path();
let dst_path = destination.join(entry.file_name());
let source_metadata = fs::symlink_metadata(&src_path).await.map_err(|error| {
PluginError::LoadingError(format!("Failed to inspect plugin source entry: {error}"))
})?;
if source_metadata.file_type().is_symlink() {
return Err(PluginError::LoadingError(format!(
"Refusing to copy symlinked plugin entry: {}",
src_path.display()
)));
}
if source_metadata.is_dir() {
self.copy_plugin_to_cache(&src_path, &dst_path).await?;
} else if source_metadata.is_file() {
if let Ok(destination_metadata) = fs::symlink_metadata(&dst_path).await
&& destination_metadata.file_type().is_symlink()
{
return Err(PluginError::LoadingError(format!(
"Refusing to replace symlinked plugin cache entry: {}",
dst_path.display()
)));
}
let source = src_path.clone();
let contents = tokio::task::spawn_blocking(move || VtCodePaths::read_file_no_follow(&source))
.await
.map_err(|error| {
PluginError::LoadingError(format!("Plugin source read task panicked: {error}"))
})?
.map_err(|error| PluginError::LoadingError(format!("Failed to read plugin file: {error}")))?;
write_private_file_atomic(&dst_path, contents)
.await
.map_err(|e| PluginError::LoadingError(format!("Failed to copy file: {e}")))?;
} else {
return Err(PluginError::LoadingError(format!(
"Refusing to copy special plugin entry: {}",
src_path.display()
)));
}
}
Ok(())
})
.await
}
pub fn get_cached_plugin(&self, plugin_id: &str) -> Option<&PathBuf> {
self.cached_plugins.get(plugin_id)
}
}