feat(targets): complete redis mysql postgres target wiring (#2842)

Signed-off-by: jaehanbyun <awbrg789@naver.com>
Signed-off-by: houseme <housemecn@gmail.com>
Signed-off-by: Gunther Xing <jiengup@gmail.com>
Signed-off-by: JaySon-Huang <tshent@qq.com>
Co-authored-by: jaehanbyun <awbrg789@naver.com>
Co-authored-by: Gunther Xing <jiengup@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: cxymds <Cxymds@qq.com>
Co-authored-by: JaySon <tshent@qq.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
This commit is contained in:
houseme
2026-05-07 18:00:59 +08:00
committed by GitHub
parent b159d656cc
commit 5431b9273d
41 changed files with 6787 additions and 237 deletions
+83 -9
View File
@@ -14,7 +14,7 @@
use crate::arn::TargetID;
use crate::store::{Key, Store};
use crate::{StoreError, TargetError};
use crate::{StoreError, TargetError, TargetLog};
use async_trait::async_trait;
use rustfs_s3_common::EventName;
use serde::de::DeserializeOwned;
@@ -27,8 +27,11 @@ use tracing::warn;
pub mod kafka;
pub mod mqtt;
pub mod mysql;
pub mod nats;
pub mod postgres;
pub mod pulsar;
pub mod redis;
pub mod webhook;
/// A read-only snapshot of delivery counters for a target.
@@ -289,8 +292,11 @@ pub enum ChannelTargetType {
Webhook,
Kafka,
Mqtt,
MySql,
Nats,
Postgres,
Pulsar,
Redis,
}
impl ChannelTargetType {
@@ -299,8 +305,11 @@ impl ChannelTargetType {
ChannelTargetType::Webhook => "webhook",
ChannelTargetType::Kafka => "kafka",
ChannelTargetType::Mqtt => "mqtt",
ChannelTargetType::MySql => "mysql",
ChannelTargetType::Nats => "nats",
ChannelTargetType::Postgres => "postgres",
ChannelTargetType::Pulsar => "pulsar",
ChannelTargetType::Redis => "redis",
}
}
}
@@ -311,20 +320,15 @@ impl std::fmt::Display for ChannelTargetType {
ChannelTargetType::Webhook => write!(f, "webhook"),
ChannelTargetType::Kafka => write!(f, "kafka"),
ChannelTargetType::Mqtt => write!(f, "mqtt"),
ChannelTargetType::MySql => write!(f, "mysql"),
ChannelTargetType::Nats => write!(f, "nats"),
ChannelTargetType::Postgres => write!(f, "postgres"),
ChannelTargetType::Pulsar => write!(f, "pulsar"),
ChannelTargetType::Redis => write!(f, "redis"),
}
}
}
pub fn parse_bool(value: &str) -> Result<bool, TargetError> {
match value.to_lowercase().as_str() {
"true" | "on" | "yes" | "1" => Ok(true),
"false" | "off" | "no" | "0" => Ok(false),
_ => Err(TargetError::ParseError(format!("Unable to parse boolean: {value}"))),
}
}
/// `TargetType` enum represents the type of target in the notification system.
#[derive(Debug, Clone)]
pub enum TargetType {
@@ -350,6 +354,23 @@ impl std::fmt::Display for TargetType {
}
}
pub(crate) fn sanitize_queue_dir_component(component: &str) -> String {
let mut sanitized = String::with_capacity(component.len());
for ch in component.chars() {
if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') {
sanitized.push(ch);
} else {
sanitized.push('_');
}
}
if sanitized.is_empty() { "_".to_string() } else { sanitized }
}
pub(crate) fn queue_store_subdir_name(target_type: &str, target_id: &str) -> String {
format!("rustfs-{target_type}-{}", sanitize_queue_dir_component(target_id))
}
/// Decodes a form-urlencoded object name to its original form.
///
/// This function properly handles form-urlencoded strings where spaces are
@@ -377,6 +398,31 @@ pub fn decode_object_name(encoded: &str) -> Result<String, TargetError> {
.map_err(|e| TargetError::Encoding(format!("Failed to decode object key: {e}")))
}
pub(crate) fn build_queued_payload<E>(event: &EntityTarget<E>) -> Result<QueuedPayload, TargetError>
where
E: Send + Sync + 'static + Clone + Serialize + DeserializeOwned,
{
let object_name = decode_object_name(&event.object_name)?;
let key = format!("{}/{}", event.bucket_name, object_name);
let log = TargetLog {
event_name: event.event_name,
key,
records: vec![event.data.clone()],
};
let body = serde_json::to_vec(&log).map_err(|err| TargetError::Serialization(format!("Failed to serialize event: {err}")))?;
let meta = QueuedPayloadMeta::new(
event.event_name,
event.bucket_name.clone(),
event.object_name.clone(),
"application/json",
body.len(),
);
Ok(QueuedPayload::new(meta, body))
}
pub(crate) fn delete_stored_payload(
store: &(dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync),
key: &Key,
@@ -412,9 +458,37 @@ mod tests {
assert_eq!(decoded.body, br#"{"ok":true}"#);
}
#[test]
fn build_queued_payload_uses_event_data_shape() {
let event = EntityTarget {
object_name: "greeting+file+%282%29.csv".to_string(),
bucket_name: "bucket-a".to_string(),
event_name: EventName::ObjectCreatedPut,
data: "payload-data".to_string(),
};
let payload = build_queued_payload(&event).unwrap();
let value: serde_json::Value = serde_json::from_slice(&payload.body).unwrap();
assert_eq!(value["Key"], "bucket-a/greeting file (2).csv");
assert_eq!(value["Records"][0], "payload-data");
}
#[test]
fn queued_payload_decode_rejects_invalid_magic() {
let err = QueuedPayload::decode(b"bad-payload").unwrap_err();
assert!(err.to_string().contains("magic") || err.to_string().contains("short"));
}
#[test]
fn sanitize_queue_dir_component_replaces_non_path_safe_characters() {
let sanitized = sanitize_queue_dir_component("tenant:alpha/beta\\gamma?*");
assert_eq!(sanitized, "tenant_alpha_beta_gamma__");
}
#[test]
fn queue_store_subdir_name_sanitizes_target_id() {
let dir = queue_store_subdir_name("redis", "tenant:alpha");
assert_eq!(dir, "rustfs-redis-tenant_alpha");
}
}