init plugins

This commit is contained in:
Bradley Nelson
2026-06-16 17:57:57 -06:00
parent 23e78e9a9c
commit 87d68c5b6f
27 changed files with 3099 additions and 24 deletions
+69
View File
@@ -936,6 +936,45 @@ impl Default for ContentSearchConfig {
}
}
/// WASM plugin runtime configuration (M0 walking skeleton).
///
/// The runtime is doubly gated: it is only compiled when the `plugins` cargo
/// feature is enabled, and only activated when `enabled` is `true`. The limits
/// below are conservative starting defaults, not part of the plugin ABI — each
/// deployment may tune them.
#[derive(Debug, Clone)]
pub struct PluginConfig {
/// Master switch. When disabled, no plugins are loaded and the lifecycle
/// bridge hook is never registered. Env: `OXICLOUD_ENABLE_PLUGINS`.
pub enabled: bool,
/// Directory scanned for plugins at startup; each plugin is a subdirectory
/// containing `plugin.toml` + its `.wasm`. Default: `{storage_path}/.plugins`.
/// Env: `OXICLOUD_PLUGINS_DIR`.
pub plugins_dir: Option<PathBuf>,
/// Wall-clock timeout for a single `handle` invocation. A runaway plugin
/// cannot stall the upload path beyond this. Default: 250.
/// Env: `OXICLOUD_PLUGIN_TIMEOUT_MS`.
pub invocation_timeout_ms: u64,
/// Max linear memory per plugin instance, in WASM pages (64 KiB each).
/// Default: 256 (≈ 16 MiB). Env: `OXICLOUD_PLUGIN_MAX_MEMORY_PAGES`.
pub max_memory_pages: u32,
/// Hard cap on the serialized event payload handed to a plugin. Default:
/// 256 KiB. Env: `OXICLOUD_PLUGIN_MAX_INPUT_BYTES`.
pub max_input_bytes: usize,
}
impl Default for PluginConfig {
fn default() -> Self {
Self {
enabled: false,
plugins_dir: None,
invocation_timeout_ms: 250,
max_memory_pages: 256,
max_input_bytes: 256 * 1024,
}
}
}
/// Global application configuration
#[derive(Debug, Clone)]
pub struct AppConfig {
@@ -977,6 +1016,8 @@ pub struct AppConfig {
pub i18n: I18nConfig,
/// Content-search configuration (embedded full-text index)
pub content_search: ContentSearchConfig,
/// WASM plugin runtime configuration
pub plugins: PluginConfig,
}
/// Server-side i18n knobs.
@@ -1029,6 +1070,7 @@ impl Default for AppConfig {
magic_link: MagicLinkConfig::default(),
i18n: I18nConfig::default(),
content_search: ContentSearchConfig::default(),
plugins: PluginConfig::default(),
}
}
}
@@ -1318,6 +1360,33 @@ impl AppConfig {
config.content_search.max_text_bytes = val;
}
// WASM plugin runtime
if let Ok(v) = env::var("OXICLOUD_ENABLE_PLUGINS").map(|v| v.parse::<bool>())
&& let Ok(val) = v
{
config.plugins.enabled = val;
}
if let Ok(dir) = env::var("OXICLOUD_PLUGINS_DIR")
&& !dir.trim().is_empty()
{
config.plugins.plugins_dir = Some(PathBuf::from(dir.trim()));
}
if let Ok(v) = env::var("OXICLOUD_PLUGIN_TIMEOUT_MS").map(|v| v.parse::<u64>())
&& let Ok(val) = v
{
config.plugins.invocation_timeout_ms = val;
}
if let Ok(v) = env::var("OXICLOUD_PLUGIN_MAX_MEMORY_PAGES").map(|v| v.parse::<u32>())
&& let Ok(val) = v
{
config.plugins.max_memory_pages = val;
}
if let Ok(v) = env::var("OXICLOUD_PLUGIN_MAX_INPUT_BYTES").map(|v| v.parse::<usize>())
&& let Ok(val) = v
{
config.plugins.max_input_bytes = val;
}
if let Ok(v) = env::var("OXICLOUD_EXPOSE_SYSTEM_USERS").map(|v| v.parse::<bool>())
&& let Ok(val) = v
{
+75 -9
View File
@@ -457,13 +457,28 @@ impl AppServiceFactory {
authz.clone(),
));
// Built before the upload/management services so the plugin lifecycle
// bridge (which looks file metadata up by id) can be wired into the
// dispatcher they receive. It depends only on repos + core, never on
// the upload service, so the reorder is safe.
let file_retrieval_service = Arc::new(FileRetrievalService::new_with_cache(
repos.file_read_repository.clone(),
core.file_content_cache.clone(),
core.image_transcode_service.clone(),
authz.clone(),
));
// Effective lifecycle dispatcher: the core hooks (thumbnails, metadata)
// plus, when the plugins feature is enabled, the WASM plugin bridge.
let file_lifecycle = self.effective_file_lifecycle(core, &file_retrieval_service);
let file_upload_service = Arc::new(
FileUploadService::new_with_read(
repos.file_write_repository.clone(),
repos.file_read_repository.clone(),
)
.with_content_cache(core.file_content_cache.clone())
.with_file_lifecycle_hook(core.file_lifecycle.clone())
.with_file_lifecycle_hook(file_lifecycle.clone())
.with_instant_upload(
authz.clone(),
core.dedup_service.clone(),
@@ -485,13 +500,6 @@ impl AppServiceFactory {
),
);
let file_retrieval_service = Arc::new(FileRetrievalService::new_with_cache(
repos.file_read_repository.clone(),
core.file_content_cache.clone(),
core.image_transcode_service.clone(),
authz.clone(),
));
// FileManagementService — ref_count handled by PG trigger, no dedup port needed
let file_management_service = Arc::new(
FileManagementService::with_trash(
@@ -502,7 +510,7 @@ impl AppServiceFactory {
Some(core.file_content_cache.clone()),
authz.clone(),
)
.with_file_lifecycle_hook(core.file_lifecycle.clone()),
.with_file_lifecycle_hook(file_lifecycle.clone()),
);
let file_use_case_factory = Arc::new(AppFileUseCaseFactory::new(
@@ -549,6 +557,64 @@ impl AppServiceFactory {
}
}
/// Builds the file lifecycle dispatcher handed to the upload/management
/// services. By default this is just the core dispatcher (thumbnails,
/// metadata). When the `plugins` feature is built and enabled, it wraps the
/// core dispatcher together with the WASM plugin bridge so plugins observe
/// `file.uploaded` events without any of the core hooks being aware of them.
fn effective_file_lifecycle(
&self,
core: &CoreServices,
file_retrieval: &Arc<FileRetrievalService>,
) -> Arc<dyn crate::application::ports::file_lifecycle::FileLifecycleHook> {
#[cfg(feature = "plugins")]
if self.config.plugins.enabled
&& let Some(manager) = self.create_plugin_manager()
{
use crate::application::adapters::plugin_lifecycle_hook::PluginLifecycleHook;
use crate::application::ports::plugin_ports::PluginDispatchPort;
let dispatch: Arc<dyn PluginDispatchPort> = manager;
let bridge = Arc::new(PluginLifecycleHook::new(dispatch, file_retrieval.clone()));
let composite = FileLifecycleService::new()
.with_hook(core.file_lifecycle.clone())
.with_hook(bridge);
return Arc::new(composite);
}
let _ = file_retrieval;
core.file_lifecycle.clone()
}
/// Discovers and loads WASM plugins when the `plugins` feature is enabled and
/// `OXICLOUD_ENABLE_PLUGINS=true`. Plugins live under
/// `OXICLOUD_PLUGINS_DIR` (default `{storage_path}/.plugins`); a missing or
/// empty directory simply yields no plugins.
#[cfg(feature = "plugins")]
fn create_plugin_manager(
&self,
) -> Option<Arc<crate::infrastructure::services::plugins::ExtismPluginManager>> {
if !self.config.plugins.enabled {
return None;
}
let dir = self
.config
.plugins
.plugins_dir
.clone()
.unwrap_or_else(|| self.config.storage_path.join(".plugins"));
let manager = crate::infrastructure::services::plugins::ExtismPluginManager::load_from_dir(
self.config.plugins.clone(),
&dir,
);
tracing::info!(
target: "oxicloud::plugins",
loaded = manager.loaded_count(),
"plugin manager initialized"
);
Some(Arc::new(manager))
}
/// Creates the audio metadata service (extracts ID3 tags from audio files)
pub fn create_audio_metadata_service(
&self,