feat: worker plugin system — domain ports, pipeline executor, built-in plugins

- PluginExecutor + PluginRegistry ports in domain
- ExecutePipelineCommand orchestrates job→pipeline→plugin steps
- ProcessNextJobCommand polls + executes next queued job
- InMemoryPluginRegistry, NoOp/MetadataExtractor/SidecarSync plugins
- Worker main rewritten with poll loop, factories module for DI
- Deleted template job/runner/jobs remnants
This commit is contained in:
2026-05-31 11:35:05 +02:00
parent 6c88ac344c
commit dacfc3d453
22 changed files with 587 additions and 90 deletions

View File

@@ -0,0 +1,68 @@
use async_trait::async_trait;
use domain::{
entities::{AssetMetadata, MetadataSource},
errors::DomainError,
ports::{AssetMetadataRepository, AssetRepository, FileStoragePort, PluginExecutor},
value_objects::{MetadataValue, StructuredData, SystemId},
};
use std::sync::Arc;
use tracing::info;
pub struct MetadataExtractorPlugin {
asset_repo: Arc<dyn AssetRepository>,
file_storage: Arc<dyn FileStoragePort>,
metadata_repo: Arc<dyn AssetMetadataRepository>,
}
impl MetadataExtractorPlugin {
pub fn new(
asset_repo: Arc<dyn AssetRepository>,
file_storage: Arc<dyn FileStoragePort>,
metadata_repo: Arc<dyn AssetMetadataRepository>,
) -> Self {
Self {
asset_repo,
file_storage,
metadata_repo,
}
}
}
#[async_trait]
impl PluginExecutor for MetadataExtractorPlugin {
fn plugin_name(&self) -> &str {
"metadata_extractor"
}
async fn execute(
&self,
asset_id: Option<SystemId>,
_payload: &StructuredData,
_config: &StructuredData,
) -> Result<StructuredData, DomainError> {
let asset_id = asset_id.ok_or_else(|| {
DomainError::Validation("metadata_extractor requires asset_id".into())
})?;
let asset = self
.asset_repo
.find_by_id(&asset_id)
.await?
.ok_or_else(|| DomainError::NotFound(format!("Asset {} not found", asset_id)))?;
let path = &asset.source_reference.relative_path;
let data = self.file_storage.read_file(path).await?;
let file_size = data.len() as i64;
let mut extracted = StructuredData::new();
extracted.insert("file_size_bytes", MetadataValue::Integer(file_size));
extracted.insert("mime_type", MetadataValue::String(asset.mime_type.clone()));
let metadata =
AssetMetadata::new(asset_id, MetadataSource::ExifExtracted, extracted.clone());
self.metadata_repo.save(&metadata).await?;
info!(asset_id = %asset_id, file_size, "extracted basic metadata");
Ok(extracted)
}
}

View File

@@ -0,0 +1,7 @@
pub mod metadata_extractor;
pub mod no_op;
pub mod sidecar_sync;
pub use metadata_extractor::MetadataExtractorPlugin;
pub use no_op::NoOpPlugin;
pub use sidecar_sync::SidecarSyncPlugin;

View File

@@ -0,0 +1,26 @@
use async_trait::async_trait;
use domain::{
errors::DomainError,
ports::PluginExecutor,
value_objects::{StructuredData, SystemId},
};
use tracing::info;
pub struct NoOpPlugin;
#[async_trait]
impl PluginExecutor for NoOpPlugin {
fn plugin_name(&self) -> &str {
"no_op"
}
async fn execute(
&self,
asset_id: Option<SystemId>,
_payload: &StructuredData,
_config: &StructuredData,
) -> Result<StructuredData, DomainError> {
info!(asset_id = ?asset_id, "no_op plugin executed");
Ok(StructuredData::new())
}
}

View File

@@ -0,0 +1,50 @@
use application::sidecar::{ExportSidecarCommand, ExportSidecarHandler};
use async_trait::async_trait;
use domain::{
errors::DomainError,
ports::PluginExecutor,
value_objects::{MetadataValue, StructuredData, SystemId},
};
use std::sync::Arc;
use tracing::info;
pub struct SidecarSyncPlugin {
export_handler: Arc<ExportSidecarHandler>,
}
impl SidecarSyncPlugin {
pub fn new(export_handler: Arc<ExportSidecarHandler>) -> Self {
Self { export_handler }
}
}
#[async_trait]
impl PluginExecutor for SidecarSyncPlugin {
fn plugin_name(&self) -> &str {
"sidecar_sync"
}
async fn execute(
&self,
asset_id: Option<SystemId>,
_payload: &StructuredData,
_config: &StructuredData,
) -> Result<StructuredData, DomainError> {
let asset_id = asset_id
.ok_or_else(|| DomainError::Validation("sidecar_sync requires asset_id".into()))?;
let record = self
.export_handler
.execute(ExportSidecarCommand { asset_id })
.await?;
let mut result = StructuredData::new();
result.insert(
"sidecar_path",
MetadataValue::String(record.sidecar_storage_path),
);
info!(asset_id = %asset_id, "sidecar synced");
Ok(result)
}
}