examples: add example plugins
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I9eac30c7d4c1c89178f4930b215e523d6a6a6964
This commit is contained in:
parent
bee18284ee
commit
708f8a0b67
5 changed files with 945 additions and 0 deletions
518
examples/plugins/README.md
Normal file
518
examples/plugins/README.md
Normal file
|
|
@ -0,0 +1,518 @@
|
|||
# Pinakes Plugin Examples
|
||||
|
||||
This directory contains example plugins demonstrating the Pinakes plugin system.
|
||||
|
||||
## Overview
|
||||
|
||||
Pinakes supports extensibility through a WASM-based plugin system. Plugins can
|
||||
extend Pinakes functionality by:
|
||||
|
||||
- **Media Type Providers**: Add support for new file formats
|
||||
- **Metadata Extractors**: Extract metadata from files
|
||||
- **Thumbnail Generators**: Generate thumbnails for media types
|
||||
- **Search Backends**: Implement custom search algorithms
|
||||
- **Event Handlers**: React to system events
|
||||
- **Theme Providers**: Provide custom UI themes
|
||||
|
||||
## Example Plugins
|
||||
|
||||
### 1. Markdown Metadata Extractor
|
||||
|
||||
**Directory**: `markdown-metadata/`
|
||||
|
||||
Enhances markdown file support with advanced frontmatter parsing.
|
||||
|
||||
**Demonstrates**:
|
||||
|
||||
- Metadata extraction from files
|
||||
- Plugin configuration via `plugin.toml`
|
||||
- Minimal capability requirements
|
||||
|
||||
**Plugin Kind**: `metadata_extractor`
|
||||
|
||||
### 2. HEIF/HEIC Support
|
||||
|
||||
**Directory**: `heif-support/`
|
||||
|
||||
Adds support for HEIF and HEIC image formats.
|
||||
|
||||
**Demonstrates**:
|
||||
|
||||
- Media type registration
|
||||
- Metadata extraction from binary formats
|
||||
- Thumbnail generation
|
||||
- Resource limits (memory, CPU time)
|
||||
|
||||
**Plugin Kinds**: `media_type`, `metadata_extractor`, `thumbnail_generator`
|
||||
|
||||
## Plugin Architecture
|
||||
|
||||
### Plugin Structure
|
||||
|
||||
```
|
||||
my-plugin/
|
||||
├── plugin.toml # Plugin manifest
|
||||
├── Cargo.toml # Rust project configuration
|
||||
├── src/
|
||||
│ └── lib.rs # Plugin implementation
|
||||
└── README.md # Plugin documentation
|
||||
```
|
||||
|
||||
### Plugin Manifest (plugin.toml)
|
||||
|
||||
```toml
|
||||
[plugin]
|
||||
name = "my-plugin"
|
||||
version = "1.0.0"
|
||||
api_version = "1.0"
|
||||
author = "Your Name"
|
||||
description = "Description of your plugin"
|
||||
kind = ["metadata_extractor"]
|
||||
|
||||
[plugin.binary]
|
||||
wasm = "my_plugin.wasm"
|
||||
|
||||
[capabilities]
|
||||
network = false
|
||||
|
||||
[capabilities.filesystem]
|
||||
read = ["/path/to/read"]
|
||||
write = ["/path/to/write"]
|
||||
|
||||
[config]
|
||||
# Plugin-specific configuration
|
||||
option1 = "value1"
|
||||
option2 = 42
|
||||
```
|
||||
|
||||
### Manifest Fields
|
||||
|
||||
#### [plugin] Section
|
||||
|
||||
- `name`: Plugin identifier (must be unique)
|
||||
- `version`: Semantic version (e.g., "1.0.0")
|
||||
- `api_version`: Pinakes Plugin API version (currently "1.0")
|
||||
- `author`: Plugin author (optional)
|
||||
- `description`: Short description (optional)
|
||||
- `homepage`: Plugin homepage URL (optional)
|
||||
- `license`: License identifier (optional)
|
||||
- `kind`: Array of plugin kinds
|
||||
- `dependencies`: Array of plugin names this plugin depends on (optional)
|
||||
|
||||
#### [plugin.binary] Section
|
||||
|
||||
- `wasm`: Path to WASM binary (relative to manifest)
|
||||
- `entrypoint`: Custom entrypoint function name (optional, default: "_start")
|
||||
|
||||
#### [capabilities] Section
|
||||
|
||||
Capabilities define what the plugin can access:
|
||||
|
||||
**Filesystem**:
|
||||
|
||||
```toml
|
||||
[capabilities.filesystem]
|
||||
read = ["/tmp/cache", "/var/data"]
|
||||
write = ["/tmp/output"]
|
||||
```
|
||||
|
||||
**Network**:
|
||||
|
||||
```toml
|
||||
[capabilities]
|
||||
network = true # or false
|
||||
```
|
||||
|
||||
**Environment**:
|
||||
|
||||
```toml
|
||||
[capabilities]
|
||||
environment = ["PATH", "HOME"] # or omit for no access
|
||||
```
|
||||
|
||||
**Resource Limits**:
|
||||
|
||||
```toml
|
||||
[capabilities]
|
||||
max_memory_mb = 128
|
||||
max_cpu_time_secs = 10
|
||||
```
|
||||
|
||||
### Plugin Kinds
|
||||
|
||||
#### media_type
|
||||
|
||||
Register new media types with file extensions and MIME types.
|
||||
|
||||
**Trait**: `MediaTypeProvider`
|
||||
|
||||
**Methods**:
|
||||
|
||||
- `supported_media_types()`: Returns list of media type definitions
|
||||
- `can_handle(path, mime_type)`: Check if plugin can handle a file
|
||||
|
||||
#### metadata_extractor
|
||||
|
||||
Extract metadata from files.
|
||||
|
||||
**Trait**: `MetadataExtractor`
|
||||
|
||||
**Methods**:
|
||||
|
||||
- `extract_metadata(path)`: Extract metadata from file
|
||||
- `supported_types()`: Returns list of supported media type IDs
|
||||
|
||||
#### thumbnail_generator
|
||||
|
||||
Generate thumbnails for media files.
|
||||
|
||||
**Trait**: `ThumbnailGenerator`
|
||||
|
||||
**Methods**:
|
||||
|
||||
- `generate_thumbnail(path, output_path, options)`: Generate thumbnail
|
||||
- `supported_types()`: Returns list of supported media type IDs
|
||||
|
||||
#### search_backend
|
||||
|
||||
Implement custom search algorithms.
|
||||
|
||||
**Trait**: `SearchBackend`
|
||||
|
||||
**Methods**:
|
||||
|
||||
- `index_item(item)`: Index a media item
|
||||
- `remove_item(item_id)`: Remove item from index
|
||||
- `search(query)`: Perform search
|
||||
- `get_stats()`: Get index statistics
|
||||
|
||||
#### event_handler
|
||||
|
||||
React to system events.
|
||||
|
||||
**Trait**: `EventHandler`
|
||||
|
||||
**Methods**:
|
||||
|
||||
- `handle_event(event)`: Handle an event
|
||||
- `interested_events()`: Returns list of event types to receive
|
||||
|
||||
#### theme_provider
|
||||
|
||||
Provide UI themes.
|
||||
|
||||
**Trait**: `ThemeProvider`
|
||||
|
||||
**Methods**:
|
||||
|
||||
- `get_themes()`: List available themes
|
||||
- `load_theme(theme_id)`: Load theme data
|
||||
|
||||
## Creating a Plugin
|
||||
|
||||
### Step 1: Set Up Project
|
||||
|
||||
```bash
|
||||
# Create new Rust library project
|
||||
cargo new --lib my-plugin
|
||||
cd my-plugin
|
||||
|
||||
# Add dependencies
|
||||
cat >> Cargo.toml <<EOF
|
||||
[dependencies]
|
||||
pinakes-plugin-api = { path = "../../crates/pinakes-plugin-api" }
|
||||
async-trait = "0.1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
EOF
|
||||
```
|
||||
|
||||
### Step 2: Implement Plugin
|
||||
|
||||
```rust
|
||||
use async_trait::async_trait;
|
||||
use pinakes_plugin_api::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub struct MyPlugin {
|
||||
context: Option<PluginContext>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Plugin for MyPlugin {
|
||||
fn metadata(&self) -> &PluginMetadata {
|
||||
// Return plugin metadata
|
||||
}
|
||||
|
||||
async fn initialize(&mut self, context: PluginContext) -> PluginResult<()> {
|
||||
self.context = Some(context);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn shutdown(&mut self) -> PluginResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn health_check(&self) -> PluginResult<HealthStatus> {
|
||||
Ok(HealthStatus {
|
||||
healthy: true,
|
||||
message: None,
|
||||
metrics: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MetadataExtractor for MyPlugin {
|
||||
async fn extract_metadata(&self, path: &PathBuf) -> PluginResult<ExtractedMetadata> {
|
||||
// Extract metadata from file
|
||||
}
|
||||
|
||||
fn supported_types(&self) -> Vec<String> {
|
||||
vec!["my_type".to_string()]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Build to WASM
|
||||
|
||||
```bash
|
||||
# Install WASM target
|
||||
rustup target add wasm32-wasi
|
||||
|
||||
# Build
|
||||
cargo build --target wasm32-wasi --release
|
||||
|
||||
# Optimize (optional, wasm-tools provides wasm-strip functionality)
|
||||
cargo install wasm-tools
|
||||
wasm-tools strip target/wasm32-wasi/release/my_plugin.wasm -o target/wasm32-wasi/release/my_plugin.wasm
|
||||
|
||||
# Copy to plugin directory
|
||||
cp target/wasm32-wasi/release/my_plugin.wasm .
|
||||
```
|
||||
|
||||
### Step 4: Create Manifest
|
||||
|
||||
Create `plugin.toml` with appropriate configuration (see examples above).
|
||||
|
||||
### Step 5: Install Plugin
|
||||
|
||||
```bash
|
||||
# Copy to plugins directory
|
||||
cp -r my-plugin ~/.config/pinakes/plugins/
|
||||
|
||||
# Or use API
|
||||
curl -X POST http://localhost:3000/api/v1/plugins/install \
|
||||
-d '{"source": "/path/to/my-plugin"}'
|
||||
```
|
||||
|
||||
## Testing Plugins
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_metadata_extraction() {
|
||||
let mut plugin = MyPlugin::default();
|
||||
let context = PluginContext {
|
||||
data_dir: PathBuf::from("/tmp/data"),
|
||||
cache_dir: PathBuf::from("/tmp/cache"),
|
||||
config: Default::default(),
|
||||
capabilities: Default::default(),
|
||||
};
|
||||
|
||||
plugin.initialize(context).await.unwrap();
|
||||
|
||||
let metadata = plugin
|
||||
.extract_metadata(&PathBuf::from("test.txt"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(metadata.title.is_some());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
|
||||
```bash
|
||||
# Load plugin in test Pinakes instance
|
||||
pinakes --config test-config.toml plugin load /path/to/plugin
|
||||
|
||||
# Verify plugin is loaded
|
||||
pinakes plugin list
|
||||
|
||||
# Test plugin functionality
|
||||
pinakes scan /path/to/test/files
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Capability-Based Security
|
||||
|
||||
Plugins operate in a sandbox with explicit capabilities. Only request the
|
||||
minimum capabilities needed:
|
||||
|
||||
**Good**:
|
||||
|
||||
```toml
|
||||
[capabilities.filesystem]
|
||||
read = ["/tmp/cache"]
|
||||
```
|
||||
|
||||
**Bad**:
|
||||
|
||||
```toml
|
||||
[capabilities.filesystem]
|
||||
read = ["/", "/home", "/etc"] # Too broad!
|
||||
```
|
||||
|
||||
### Resource Limits
|
||||
|
||||
Always set appropriate resource limits:
|
||||
|
||||
```toml
|
||||
[capabilities]
|
||||
max_memory_mb = 128 # Reasonable for image processing
|
||||
max_cpu_time_secs = 10 # Prevent runaway operations
|
||||
```
|
||||
|
||||
### Input Validation
|
||||
|
||||
Always validate input in your plugin:
|
||||
|
||||
```rust
|
||||
async fn extract_metadata(&self, path: &PathBuf) -> PluginResult<ExtractedMetadata> {
|
||||
// Check file exists
|
||||
if !path.exists() {
|
||||
return Err(PluginError::InvalidInput("File not found".to_string()));
|
||||
}
|
||||
|
||||
// Check file size
|
||||
let metadata = std::fs::metadata(path)
|
||||
.map_err(|e| PluginError::IoError(e.to_string()))?;
|
||||
if metadata.len() > 10_000_000 { // 10MB limit
|
||||
return Err(PluginError::InvalidInput("File too large".to_string()));
|
||||
}
|
||||
|
||||
// Process file...
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Error Handling
|
||||
|
||||
- Use descriptive error messages
|
||||
- Return appropriate `PluginError` variants
|
||||
- Don't panic - return errors instead
|
||||
|
||||
### Performance
|
||||
|
||||
- Avoid blocking operations in async functions
|
||||
- Use streaming for large files
|
||||
- Implement timeouts for external operations
|
||||
- Cache results when appropriate
|
||||
|
||||
### Configuration
|
||||
|
||||
- Provide sensible defaults
|
||||
- Document all configuration options
|
||||
- Validate configuration during initialization
|
||||
|
||||
### Documentation
|
||||
|
||||
- Write clear README with examples
|
||||
- Document all configuration options
|
||||
- Include troubleshooting section
|
||||
- Provide integration examples
|
||||
|
||||
## API Reference
|
||||
|
||||
See the
|
||||
[pinakes-plugin-api documentation](../../crates/pinakes-plugin-api/README.md)
|
||||
for detailed API reference.
|
||||
|
||||
## Plugin Distribution
|
||||
|
||||
### Plugin Registry (Future)
|
||||
|
||||
A centralized plugin registry is planned for future releases:
|
||||
|
||||
```bash
|
||||
# Install from registry
|
||||
pinakes plugin install markdown-metadata
|
||||
|
||||
# Search plugins
|
||||
pinakes plugin search heif
|
||||
|
||||
# Update all plugins
|
||||
pinakes plugin update --all
|
||||
```
|
||||
|
||||
### Manual Distribution
|
||||
|
||||
Currently, plugins are distributed as directories containing:
|
||||
|
||||
- `plugin.toml` manifest
|
||||
- WASM binary
|
||||
- README and documentation
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin Won't Load
|
||||
|
||||
**Check manifest syntax**:
|
||||
|
||||
```bash
|
||||
# Validate TOML syntax
|
||||
taplo check plugin.toml
|
||||
```
|
||||
|
||||
**Check API version**: Ensure `api_version = "1.0"` in manifest.
|
||||
|
||||
**Check binary path**: Verify WASM binary exists at path specified in
|
||||
`plugin.binary.wasm`.
|
||||
|
||||
### Plugin Crashes
|
||||
|
||||
**Check resource limits**: Increase `max_memory_mb` or `max_cpu_time_secs` if
|
||||
operations are timing out.
|
||||
|
||||
**Check capabilities**: Ensure plugin has necessary filesystem/network
|
||||
capabilities.
|
||||
|
||||
**Check logs**:
|
||||
|
||||
```bash
|
||||
# View plugin logs
|
||||
tail -f ~/.local/share/pinakes/logs/plugin.log
|
||||
```
|
||||
|
||||
### Permission Denied Errors
|
||||
|
||||
**Filesystem capabilities**: Add required paths to
|
||||
`capabilities.filesystem.read` or `.write`.
|
||||
|
||||
**Network capabilities**: Set `capabilities.network = true` if plugin needs
|
||||
network access.
|
||||
|
||||
## Support
|
||||
|
||||
- **Issues**: https://github.com/notashelf/pinakes/issues
|
||||
- **Discussions**: https://github.com/notashelf/pinakes/discussions
|
||||
- **Documentation**: https://pinakes.readthedocs.io
|
||||
|
||||
## License
|
||||
|
||||
All example plugins are licensed under MIT.
|
||||
Loading…
Add table
Add a link
Reference in a new issue