vtcode_core/plugins/
caching.rs1use hashbrown::HashMap;
7use std::path::{Path, PathBuf};
8
9use tokio::fs;
10use vtcode_commons::VtCodePaths;
11use vtcode_commons::fs::write_private_file_atomic;
12
13use super::{PluginError, PluginResult};
14
15pub struct PluginCache {
17 cache_dir: PathBuf,
19 cached_plugins: HashMap<String, PathBuf>,
21}
22
23impl PluginCache {
24 pub fn new(cache_dir: PathBuf) -> Self {
26 Self { cache_dir, cached_plugins: HashMap::new() }
27 }
28
29 pub async fn cache_plugin(&mut self, plugin_id: &str, source_path: &Path) -> PluginResult<PathBuf> {
31 super::validate_plugin_component(plugin_id)?;
32
33 let source_metadata = fs::symlink_metadata(source_path).await.map_err(|error| {
35 PluginError::LoadingError(format!("Failed to inspect source path {}: {error}", source_path.display()))
36 })?;
37 if source_metadata.file_type().is_symlink() || !source_metadata.is_dir() {
38 return Err(PluginError::LoadingError(format!(
39 "Plugin source is not a directory: {}",
40 source_path.display()
41 )));
42 }
43
44 VtCodePaths::ensure_user_dir(&self.cache_dir)
46 .map_err(|e| PluginError::LoadingError(format!("Failed to create cache directory: {e}")))?;
47
48 let cache_path = self.cache_dir.join(plugin_id);
50
51 if let Ok(metadata) = fs::symlink_metadata(&cache_path).await {
53 if metadata.file_type().is_symlink() || !metadata.is_dir() {
54 return Err(PluginError::LoadingError(format!(
55 "Refusing to replace non-directory plugin cache path: {}",
56 cache_path.display()
57 )));
58 }
59 fs::remove_dir_all(&cache_path)
60 .await
61 .map_err(|e| PluginError::LoadingError(format!("Failed to remove existing cache: {e}")))?;
62 }
63
64 self.copy_plugin_to_cache(source_path, &cache_path).await?;
66
67 self.cached_plugins.insert(plugin_id.to_string(), cache_path.clone());
69
70 Ok(cache_path)
71 }
72
73 async fn copy_plugin_to_cache(&self, source: &Path, destination: &Path) -> PluginResult<()> {
75 Box::pin(async {
76 VtCodePaths::ensure_user_dir(destination)
77 .map_err(|e| PluginError::LoadingError(format!("Failed to create destination directory: {e}")))?;
78
79 let mut entries = fs::read_dir(source)
80 .await
81 .map_err(|e| PluginError::LoadingError(format!("Failed to read source directory: {e}")))?;
82
83 while let Some(entry) = entries
84 .next_entry()
85 .await
86 .map_err(|e| PluginError::LoadingError(format!("Failed to read directory entry: {e}")))?
87 {
88 let src_path = entry.path();
89 let dst_path = destination.join(entry.file_name());
90
91 let source_metadata = fs::symlink_metadata(&src_path).await.map_err(|error| {
92 PluginError::LoadingError(format!("Failed to inspect plugin source entry: {error}"))
93 })?;
94 if source_metadata.file_type().is_symlink() {
95 return Err(PluginError::LoadingError(format!(
96 "Refusing to copy symlinked plugin entry: {}",
97 src_path.display()
98 )));
99 }
100 if source_metadata.is_dir() {
101 self.copy_plugin_to_cache(&src_path, &dst_path).await?;
102 } else if source_metadata.is_file() {
103 if let Ok(destination_metadata) = fs::symlink_metadata(&dst_path).await
104 && destination_metadata.file_type().is_symlink()
105 {
106 return Err(PluginError::LoadingError(format!(
107 "Refusing to replace symlinked plugin cache entry: {}",
108 dst_path.display()
109 )));
110 }
111 let source = src_path.clone();
112 let contents = tokio::task::spawn_blocking(move || VtCodePaths::read_file_no_follow(&source))
113 .await
114 .map_err(|error| {
115 PluginError::LoadingError(format!("Plugin source read task panicked: {error}"))
116 })?
117 .map_err(|error| PluginError::LoadingError(format!("Failed to read plugin file: {error}")))?;
118 write_private_file_atomic(&dst_path, contents)
119 .await
120 .map_err(|e| PluginError::LoadingError(format!("Failed to copy file: {e}")))?;
121 } else {
122 return Err(PluginError::LoadingError(format!(
123 "Refusing to copy special plugin entry: {}",
124 src_path.display()
125 )));
126 }
127 }
128
129 Ok(())
130 })
131 .await
132 }
133
134 pub fn get_cached_plugin(&self, plugin_id: &str) -> Option<&PathBuf> {
136 self.cached_plugins.get(plugin_id)
137 }
138}