Skip to main content

voirs_cli/plugins/
registry.rs

1//! Plugin registry for managing installed plugins.
2
3use super::{Plugin, PluginError, PluginManager, PluginManifest, PluginResult, PluginType};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8use tokio::sync::RwLock;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct RegistryEntry {
12    pub manifest: PluginManifest,
13    pub install_path: PathBuf,
14    pub install_date: chrono::DateTime<chrono::Utc>,
15    pub last_updated: chrono::DateTime<chrono::Utc>,
16    pub enabled: bool,
17    pub auto_update: bool,
18    pub usage_count: u64,
19    pub last_used: Option<chrono::DateTime<chrono::Utc>>,
20    pub checksum: String,
21    pub metadata: HashMap<String, serde_json::Value>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct RegistryConfig {
26    pub registry_path: PathBuf,
27    pub auto_discovery: bool,
28    pub auto_update_check: bool,
29    pub update_interval_hours: u64,
30    pub cleanup_unused_after_days: u64,
31    pub max_registry_entries: usize,
32}
33
34impl Default for RegistryConfig {
35    fn default() -> Self {
36        Self {
37            registry_path: dirs::config_dir()
38                .unwrap_or_default()
39                .join("voirs")
40                .join("plugin_registry.json"),
41            auto_discovery: true,
42            auto_update_check: false,
43            update_interval_hours: 24,
44            cleanup_unused_after_days: 90,
45            max_registry_entries: 1000,
46        }
47    }
48}
49
50pub struct PluginRegistry {
51    config: RegistryConfig,
52    entries: RwLock<HashMap<String, RegistryEntry>>,
53    manager: Arc<PluginManager>,
54}
55
56impl PluginRegistry {
57    pub fn new(config: RegistryConfig, manager: Arc<PluginManager>) -> Self {
58        Self {
59            config,
60            entries: RwLock::new(HashMap::new()),
61            manager,
62        }
63    }
64
65    pub async fn load(&self) -> PluginResult<()> {
66        if !self.config.registry_path.exists() {
67            // Create empty registry if it doesn't exist
68            self.save().await?;
69            return Ok(());
70        }
71
72        let content = tokio::fs::read_to_string(&self.config.registry_path).await?;
73        let entries: HashMap<String, RegistryEntry> = serde_json::from_str(&content)?;
74
75        {
76            let mut entries_guard = self.entries.write().await;
77            *entries_guard = entries;
78        }
79
80        Ok(())
81    }
82
83    pub async fn save(&self) -> PluginResult<()> {
84        let entries = self.entries.read().await;
85        let content = serde_json::to_string_pretty(&*entries)?;
86
87        // Ensure parent directory exists
88        if let Some(parent) = self.config.registry_path.parent() {
89            tokio::fs::create_dir_all(parent).await?;
90        }
91
92        tokio::fs::write(&self.config.registry_path, content).await?;
93        Ok(())
94    }
95
96    pub async fn register_plugin(
97        &self,
98        manifest: PluginManifest,
99        install_path: PathBuf,
100    ) -> PluginResult<()> {
101        let now = chrono::Utc::now();
102        let checksum = self.calculate_checksum(&install_path).await?;
103
104        let entry = RegistryEntry {
105            manifest: manifest.clone(),
106            install_path,
107            install_date: now,
108            last_updated: now,
109            enabled: true,
110            auto_update: false,
111            usage_count: 0,
112            last_used: None,
113            checksum,
114            metadata: HashMap::new(),
115        };
116
117        {
118            let mut entries_guard = self.entries.write().await;
119
120            // Check registry size limit
121            if entries_guard.len() >= self.config.max_registry_entries {
122                return Err(PluginError::LoadingFailed(
123                    "Registry is full. Please clean up unused plugins.".to_string(),
124                ));
125            }
126
127            entries_guard.insert(manifest.name.clone(), entry);
128        }
129
130        self.save().await?;
131        Ok(())
132    }
133
134    pub async fn unregister_plugin(&self, name: &str) -> PluginResult<()> {
135        {
136            let mut entries_guard = self.entries.write().await;
137            if entries_guard.remove(name).is_none() {
138                return Err(PluginError::NotFound(name.to_string()));
139            }
140        }
141
142        self.save().await?;
143        Ok(())
144    }
145
146    pub async fn get_plugin(&self, name: &str) -> Option<RegistryEntry> {
147        let entries = self.entries.read().await;
148        entries.get(name).cloned()
149    }
150
151    pub async fn list_plugins(&self) -> Vec<RegistryEntry> {
152        let entries = self.entries.read().await;
153        entries.values().cloned().collect()
154    }
155
156    pub async fn list_plugins_by_type(&self, plugin_type: PluginType) -> Vec<RegistryEntry> {
157        let entries = self.entries.read().await;
158        entries
159            .values()
160            .filter(|entry| entry.manifest.plugin_type == plugin_type)
161            .cloned()
162            .collect()
163    }
164
165    pub async fn enable_plugin(&self, name: &str) -> PluginResult<()> {
166        {
167            let mut entries_guard = self.entries.write().await;
168            if let Some(entry) = entries_guard.get_mut(name) {
169                entry.enabled = true;
170                entry.last_updated = chrono::Utc::now();
171            } else {
172                return Err(PluginError::NotFound(name.to_string()));
173            }
174        }
175
176        self.save().await?;
177        Ok(())
178    }
179
180    pub async fn disable_plugin(&self, name: &str) -> PluginResult<()> {
181        {
182            let mut entries_guard = self.entries.write().await;
183            if let Some(entry) = entries_guard.get_mut(name) {
184                entry.enabled = false;
185                entry.last_updated = chrono::Utc::now();
186            } else {
187                return Err(PluginError::NotFound(name.to_string()));
188            }
189        }
190
191        self.save().await?;
192        Ok(())
193    }
194
195    pub async fn record_usage(&self, name: &str) -> PluginResult<()> {
196        {
197            let mut entries_guard = self.entries.write().await;
198            if let Some(entry) = entries_guard.get_mut(name) {
199                entry.usage_count += 1;
200                entry.last_used = Some(chrono::Utc::now());
201            } else {
202                return Err(PluginError::NotFound(name.to_string()));
203            }
204        }
205
206        // Save periodically to avoid too frequent I/O
207        if fastrand::f32() < 0.1 {
208            self.save().await?;
209        }
210
211        Ok(())
212    }
213
214    pub async fn update_plugin_metadata(
215        &self,
216        name: &str,
217        key: &str,
218        value: serde_json::Value,
219    ) -> PluginResult<()> {
220        {
221            let mut entries_guard = self.entries.write().await;
222            if let Some(entry) = entries_guard.get_mut(name) {
223                entry.metadata.insert(key.to_string(), value);
224                entry.last_updated = chrono::Utc::now();
225            } else {
226                return Err(PluginError::NotFound(name.to_string()));
227            }
228        }
229
230        self.save().await?;
231        Ok(())
232    }
233
234    pub async fn search_plugins(&self, query: &str) -> Vec<RegistryEntry> {
235        let entries = self.entries.read().await;
236        let query_lower = query.to_lowercase();
237
238        entries
239            .values()
240            .filter(|entry| {
241                entry.manifest.name.to_lowercase().contains(&query_lower)
242                    || entry
243                        .manifest
244                        .description
245                        .to_lowercase()
246                        .contains(&query_lower)
247                    || entry.manifest.author.to_lowercase().contains(&query_lower)
248            })
249            .cloned()
250            .collect()
251    }
252
253    pub async fn get_enabled_plugins(&self) -> Vec<RegistryEntry> {
254        let entries = self.entries.read().await;
255        entries
256            .values()
257            .filter(|entry| entry.enabled)
258            .cloned()
259            .collect()
260    }
261
262    pub async fn get_stats(&self) -> RegistryStats {
263        let entries = self.entries.read().await;
264        let total = entries.len();
265        let enabled = entries.values().filter(|e| e.enabled).count();
266        let by_type = entries
267            .values()
268            .map(|e| e.manifest.plugin_type.clone())
269            .fold(HashMap::new(), |mut acc, t| {
270                *acc.entry(format!("{:?}", t)).or_insert(0) += 1;
271                acc
272            });
273
274        RegistryStats {
275            total_plugins: total,
276            enabled_plugins: enabled,
277            disabled_plugins: total - enabled,
278            plugins_by_type: by_type,
279            total_usage: entries.values().map(|e| e.usage_count).sum(),
280        }
281    }
282
283    pub async fn cleanup_unused(&self) -> PluginResult<Vec<String>> {
284        let cutoff_date = chrono::Utc::now()
285            - chrono::Duration::days(self.config.cleanup_unused_after_days as i64);
286        let mut removed = Vec::new();
287
288        {
289            let mut entries_guard = self.entries.write().await;
290            entries_guard.retain(|name, entry| {
291                let should_keep = entry.enabled
292                    || entry
293                        .last_used
294                        .is_some_and(|last_used| last_used > cutoff_date)
295                    || entry.usage_count > 0;
296
297                if !should_keep {
298                    removed.push(name.clone());
299                }
300
301                should_keep
302            });
303        }
304
305        if !removed.is_empty() {
306            self.save().await?;
307        }
308
309        Ok(removed)
310    }
311
312    pub async fn validate_integrity(&self) -> PluginResult<Vec<String>> {
313        let mut issues = Vec::new();
314        let entries = self.entries.read().await;
315
316        for (name, entry) in entries.iter() {
317            // Check if plugin files still exist
318            if !entry.install_path.exists() {
319                issues.push(format!(
320                    "Plugin '{}' file not found at {}",
321                    name,
322                    entry.install_path.display()
323                ));
324                continue;
325            }
326
327            // Check checksum
328            match self.calculate_checksum(&entry.install_path).await {
329                Ok(current_checksum) => {
330                    if current_checksum != entry.checksum {
331                        issues.push(format!(
332                            "Plugin '{}' checksum mismatch (possible corruption)",
333                            name
334                        ));
335                    }
336                }
337                Err(e) => {
338                    issues.push(format!(
339                        "Plugin '{}' checksum calculation failed: {}",
340                        name, e
341                    ));
342                }
343            }
344
345            // Check manifest validity
346            if entry.manifest.name.is_empty() || entry.manifest.version.is_empty() {
347                issues.push(format!("Plugin '{}' has invalid manifest", name));
348            }
349        }
350
351        Ok(issues)
352    }
353
354    async fn calculate_checksum(&self, path: &Path) -> PluginResult<String> {
355        use sha2::{Digest, Sha256};
356
357        let content = tokio::fs::read(path).await?;
358        let mut hasher = Sha256::new();
359        hasher.update(&content);
360        let result = hasher.finalize();
361        Ok(format!("{:x}", result))
362    }
363
364    pub async fn discover_and_register(&self) -> PluginResult<usize> {
365        if !self.config.auto_discovery {
366            return Ok(0);
367        }
368
369        let discovered = self.manager.discover_plugins().await?;
370        let mut registered_count = 0;
371
372        for plugin_info in discovered {
373            if !self
374                .entries
375                .read()
376                .await
377                .contains_key(&plugin_info.manifest.name)
378            {
379                self.register_plugin(plugin_info.manifest, plugin_info.path)
380                    .await?;
381                registered_count += 1;
382            }
383        }
384
385        Ok(registered_count)
386    }
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct RegistryStats {
391    pub total_plugins: usize,
392    pub enabled_plugins: usize,
393    pub disabled_plugins: usize,
394    pub plugins_by_type: HashMap<String, usize>,
395    pub total_usage: u64,
396}
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401    use std::sync::Arc;
402
403    fn create_test_manifest() -> PluginManifest {
404        PluginManifest {
405            name: "test-plugin".to_string(),
406            version: "1.0.0".to_string(),
407            description: "Test plugin".to_string(),
408            author: "Test Author".to_string(),
409            api_version: "1.0.0".to_string(),
410            plugin_type: PluginType::Extension,
411            entry_point: "test_plugin.dll".to_string(),
412            dependencies: vec![],
413            permissions: vec![],
414            configuration: None,
415        }
416    }
417
418    #[tokio::test]
419    async fn test_registry_creation() {
420        let config = RegistryConfig::default();
421        let manager = Arc::new(PluginManager::new());
422        let registry = PluginRegistry::new(config, manager);
423
424        let stats = registry.get_stats().await;
425        assert_eq!(stats.total_plugins, 0);
426    }
427
428    #[tokio::test]
429    async fn test_plugin_registration() {
430        let config = RegistryConfig {
431            registry_path: PathBuf::from("/tmp/test_registry.json"),
432            ..Default::default()
433        };
434        let manager = Arc::new(PluginManager::new());
435        let registry = PluginRegistry::new(config, manager);
436
437        let manifest = create_test_manifest();
438        let install_path = PathBuf::from("/tmp/test_plugin");
439
440        // This will fail because the path doesn't exist, but that's expected in a test
441        let result = registry.register_plugin(manifest, install_path).await;
442        assert!(result.is_err()); // Checksum calculation will fail
443
444        let stats = registry.get_stats().await;
445        assert_eq!(stats.total_plugins, 0); // Registration failed
446    }
447
448    #[tokio::test]
449    async fn test_plugin_search() {
450        let config = RegistryConfig::default();
451        let manager = Arc::new(PluginManager::new());
452        let registry = PluginRegistry::new(config, manager);
453
454        let results = registry.search_plugins("test").await;
455        assert_eq!(results.len(), 0);
456    }
457
458    #[tokio::test]
459    async fn test_registry_stats() {
460        let config = RegistryConfig::default();
461        let manager = Arc::new(PluginManager::new());
462        let registry = PluginRegistry::new(config, manager);
463
464        let stats = registry.get_stats().await;
465        assert_eq!(stats.total_plugins, 0);
466        assert_eq!(stats.enabled_plugins, 0);
467        assert_eq!(stats.disabled_plugins, 0);
468        assert_eq!(stats.total_usage, 0);
469    }
470}