feat: real XMP sidecar adapter, replaces LogSidecarWriter stubs
- adapters-sidecar: XmpSidecarWriter using xmp_toolkit - Writes StructuredData → XMP with EXIF/DC/XMP namespace routing - Reads XMP back to StructuredData - Wired into bootstrap + worker, deleted both LogSidecarWriter stubs
This commit is contained in:
101
Cargo.lock
generated
101
Cargo.lock
generated
@@ -78,6 +78,17 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adapters-sidecar"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"domain",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"xmp_toolkit",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "adapters-storage"
|
name = "adapters-storage"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -478,6 +489,7 @@ dependencies = [
|
|||||||
"adapters-event-transport",
|
"adapters-event-transport",
|
||||||
"adapters-nats",
|
"adapters-nats",
|
||||||
"adapters-postgres",
|
"adapters-postgres",
|
||||||
|
"adapters-sidecar",
|
||||||
"adapters-storage",
|
"adapters-storage",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"application",
|
"application",
|
||||||
@@ -988,6 +1000,12 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fs_extra"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
@@ -2020,6 +2038,28 @@ dependencies = [
|
|||||||
"libm",
|
"libm",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_enum"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
|
||||||
|
dependencies = [
|
||||||
|
"num_enum_derive",
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_enum_derive"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-crate",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object_store"
|
name = "object_store"
|
||||||
version = "0.11.2"
|
version = "0.11.2"
|
||||||
@@ -2261,6 +2301,15 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-crate"
|
||||||
|
version = "3.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||||
|
dependencies = [
|
||||||
|
"toml_edit",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.104"
|
version = "1.0.104"
|
||||||
@@ -3474,6 +3523,36 @@ dependencies = [
|
|||||||
"webpki-roots 0.26.11",
|
"webpki-roots 0.26.11",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "1.1.1+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.25.12+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_parser",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_parser"
|
||||||
|
version = "1.1.2+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||||
|
dependencies = [
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower"
|
name = "tower"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
@@ -4164,6 +4243,15 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wit-bindgen"
|
name = "wit-bindgen"
|
||||||
version = "0.46.0"
|
version = "0.46.0"
|
||||||
@@ -4266,6 +4354,7 @@ dependencies = [
|
|||||||
"adapters-exif",
|
"adapters-exif",
|
||||||
"adapters-nats",
|
"adapters-nats",
|
||||||
"adapters-postgres",
|
"adapters-postgres",
|
||||||
|
"adapters-sidecar",
|
||||||
"adapters-storage",
|
"adapters-storage",
|
||||||
"adapters-thumbnail",
|
"adapters-thumbnail",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -4286,6 +4375,18 @@ version = "0.6.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xmp_toolkit"
|
||||||
|
version = "1.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04517ad6f440d16e52ade0ad6d781029313bdacaac4590de9a9114f8d737af61"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"fs_extra",
|
||||||
|
"num_enum",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "y4m"
|
name = "y4m"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ members = [
|
|||||||
"crates/adapters/nats",
|
"crates/adapters/nats",
|
||||||
"crates/adapters/exif",
|
"crates/adapters/exif",
|
||||||
"crates/adapters/thumbnail",
|
"crates/adapters/thumbnail",
|
||||||
|
"crates/adapters/sidecar",
|
||||||
"crates/presentation",
|
"crates/presentation",
|
||||||
"crates/bootstrap",
|
"crates/bootstrap",
|
||||||
"crates/worker",
|
"crates/worker",
|
||||||
@@ -50,6 +51,7 @@ adapters-event-transport = { path = "crates/adapters/event-transport" }
|
|||||||
adapters-nats = { path = "crates/adapters/nats" }
|
adapters-nats = { path = "crates/adapters/nats" }
|
||||||
adapters-exif = { path = "crates/adapters/exif" }
|
adapters-exif = { path = "crates/adapters/exif" }
|
||||||
adapters-thumbnail = { path = "crates/adapters/thumbnail" }
|
adapters-thumbnail = { path = "crates/adapters/thumbnail" }
|
||||||
|
adapters-sidecar = { path = "crates/adapters/sidecar" }
|
||||||
adapters-postgres = { path = "crates/adapters/postgres" }
|
adapters-postgres = { path = "crates/adapters/postgres" }
|
||||||
async-nats = "0.48"
|
async-nats = "0.48"
|
||||||
async-stream = "0.3"
|
async-stream = "0.3"
|
||||||
|
|||||||
11
crates/adapters/sidecar/Cargo.toml
Normal file
11
crates/adapters/sidecar/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "adapters-sidecar"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
domain = { workspace = true }
|
||||||
|
async-trait = { workspace = true }
|
||||||
|
tokio = { workspace = true, features = ["fs"] }
|
||||||
|
xmp_toolkit = "1.11"
|
||||||
|
tracing = { workspace = true }
|
||||||
139
crates/adapters/sidecar/src/lib.rs
Normal file
139
crates/adapters/sidecar/src/lib.rs
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use domain::{
|
||||||
|
errors::DomainError,
|
||||||
|
ports::SidecarWriterPort,
|
||||||
|
value_objects::{MetadataValue, StructuredData},
|
||||||
|
};
|
||||||
|
use xmp_toolkit::{
|
||||||
|
XmpMeta, XmpValue,
|
||||||
|
xmp_ns::{DC, EXIF, XMP},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct XmpSidecarWriter;
|
||||||
|
|
||||||
|
const EXIF_TAGS: &[&str] = &[
|
||||||
|
"DateTimeOriginal",
|
||||||
|
"ExposureTime",
|
||||||
|
"FNumber",
|
||||||
|
"ISOSpeedRatings",
|
||||||
|
"FocalLength",
|
||||||
|
"Make",
|
||||||
|
"Model",
|
||||||
|
"LensModel",
|
||||||
|
"GPSLatitude",
|
||||||
|
"GPSLongitude",
|
||||||
|
"GPSAltitude",
|
||||||
|
"Orientation",
|
||||||
|
"ImageWidth",
|
||||||
|
"ImageHeight",
|
||||||
|
"Flash",
|
||||||
|
"MeteringMode",
|
||||||
|
"WhiteBalance",
|
||||||
|
"ExposureProgram",
|
||||||
|
"ExposureBiasValue",
|
||||||
|
"ModifyDate",
|
||||||
|
"Software",
|
||||||
|
];
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl SidecarWriterPort for XmpSidecarWriter {
|
||||||
|
fn format_name(&self) -> &str {
|
||||||
|
"xmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_sidecar(&self, data: &StructuredData, path: &str) -> Result<(), DomainError> {
|
||||||
|
let mut xmp =
|
||||||
|
XmpMeta::new().map_err(|e| DomainError::Internal(format!("xmp init failed: {e}")))?;
|
||||||
|
|
||||||
|
register_namespaces()?;
|
||||||
|
|
||||||
|
for (key, value) in data.inner() {
|
||||||
|
let value_str = match value {
|
||||||
|
MetadataValue::String(s) => s.clone(),
|
||||||
|
MetadataValue::Integer(i) => i.to_string(),
|
||||||
|
MetadataValue::Float(f) => f.to_string(),
|
||||||
|
MetadataValue::Boolean(b) => b.to_string(),
|
||||||
|
MetadataValue::Null => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let ns = if EXIF_TAGS.contains(&key.as_str()) || key.starts_with("track:") {
|
||||||
|
EXIF
|
||||||
|
} else if key == "title" || key == "description" || key == "subject" {
|
||||||
|
DC
|
||||||
|
} else {
|
||||||
|
XMP
|
||||||
|
};
|
||||||
|
|
||||||
|
set_prop(&mut xmp, ns, key, &value_str)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let xmp_str = xmp.to_string();
|
||||||
|
let path = path.to_string();
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
if let Some(parent) = std::path::Path::new(&path).parent() {
|
||||||
|
std::fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| DomainError::Internal(format!("mkdir failed: {e}")))?;
|
||||||
|
}
|
||||||
|
std::fs::write(&path, xmp_str)
|
||||||
|
.map_err(|e| DomainError::Internal(format!("write failed: {e}")))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| DomainError::Internal(format!("spawn_blocking failed: {e}")))?
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_sidecar(&self, path: &str) -> Result<StructuredData, DomainError> {
|
||||||
|
let path = path.to_string();
|
||||||
|
let content = tokio::fs::read_to_string(&path).await.map_err(|e| {
|
||||||
|
if e.kind() == std::io::ErrorKind::NotFound {
|
||||||
|
DomainError::NotFound(format!("sidecar not found: {path}"))
|
||||||
|
} else {
|
||||||
|
DomainError::Internal(format!("read failed: {e}"))
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let xmp: XmpMeta = content
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| DomainError::Internal(format!("xmp parse failed: {e}")))?;
|
||||||
|
|
||||||
|
let mut data = StructuredData::new();
|
||||||
|
|
||||||
|
for ns in [DC, EXIF, XMP] {
|
||||||
|
let iter = xmp.iter(xmp_toolkit::IterOptions::default().schema_ns(ns));
|
||||||
|
for prop in iter {
|
||||||
|
if prop.name.is_empty() || prop.value.value.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let key = prop
|
||||||
|
.name
|
||||||
|
.split(':')
|
||||||
|
.next_back()
|
||||||
|
.unwrap_or(&prop.name)
|
||||||
|
.to_string();
|
||||||
|
if !key.is_empty() {
|
||||||
|
data.insert(key, MetadataValue::String(prop.value.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_namespaces() -> Result<(), DomainError> {
|
||||||
|
XmpMeta::register_namespace(DC, "dc")
|
||||||
|
.map_err(|e| DomainError::Internal(format!("ns register failed: {e}")))?;
|
||||||
|
XmpMeta::register_namespace(EXIF, "exif")
|
||||||
|
.map_err(|e| DomainError::Internal(format!("ns register failed: {e}")))?;
|
||||||
|
XmpMeta::register_namespace(XMP, "xmp")
|
||||||
|
.map_err(|e| DomainError::Internal(format!("ns register failed: {e}")))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_prop(xmp: &mut XmpMeta, ns: &str, key: &str, value: &str) -> Result<(), DomainError> {
|
||||||
|
xmp.set_property(ns, key, &XmpValue::from(value))
|
||||||
|
.map_err(|e| DomainError::Internal(format!("set {key} failed: {e}")))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
45
crates/adapters/sidecar/src/tests.rs
Normal file
45
crates/adapters/sidecar/src/tests.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use crate::XmpSidecarWriter;
|
||||||
|
use domain::{
|
||||||
|
ports::SidecarWriterPort,
|
||||||
|
value_objects::{MetadataValue, StructuredData},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn sample_metadata() -> StructuredData {
|
||||||
|
let mut data = StructuredData::new();
|
||||||
|
data.insert("Make", MetadataValue::String("Canon".into()));
|
||||||
|
data.insert("Model", MetadataValue::String("EOS R5".into()));
|
||||||
|
data.insert(
|
||||||
|
"DateTimeOriginal",
|
||||||
|
MetadataValue::String("2024:06:15 14:30:00".into()),
|
||||||
|
);
|
||||||
|
data.insert("ISOSpeedRatings", MetadataValue::Integer(800));
|
||||||
|
data
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn write_and_read_roundtrip() {
|
||||||
|
let writer = XmpSidecarWriter;
|
||||||
|
let data = sample_metadata();
|
||||||
|
let path = "/tmp/k-photos-test-sidecar-roundtrip.xmp";
|
||||||
|
|
||||||
|
writer.write_sidecar(&data, path).await.unwrap();
|
||||||
|
let read_back = writer.read_sidecar(path).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(read_back.get_string("Make"), Some("Canon"));
|
||||||
|
assert_eq!(read_back.get_string("Model"), Some("EOS R5"));
|
||||||
|
|
||||||
|
tokio::fs::remove_file(path).await.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn read_missing_returns_not_found() {
|
||||||
|
let writer = XmpSidecarWriter;
|
||||||
|
let result = writer.read_sidecar("/tmp/nonexistent-xmp-file.xmp").await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn format_name_is_xmp() {
|
||||||
|
let writer = XmpSidecarWriter;
|
||||||
|
assert_eq!(writer.format_name(), "xmp");
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ application = { workspace = true }
|
|||||||
adapters-auth = { workspace = true }
|
adapters-auth = { workspace = true }
|
||||||
|
|
||||||
adapters-storage = { workspace = true, features = ["s3"] }
|
adapters-storage = { workspace = true, features = ["s3"] }
|
||||||
|
adapters-sidecar = { workspace = true }
|
||||||
adapters-nats = { workspace = true }
|
adapters-nats = { workspace = true }
|
||||||
adapters-event-transport = { workspace = true }
|
adapters-event-transport = { workspace = true }
|
||||||
async-nats = { workspace = true }
|
async-nats = { workspace = true }
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod factory;
|
pub mod factory;
|
||||||
pub mod log_sidecar_writer;
|
|
||||||
pub mod services;
|
pub mod services;
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
use async_trait::async_trait;
|
|
||||||
use domain::{errors::DomainError, ports::SidecarWriterPort, value_objects::StructuredData};
|
|
||||||
|
|
||||||
pub struct LogSidecarWriter;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl SidecarWriterPort for LogSidecarWriter {
|
|
||||||
fn format_name(&self) -> &str {
|
|
||||||
"log_noop"
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn write_sidecar(&self, _data: &StructuredData, path: &str) -> Result<(), DomainError> {
|
|
||||||
tracing::info!(path, "sidecar write (no-op)");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn read_sidecar(&self, path: &str) -> Result<StructuredData, DomainError> {
|
|
||||||
tracing::info!(path, "sidecar read (no-op)");
|
|
||||||
Ok(StructuredData::new())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@ use tracing::info;
|
|||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod factory;
|
mod factory;
|
||||||
mod log_sidecar_writer;
|
|
||||||
mod services;
|
mod services;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
|||||||
@@ -9,13 +9,12 @@ use application::sidecar::{
|
|||||||
};
|
};
|
||||||
use presentation::state::SidecarHandlers;
|
use presentation::state::SidecarHandlers;
|
||||||
|
|
||||||
use crate::log_sidecar_writer::LogSidecarWriter;
|
|
||||||
|
|
||||||
pub fn build(pool: &PgPool) -> SidecarHandlers {
|
pub fn build(pool: &PgPool) -> SidecarHandlers {
|
||||||
let metadata_repo = Arc::new(PostgresAssetMetadataRepository::new(pool.clone()));
|
let metadata_repo = Arc::new(PostgresAssetMetadataRepository::new(pool.clone()));
|
||||||
let asset_repo = Arc::new(PostgresAssetRepository::new(pool.clone()));
|
let asset_repo = Arc::new(PostgresAssetRepository::new(pool.clone()));
|
||||||
let sidecar_repo = Arc::new(PostgresSidecarRepository::new(pool.clone()));
|
let sidecar_repo = Arc::new(PostgresSidecarRepository::new(pool.clone()));
|
||||||
let sidecar_writer: Arc<LogSidecarWriter> = Arc::new(LogSidecarWriter);
|
let sidecar_writer: Arc<adapters_sidecar::XmpSidecarWriter> =
|
||||||
|
Arc::new(adapters_sidecar::XmpSidecarWriter);
|
||||||
|
|
||||||
let export = Arc::new(ExportSidecarHandler::new(
|
let export = Arc::new(ExportSidecarHandler::new(
|
||||||
metadata_repo.clone(),
|
metadata_repo.clone(),
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ adapters-nats = { workspace = true }
|
|||||||
adapters-event-transport = { workspace = true }
|
adapters-event-transport = { workspace = true }
|
||||||
adapters-exif = { workspace = true }
|
adapters-exif = { workspace = true }
|
||||||
adapters-thumbnail = { workspace = true }
|
adapters-thumbnail = { workspace = true }
|
||||||
|
adapters-sidecar = { workspace = true }
|
||||||
async-nats = { workspace = true }
|
async-nats = { workspace = true }
|
||||||
|
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
let file_storage = Arc::new(adapters_storage::LocalFileStorage::new(
|
let file_storage = Arc::new(adapters_storage::LocalFileStorage::new(
|
||||||
&config.storage_path,
|
&config.storage_path,
|
||||||
));
|
));
|
||||||
let sidecar_writer: Arc<dyn domain::ports::SidecarWriterPort> = Arc::new(LogSidecarWriter);
|
let sidecar_writer: Arc<dyn domain::ports::SidecarWriterPort> =
|
||||||
|
Arc::new(adapters_sidecar::XmpSidecarWriter);
|
||||||
|
|
||||||
// Publisher transport consumes a client clone; the consumer gets another.
|
// Publisher transport consumes a client clone; the consumer gets another.
|
||||||
let pub_transport = adapters_nats::NatsTransport::new(nats_client.clone());
|
let pub_transport = adapters_nats::NatsTransport::new(nats_client.clone());
|
||||||
@@ -168,29 +169,3 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
error!("event loop: NATS stream ended unexpectedly");
|
error!("event loop: NATS stream ended unexpectedly");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LogSidecarWriter;
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl domain::ports::SidecarWriterPort for LogSidecarWriter {
|
|
||||||
fn format_name(&self) -> &str {
|
|
||||||
"log_noop"
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn write_sidecar(
|
|
||||||
&self,
|
|
||||||
_data: &domain::value_objects::StructuredData,
|
|
||||||
path: &str,
|
|
||||||
) -> Result<(), domain::errors::DomainError> {
|
|
||||||
info!(path, "sidecar write (no-op)");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn read_sidecar(
|
|
||||||
&self,
|
|
||||||
path: &str,
|
|
||||||
) -> Result<domain::value_objects::StructuredData, domain::errors::DomainError> {
|
|
||||||
info!(path, "sidecar read (no-op)");
|
|
||||||
Ok(domain::value_objects::StructuredData::new())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user