mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-30 08:49:26 +00:00
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:
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user