This commit is contained in:
houseme
2025-06-20 10:51:36 +08:00
parent c658d88d25
commit d3cc36f6e0
22 changed files with 353 additions and 544 deletions
+1 -2
View File
@@ -17,5 +17,4 @@ deploy/certs/*
.rustfs.sys .rustfs.sys
.cargo .cargo
profile.json profile.json
.docker/openobserve-otel/data .docker/openobserve-otel/data
rustfs
Generated
+1 -51
View File
@@ -506,16 +506,6 @@ dependencies = [
"zbus 5.7.1", "zbus 5.7.1",
] ]
[[package]]
name = "assert-json-diff"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
dependencies = [
"serde",
"serde_json",
]
[[package]] [[package]]
name = "async-broadcast" name = "async-broadcast"
version = "0.7.2" version = "0.7.2"
@@ -1839,15 +1829,6 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "colored"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
dependencies = [
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "combine" name = "combine"
version = "4.6.7" version = "4.6.7"
@@ -5989,30 +5970,6 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "mockito"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7760e0e418d9b7e5777c0374009ca4c93861b9066f18cb334a20ce50ab63aa48"
dependencies = [
"assert-json-diff",
"bytes",
"colored",
"futures-util",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"hyper 1.6.0",
"hyper-util",
"log",
"rand 0.9.1",
"regex",
"serde_json",
"serde_urlencoded",
"similar",
"tokio",
]
[[package]] [[package]]
name = "muda" name = "muda"
version = "0.11.5" version = "0.11.5"
@@ -8431,13 +8388,12 @@ dependencies = [
"chrono", "chrono",
"const-str", "const-str",
"ecstore", "ecstore",
"libc",
"mockito",
"once_cell", "once_cell",
"quick-xml", "quick-xml",
"reqwest", "reqwest",
"rumqttc", "rumqttc",
"rustfs-config", "rustfs-config",
"rustfs-utils",
"serde", "serde",
"serde_json", "serde_json",
"snap", "snap",
@@ -9288,12 +9244,6 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "similar"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
[[package]] [[package]]
name = "simple_asn1" name = "simple_asn1"
version = "0.6.3" version = "0.6.3"
+1 -3
View File
@@ -121,14 +121,12 @@ keyring = { version = "3.6.2", features = [
"sync-secret-service", "sync-secret-service",
] } ] }
lazy_static = "1.5.0" lazy_static = "1.5.0"
libc = "0.2.174"
libsystemd = { version = "0.7.2" } libsystemd = { version = "0.7.2" }
local-ip-address = "0.6.5" local-ip-address = "0.6.5"
matchit = "0.8.4" matchit = "0.8.4"
md-5 = "0.10.6" md-5 = "0.10.6"
mime = "0.3.17" mime = "0.3.17"
mime_guess = "2.0.5" mime_guess = "2.0.5"
mockito = "1.7.0"
netif = "0.1.6" netif = "0.1.6"
nix = { version = "0.30.1", features = ["fs"] } nix = { version = "0.30.1", features = ["fs"] }
nu-ansi-term = "0.50.1" nu-ansi-term = "0.50.1"
@@ -171,7 +169,7 @@ rdkafka = { version = "0.37.0", features = ["tokio"] }
reed-solomon-erasure = { version = "6.0.0", features = ["simd-accel"] } reed-solomon-erasure = { version = "6.0.0", features = ["simd-accel"] }
reed-solomon-simd = { version = "3.0.0" } reed-solomon-simd = { version = "3.0.0" }
regex = { version = "1.11.1" } regex = { version = "1.11.1" }
reqwest = { version = "0.12.19", default-features = false, features = [ reqwest = { version = "0.12.20", default-features = false, features = [
"rustls-tls", "rustls-tls",
"charset", "charset",
"http2", "http2",
+6 -27
View File
@@ -10,11 +10,10 @@ use bytes::Bytes;
use rmp::Marker; use rmp::Marker;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::cmp::Ordering; use std::cmp::Ordering;
use std::convert::TryFrom;
use std::hash::Hasher; use std::hash::Hasher;
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::{collections::HashMap, io::Cursor}; use std::{collections::HashMap, io::Cursor};
use std::convert::TryFrom;
use std::fs::File;
use time::OffsetDateTime; use time::OffsetDateTime;
use tokio::io::AsyncRead; use tokio::io::AsyncRead;
use uuid::Uuid; use uuid::Uuid;
@@ -518,11 +517,7 @@ impl FileMeta {
let has_vid = { let has_vid = {
if !version_id.is_empty() { if !version_id.is_empty() {
let id = Uuid::parse_str(version_id)?; let id = Uuid::parse_str(version_id)?;
if !id.is_nil() { if !id.is_nil() { Some(id) } else { None }
Some(id)
} else {
None
}
} else { } else {
None None
} }
@@ -1253,11 +1248,7 @@ impl FileMetaVersionHeader {
cur.read_exact(&mut buf)?; cur.read_exact(&mut buf)?;
self.version_id = { self.version_id = {
let id = Uuid::from_bytes(buf); let id = Uuid::from_bytes(buf);
if id.is_nil() { if id.is_nil() { None } else { Some(id) }
None
} else {
Some(id)
}
}; };
// mod_time // mod_time
@@ -1431,11 +1422,7 @@ impl MetaObject {
cur.read_exact(&mut buf)?; cur.read_exact(&mut buf)?;
self.version_id = { self.version_id = {
let id = Uuid::from_bytes(buf); let id = Uuid::from_bytes(buf);
if id.is_nil() { if id.is_nil() { None } else { Some(id) }
None
} else {
Some(id)
}
}; };
} }
"DDir" => { "DDir" => {
@@ -1444,11 +1431,7 @@ impl MetaObject {
cur.read_exact(&mut buf)?; cur.read_exact(&mut buf)?;
self.data_dir = { self.data_dir = {
let id = Uuid::from_bytes(buf); let id = Uuid::from_bytes(buf);
if id.is_nil() { if id.is_nil() { None } else { Some(id) }
None
} else {
Some(id)
}
}; };
} }
"EcAlgo" => { "EcAlgo" => {
@@ -2017,11 +2000,7 @@ impl MetaDeleteMarker {
cur.read_exact(&mut buf)?; cur.read_exact(&mut buf)?;
self.version_id = { self.version_id = {
let id = Uuid::from_bytes(buf); let id = Uuid::from_bytes(buf);
if id.is_nil() { if id.is_nil() { None } else { Some(id) }
None
} else {
Some(id)
}
}; };
} }
+1 -2
View File
@@ -12,7 +12,6 @@ async-trait = { workspace = true }
chrono = { workspace = true, features = ["serde"] } chrono = { workspace = true, features = ["serde"] }
const-str = { workspace = true } const-str = { workspace = true }
ecstore = { workspace = true } ecstore = { workspace = true }
libc = { workspace = true }
once_cell = { workspace = true } once_cell = { workspace = true }
quick-xml = { workspace = true, features = ["serialize", "async-tokio"] } quick-xml = { workspace = true, features = ["serialize", "async-tokio"] }
reqwest = { workspace = true } reqwest = { workspace = true }
@@ -31,9 +30,9 @@ wildmatch = { workspace = true, features = ["serde"] }
[dev-dependencies] [dev-dependencies]
tokio = { workspace = true, features = ["test-util"] } tokio = { workspace = true, features = ["test-util"] }
mockito = "1.7"
reqwest = { workspace = true, default-features = false, features = ["rustls-tls", "charset", "http2", "system-proxy", "stream", "json", "blocking"] } reqwest = { workspace = true, default-features = false, features = ["rustls-tls", "charset", "http2", "system-proxy", "stream", "json", "blocking"] }
axum = { workspace = true } axum = { workspace = true }
rustfs-utils = { workspace = true, features = ["path"] }
[lints] [lints]
workspace = true workspace = true
+30 -29
View File
@@ -1,8 +1,6 @@
use notify::arn::TargetID; use rustfs_notify::arn::TargetID;
use notify::global::notification_system; use rustfs_notify::global::notification_system;
use notify::{ use rustfs_notify::{init_logger, BucketNotificationConfig, Event, EventName, LogLevel, NotificationError};
init_logger, BucketNotificationConfig, Event, EventName, LogLevel, NotificationError,
};
use std::time::Duration; use std::time::Duration;
use tracing::info; use tracing::info;
@@ -12,18 +10,24 @@ async fn main() -> Result<(), NotificationError> {
let system = notification_system(); let system = notification_system();
// --- 初始配置 (Webhook MQTT) --- // --- Initial configuration (Webhook and MQTT) ---
let mut config = notify::Config::new(); let mut config = rustfs_notify::Config::new();
let current_root = rustfs_utils::dirs::get_project_root().expect("failed to get project root");
println!("Current project root: {}", current_root.display());
// Webhook target configuration // Webhook target configuration
let mut webhook_kvs = notify::KVS::new(); let mut webhook_kvs = rustfs_notify::KVS::new();
webhook_kvs.set("enable", "on"); webhook_kvs.set("enable", "on");
webhook_kvs.set("endpoint", "http://127.0.0.1:3020/webhook"); webhook_kvs.set("endpoint", "http://127.0.0.1:3020/webhook");
webhook_kvs.set("auth_token", "secret-token"); webhook_kvs.set("auth_token", "secret-token");
// webhook_kvs.set("queue_dir", "/tmp/data/webhook"); // webhook_kvs.set("queue_dir", "/tmp/data/webhook");
webhook_kvs.set( webhook_kvs.set(
"queue_dir", "queue_dir",
"/Users/qun/Documents/rust/rustfs/notify/logs/webhook", current_root
.clone()
.join("../../deploy/logs/notify/webhook")
.to_str()
.unwrap()
.to_string(),
); );
webhook_kvs.set("queue_limit", "10000"); webhook_kvs.set("queue_limit", "10000");
let mut webhook_targets = std::collections::HashMap::new(); let mut webhook_targets = std::collections::HashMap::new();
@@ -31,7 +35,7 @@ async fn main() -> Result<(), NotificationError> {
config.insert("notify_webhook".to_string(), webhook_targets); config.insert("notify_webhook".to_string(), webhook_targets);
// MQTT target configuration // MQTT target configuration
let mut mqtt_kvs = notify::KVS::new(); let mut mqtt_kvs = rustfs_notify::KVS::new();
mqtt_kvs.set("enable", "on"); mqtt_kvs.set("enable", "on");
mqtt_kvs.set("broker", "mqtt://localhost:1883"); mqtt_kvs.set("broker", "mqtt://localhost:1883");
mqtt_kvs.set("topic", "rustfs/events"); mqtt_kvs.set("topic", "rustfs/events");
@@ -41,7 +45,11 @@ async fn main() -> Result<(), NotificationError> {
// webhook_kvs.set("queue_dir", "/tmp/data/mqtt"); // webhook_kvs.set("queue_dir", "/tmp/data/mqtt");
mqtt_kvs.set( mqtt_kvs.set(
"queue_dir", "queue_dir",
"/Users/qun/Documents/rust/rustfs/notify/logs/mqtt", current_root
.join("../../deploy/logs/notify/mqtt")
.to_str()
.unwrap()
.to_string(),
); );
mqtt_kvs.set("queue_limit", "10000"); mqtt_kvs.set("queue_limit", "10000");
@@ -49,35 +57,32 @@ async fn main() -> Result<(), NotificationError> {
mqtt_targets.insert("1".to_string(), mqtt_kvs); mqtt_targets.insert("1".to_string(), mqtt_kvs);
config.insert("notify_mqtt".to_string(), mqtt_targets); config.insert("notify_mqtt".to_string(), mqtt_targets);
// 加载配置并初始化系统 // Load the configuration and initialize the system
*system.config.write().await = config; *system.config.write().await = config;
system.init().await?; system.init().await?;
info!("✅ System initialized with Webhook and MQTT targets."); info!("✅ System initialized with Webhook and MQTT targets.");
// --- 1. 查询当前活动的 Target --- // --- Query the currently active Target ---
let active_targets = system.get_active_targets().await; let active_targets = system.get_active_targets().await;
info!("\n---> Currently active targets: {:?}", active_targets); info!("\n---> Currently active targets: {:?}", active_targets);
assert_eq!(active_targets.len(), 2); assert_eq!(active_targets.len(), 2);
tokio::time::sleep(Duration::from_secs(1)).await; tokio::time::sleep(Duration::from_secs(1)).await;
// --- 2. 精确删除一个 Target (例如 MQTT) --- // --- Exactly delete a Target (e.g. MQTT) ---
info!("\n---> Removing MQTT target..."); info!("\n---> Removing MQTT target...");
let mqtt_target_id = TargetID::new("1".to_string(), "mqtt".to_string()); let mqtt_target_id = TargetID::new("1".to_string(), "mqtt".to_string());
system.remove_target(&mqtt_target_id, "notify_mqtt").await?; system.remove_target(&mqtt_target_id, "notify_mqtt").await?;
info!("✅ MQTT target removed."); info!("✅ MQTT target removed.");
// --- 3. 再次查询活动的 Target --- // --- Query the activity's Target again ---
let active_targets_after_removal = system.get_active_targets().await; let active_targets_after_removal = system.get_active_targets().await;
info!( info!("\n---> Active targets after removal: {:?}", active_targets_after_removal);
"\n---> Active targets after removal: {:?}",
active_targets_after_removal
);
assert_eq!(active_targets_after_removal.len(), 1); assert_eq!(active_targets_after_removal.len(), 1);
assert_eq!(active_targets_after_removal[0].id, "1".to_string()); assert_eq!(active_targets_after_removal[0].id, "1".to_string());
// --- 4. 发送事件进行验证 --- // --- Send events for verification ---
// 配置一个规则,指向 Webhook 和已删除的 MQTT // Configure a rule to point to the Webhook and deleted MQTT
let mut bucket_config = BucketNotificationConfig::new("us-east-1"); let mut bucket_config = BucketNotificationConfig::new("us-east-1");
bucket_config.add_rule( bucket_config.add_rule(
&[EventName::ObjectCreatedPut], &[EventName::ObjectCreatedPut],
@@ -87,20 +92,16 @@ async fn main() -> Result<(), NotificationError> {
bucket_config.add_rule( bucket_config.add_rule(
&[EventName::ObjectCreatedPut], &[EventName::ObjectCreatedPut],
"*".to_string(), "*".to_string(),
TargetID::new("1".to_string(), "mqtt".to_string()), // 这个规则会匹配,但找不到 Target TargetID::new("1".to_string(), "mqtt".to_string()), // This rule will match, but the Target cannot be found
); );
system system.load_bucket_notification_config("my-bucket", &bucket_config).await?;
.load_bucket_notification_config("my-bucket", &bucket_config)
.await?;
info!("\n---> Sending an event..."); info!("\n---> Sending an event...");
let event = Event::new_test_event("my-bucket", "document.pdf", EventName::ObjectCreatedPut); let event = Event::new_test_event("my-bucket", "document.pdf", EventName::ObjectCreatedPut);
system system
.send_event("my-bucket", "s3:ObjectCreated:Put", "document.pdf", event) .send_event("my-bucket", "s3:ObjectCreated:Put", "document.pdf", event)
.await; .await;
info!( info!("✅ Event sent. Only the Webhook target should receive it. Check logs for warnings about the missing MQTT target.");
"✅ Event sent. Only the Webhook target should receive it. Check logs for warnings about the missing MQTT target."
);
tokio::time::sleep(Duration::from_secs(2)).await; tokio::time::sleep(Duration::from_secs(2)).await;
+23 -25
View File
@@ -1,9 +1,8 @@
use notify::arn::TargetID; // Using Global Accessories
use notify::global::notification_system; use rustfs_config::notify;
// 1. 使用全局访问器 use rustfs_notify::arn::TargetID;
use notify::{ use rustfs_notify::global::notification_system;
init_logger, BucketNotificationConfig, Event, EventName, LogLevel, NotificationError, KVS, use rustfs_notify::{init_logger, BucketNotificationConfig, Event, EventName, LogLevel, NotificationError, KVS};
};
use std::time::Duration; use std::time::Duration;
use tracing::info; use tracing::info;
@@ -11,11 +10,12 @@ use tracing::info;
async fn main() -> Result<(), NotificationError> { async fn main() -> Result<(), NotificationError> {
init_logger(LogLevel::Debug); init_logger(LogLevel::Debug);
// 获取全局 NotificationSystem 实例 // Get global NotificationSystem instance
let system = notification_system(); let system = notification_system();
// --- 初始配置 --- // --- Initial configuration ---
let mut config = notify::Config::new(); let mut config = rustfs_notify::Config::new();
let current_root = rustfs_utils::dirs::get_project_root().expect("failed to get project root");
// Webhook target // Webhook target
let mut webhook_kvs = KVS::new(); let mut webhook_kvs = KVS::new();
webhook_kvs.set("enable", "on"); webhook_kvs.set("enable", "on");
@@ -23,20 +23,25 @@ async fn main() -> Result<(), NotificationError> {
// webhook_kvs.set("queue_dir", "./logs/webhook"); // webhook_kvs.set("queue_dir", "./logs/webhook");
webhook_kvs.set( webhook_kvs.set(
"queue_dir", "queue_dir",
"/Users/qun/Documents/rust/rustfs/notify/logs/webhook", current_root
.clone()
.join("/deploy/logs/notify/webhook")
.to_str()
.unwrap()
.to_string(),
); );
let mut webhook_targets = std::collections::HashMap::new(); let mut webhook_targets = std::collections::HashMap::new();
webhook_targets.insert("1".to_string(), webhook_kvs); webhook_targets.insert("1".to_string(), webhook_kvs);
config.insert("notify_webhook".to_string(), webhook_targets); config.insert("notify_webhook".to_string(), webhook_targets);
// 加载初始配置并初始化系统 // Load the initial configuration and initialize the system
*system.config.write().await = config; *system.config.write().await = config;
system.init().await?; system.init().await?;
info!("✅ System initialized with Webhook target."); info!("✅ System initialized with Webhook target.");
tokio::time::sleep(Duration::from_secs(1)).await; tokio::time::sleep(Duration::from_secs(1)).await;
// --- 2. 动态更新系统配置:添加一个 MQTT Target --- // --- Dynamically update system configuration: Add an MQTT Target ---
info!("\n---> Dynamically adding MQTT target..."); info!("\n---> Dynamically adding MQTT target...");
let mut mqtt_kvs = KVS::new(); let mut mqtt_kvs = KVS::new();
mqtt_kvs.set("enable", "on"); mqtt_kvs.set("enable", "on");
@@ -47,18 +52,13 @@ async fn main() -> Result<(), NotificationError> {
mqtt_kvs.set("password", "123456"); mqtt_kvs.set("password", "123456");
mqtt_kvs.set("queue_limit", "10000"); mqtt_kvs.set("queue_limit", "10000");
// mqtt_kvs.set("queue_dir", "./logs/mqtt"); // mqtt_kvs.set("queue_dir", "./logs/mqtt");
mqtt_kvs.set( mqtt_kvs.set("queue_dir", current_root.join("/deploy/logs/notify/mqtt").to_str().unwrap().to_string());
"queue_dir", system.set_target_config("notify_mqtt", "1", mqtt_kvs).await?;
"/Users/qun/Documents/rust/rustfs/notify/logs/mqtt",
);
system
.set_target_config("notify_mqtt", "1", mqtt_kvs)
.await?;
info!("✅ MQTT target added and system reloaded."); info!("✅ MQTT target added and system reloaded.");
tokio::time::sleep(Duration::from_secs(1)).await; tokio::time::sleep(Duration::from_secs(1)).await;
// --- 3. 加载和管理 Bucket 配置 --- // --- Loading and managing Bucket configurations ---
info!("\n---> Loading bucket notification config..."); info!("\n---> Loading bucket notification config...");
let mut bucket_config = BucketNotificationConfig::new("us-east-1"); let mut bucket_config = BucketNotificationConfig::new("us-east-1");
bucket_config.add_rule( bucket_config.add_rule(
@@ -71,12 +71,10 @@ async fn main() -> Result<(), NotificationError> {
"*".to_string(), "*".to_string(),
TargetID::new("1".to_string(), "mqtt".to_string()), TargetID::new("1".to_string(), "mqtt".to_string()),
); );
system system.load_bucket_notification_config("my-bucket", &bucket_config).await?;
.load_bucket_notification_config("my-bucket", &bucket_config)
.await?;
info!("✅ Bucket 'my-bucket' config loaded."); info!("✅ Bucket 'my-bucket' config loaded.");
// --- 发送事件 --- // --- Send events ---
info!("\n---> Sending an event..."); info!("\n---> Sending an event...");
let event = Event::new_test_event("my-bucket", "document.pdf", EventName::ObjectCreatedPut); let event = Event::new_test_event("my-bucket", "document.pdf", EventName::ObjectCreatedPut);
system system
@@ -86,7 +84,7 @@ async fn main() -> Result<(), NotificationError> {
tokio::time::sleep(Duration::from_secs(2)).await; tokio::time::sleep(Duration::from_secs(2)).await;
// --- 动态移除配置 --- // --- Dynamically remove configuration ---
info!("\n---> Dynamically removing Webhook target..."); info!("\n---> Dynamically removing Webhook target...");
system.remove_target_config("notify_webhook", "1").await?; system.remove_target_config("notify_webhook", "1").await?;
info!("✅ Webhook target removed and system reloaded."); info!("✅ Webhook target removed and system reloaded.");
+31 -62
View File
@@ -16,40 +16,37 @@ struct ResetParams {
reason: Option<String>, reason: Option<String>,
} }
// 定义一个全局变量 统计接受到数据条数 // Define a global variable and count the number of data received
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
static WEBHOOK_COUNT: AtomicU64 = AtomicU64::new(0); static WEBHOOK_COUNT: AtomicU64 = AtomicU64::new(0);
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
// 构建应用 // Build an application
let app = Router::new() let app = Router::new()
.route("/webhook", post(receive_webhook)) .route("/webhook", post(receive_webhook))
.route( .route("/webhook/reset/{reason}", get(reset_webhook_count_with_path))
"/webhook/reset/{reason}",
get(reset_webhook_count_with_path),
)
.route("/webhook/reset", get(reset_webhook_count)) .route("/webhook/reset", get(reset_webhook_count))
.route("/webhook", get(receive_webhook)); .route("/webhook", get(receive_webhook));
// 启动服务器 // Start the server
let addr = "0.0.0.0:3020"; let addr = "0.0.0.0:3020";
let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
println!("Server running on {}", addr); println!("Server running on {}", addr);
// 服务启动后进行自检 // Self-checking after the service is started
tokio::spawn(async move { tokio::spawn(async move {
// 给服务器一点时间启动 // Give the server some time to start
tokio::time::sleep(std::time::Duration::from_secs(1)).await; tokio::time::sleep(std::time::Duration::from_secs(1)).await;
match is_service_active(addr).await { match is_service_active(addr).await {
Ok(true) => println!("服务健康检查:成功 - 服务正常运行"), Ok(true) => println!("Service health check: Successful - Service is running normally"),
Ok(false) => eprintln!("服务健康检查:失败 - 服务未响应"), Ok(false) => eprintln!("Service Health Check: Failed - Service Not Responded"),
Err(e) => eprintln!("服务健康检查错误:{}", e), Err(e) => eprintln!("Service health check errors:{}", e),
} }
}); });
// 创建关闭信号处理 // Create a shutdown signal processing
tokio::select! { tokio::select! {
result = axum::serve(listener, app) => { result = axum::serve(listener, app) => {
if let Err(e) = result { if let Err(e) = result {
@@ -62,16 +59,14 @@ async fn main() {
} }
} }
/// 创建一个方法重置 WEBHOOK_COUNT 的值 /// Create a method to reset the value of WEBHOOK_COUNT
async fn reset_webhook_count_with_path( async fn reset_webhook_count_with_path(axum::extract::Path(reason): axum::extract::Path<String>) -> Response<String> {
axum::extract::Path(reason): axum::extract::Path<String>, // Output the value of the current counter
) -> Response<String> {
// 输出当前计数器的值
let current_count = WEBHOOK_COUNT.load(Ordering::SeqCst); let current_count = WEBHOOK_COUNT.load(Ordering::SeqCst);
println!("Current webhook count: {}", current_count); println!("Current webhook count: {}", current_count);
println!("Reset webhook count, reason: {}", reason); println!("Reset webhook count, reason: {}", reason);
// 将计数器重置为 0 // Reset the counter to 0
WEBHOOK_COUNT.store(0, Ordering::SeqCst); WEBHOOK_COUNT.store(0, Ordering::SeqCst);
println!("Webhook count has been reset to 0."); println!("Webhook count has been reset to 0.");
@@ -85,17 +80,14 @@ async fn reset_webhook_count_with_path(
.unwrap() .unwrap()
} }
/// 创建一个方法重置 WEBHOOK_COUNT 的值 /// Create a method to reset the value of WEBHOOK_COUNT
/// 可以通过调用此方法来重置计数器 /// You can reset the counter by calling this method
async fn reset_webhook_count( async fn reset_webhook_count(Query(params): Query<ResetParams>, headers: HeaderMap) -> Response<String> {
Query(params): Query<ResetParams>, // Output the value of the current counter
headers: HeaderMap,
) -> Response<String> {
// 输出当前计数器的值
let current_count = WEBHOOK_COUNT.load(Ordering::SeqCst); let current_count = WEBHOOK_COUNT.load(Ordering::SeqCst);
println!("Current webhook count: {}", current_count); println!("Current webhook count: {}", current_count);
let reason = params.reason.unwrap_or_else(|| "未提供原因".to_string()); let reason = params.reason.unwrap_or_else(|| "Reason not provided".to_string());
println!("Reset webhook count, reason: {}", reason); println!("Reset webhook count, reason: {}", reason);
for header in headers { for header in headers {
@@ -104,51 +96,41 @@ async fn reset_webhook_count(
} }
println!("Reset webhook count printed headers"); println!("Reset webhook count printed headers");
// 将计数器重置为 0 // Reset the counter to 0
WEBHOOK_COUNT.store(0, Ordering::SeqCst); WEBHOOK_COUNT.store(0, Ordering::SeqCst);
println!("Webhook count has been reset to 0."); println!("Webhook count has been reset to 0.");
Response::builder() Response::builder()
.header("Foo", "Bar") .header("Foo", "Bar")
.status(StatusCode::OK) .status(StatusCode::OK)
.body(format!( .body(format!("Webhook count reset successfully current_count:{}", current_count))
"Webhook count reset successfully current_count:{}",
current_count
))
.unwrap() .unwrap()
} }
async fn is_service_active(addr: &str) -> Result<bool, String> { async fn is_service_active(addr: &str) -> Result<bool, String> {
let socket_addr = tokio::net::lookup_host(addr) let socket_addr = tokio::net::lookup_host(addr)
.await .await
.map_err(|e| format!("无法解析主机:{}", e))? .map_err(|e| format!("Unable to resolve host:{}", e))?
.next() .next()
.ok_or_else(|| "未找到地址".to_string())?; .ok_or_else(|| "Address not found".to_string())?;
println!("正在检查服务状态:{}", socket_addr); println!("Checking service status:{}", socket_addr);
match tokio::time::timeout( match tokio::time::timeout(std::time::Duration::from_secs(5), tokio::net::TcpStream::connect(socket_addr)).await {
std::time::Duration::from_secs(5),
tokio::net::TcpStream::connect(socket_addr),
)
.await
{
Ok(Ok(_)) => Ok(true), Ok(Ok(_)) => Ok(true),
Ok(Err(e)) => { Ok(Err(e)) => {
if e.kind() == std::io::ErrorKind::ConnectionRefused { if e.kind() == std::io::ErrorKind::ConnectionRefused {
Ok(false) Ok(false)
} else { } else {
Err(format!("连接失败:{}", e)) Err(format!("Connection failed:{}", e))
} }
} }
Err(_) => Err("连接超时".to_string()), Err(_) => Err("Connection timeout".to_string()),
} }
} }
async fn receive_webhook(Json(payload): Json<Value>) -> StatusCode { async fn receive_webhook(Json(payload): Json<Value>) -> StatusCode {
let start = SystemTime::now(); let start = SystemTime::now();
let since_the_epoch = start let since_the_epoch = start.duration_since(UNIX_EPOCH).expect("Time went backwards");
.duration_since(UNIX_EPOCH)
.expect("Time went backwards");
// get the number of seconds since the unix era // get the number of seconds since the unix era
let seconds = since_the_epoch.as_secs(); let seconds = since_the_epoch.as_secs();
@@ -157,20 +139,14 @@ async fn receive_webhook(Json(payload): Json<Value>) -> StatusCode {
let (year, month, day, hour, minute, second) = convert_seconds_to_date(seconds); let (year, month, day, hour, minute, second) = convert_seconds_to_date(seconds);
// output result // output result
println!( println!("current time:{:04}-{:02}-{:02} {:02}:{:02}:{:02}", year, month, day, hour, minute, second);
"current time:{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
year, month, day, hour, minute, second
);
println!( println!(
"received a webhook request time:{} content:\n {}", "received a webhook request time:{} content:\n {}",
seconds, seconds,
serde_json::to_string_pretty(&payload).unwrap() serde_json::to_string_pretty(&payload).unwrap()
); );
WEBHOOK_COUNT.fetch_add(1, Ordering::SeqCst); WEBHOOK_COUNT.fetch_add(1, Ordering::SeqCst);
println!( println!("Total webhook requests received: {}", WEBHOOK_COUNT.load(Ordering::SeqCst));
"Total webhook requests received: {}",
WEBHOOK_COUNT.load(Ordering::SeqCst)
);
StatusCode::OK StatusCode::OK
} }
@@ -221,12 +197,5 @@ fn convert_seconds_to_date(seconds: u64) -> (u32, u32, u32, u32, u32, u32) {
// calculate the number of seconds // calculate the number of seconds
second += total_seconds; second += total_seconds;
( (year as u32, month as u32, day as u32, hour as u32, minute as u32, second as u32)
year as u32,
month as u32,
day as u32,
hour as u32,
minute as u32,
second as u32,
)
} }
+46 -84
View File
@@ -3,23 +3,23 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt; use std::fmt;
/// 当解析事件名称字符串失败时返回的错误 /// Error returned when parsing event name string fails
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseEventNameError(String); pub struct ParseEventNameError(String);
impl fmt::Display for ParseEventNameError { impl fmt::Display for ParseEventNameError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "无效的事件名称:{}", self.0) write!(f, "Invalid event name:{}", self.0)
} }
} }
impl std::error::Error for ParseEventNameError {} impl std::error::Error for ParseEventNameError {}
/// 表示对象上发生的事件类型。 /// Represents the type of event that occurs on the object.
/// 基于 AWS S3 事件类型,并包含 RustFS 扩展。 /// Based on AWS S3 event type and includes RustFS extension.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum EventName { pub enum EventName {
// 单一事件类型 (值为 1-32 以兼容掩码逻辑) // Single event type (values are 1-32 for compatible mask logic)
ObjectAccessedGet = 1, ObjectAccessedGet = 1,
ObjectAccessedGetRetention = 2, ObjectAccessedGetRetention = 2,
ObjectAccessedGetLegalHold = 3, ObjectAccessedGetLegalHold = 3,
@@ -48,23 +48,23 @@ pub enum EventName {
ObjectRestoreCompleted = 26, ObjectRestoreCompleted = 26,
ObjectTransitionFailed = 27, ObjectTransitionFailed = 27,
ObjectTransitionComplete = 28, ObjectTransitionComplete = 28,
ScannerManyVersions = 29, // 对应 Go 的 ObjectManyVersions ScannerManyVersions = 29, // ObjectManyVersions corresponding to Go
ScannerLargeVersions = 30, // 对应 Go 的 ObjectLargeVersions ScannerLargeVersions = 30, // ObjectLargeVersions corresponding to Go
ScannerBigPrefix = 31, // 对应 Go 的 PrefixManyFolders ScannerBigPrefix = 31, // PrefixManyFolders corresponding to Go
LifecycleDelMarkerExpirationDelete = 32, // 对应 Go 的 ILMDelMarkerExpirationDelete LifecycleDelMarkerExpirationDelete = 32, // ILMDelMarkerExpirationDelete corresponding to Go
// 复合 "All" 事件类型 (没有用于掩码的顺序值) // Compound "All" event type (no sequential value for mask)
ObjectAccessedAll, ObjectAccessedAll,
ObjectCreatedAll, ObjectCreatedAll,
ObjectRemovedAll, ObjectRemovedAll,
ObjectReplicationAll, ObjectReplicationAll,
ObjectRestoreAll, ObjectRestoreAll,
ObjectTransitionAll, ObjectTransitionAll,
ObjectScannerAll, // 新增,来自 Go ObjectScannerAll, // New, from Go
Everything, // 新增,来自 Go Everything, // New, from Go
} }
// 用于 Everything.expand() 的单一事件类型顺序数组 // Single event type sequential array for Everything.expand()
const SINGLE_EVENT_NAMES_IN_ORDER: [EventName; 32] = [ const SINGLE_EVENT_NAMES_IN_ORDER: [EventName; 32] = [
EventName::ObjectAccessedGet, EventName::ObjectAccessedGet,
EventName::ObjectAccessedGetRetention, EventName::ObjectAccessedGetRetention,
@@ -103,7 +103,7 @@ const SINGLE_EVENT_NAMES_IN_ORDER: [EventName; 32] = [
const LAST_SINGLE_TYPE_VALUE: u32 = EventName::LifecycleDelMarkerExpirationDelete as u32; const LAST_SINGLE_TYPE_VALUE: u32 = EventName::LifecycleDelMarkerExpirationDelete as u32;
impl EventName { impl EventName {
/// 解析字符串为 EventName /// The parsed string is EventName.
pub fn parse(s: &str) -> Result<Self, ParseEventNameError> { pub fn parse(s: &str) -> Result<Self, ParseEventNameError> {
match s { match s {
"s3:BucketCreated:*" => Ok(EventName::BucketCreated), "s3:BucketCreated:*" => Ok(EventName::BucketCreated),
@@ -115,9 +115,7 @@ impl EventName {
"s3:ObjectAccessed:Head" => Ok(EventName::ObjectAccessedHead), "s3:ObjectAccessed:Head" => Ok(EventName::ObjectAccessedHead),
"s3:ObjectAccessed:Attributes" => Ok(EventName::ObjectAccessedAttributes), "s3:ObjectAccessed:Attributes" => Ok(EventName::ObjectAccessedAttributes),
"s3:ObjectCreated:*" => Ok(EventName::ObjectCreatedAll), "s3:ObjectCreated:*" => Ok(EventName::ObjectCreatedAll),
"s3:ObjectCreated:CompleteMultipartUpload" => { "s3:ObjectCreated:CompleteMultipartUpload" => Ok(EventName::ObjectCreatedCompleteMultipartUpload),
Ok(EventName::ObjectCreatedCompleteMultipartUpload)
}
"s3:ObjectCreated:Copy" => Ok(EventName::ObjectCreatedCopy), "s3:ObjectCreated:Copy" => Ok(EventName::ObjectCreatedCopy),
"s3:ObjectCreated:Post" => Ok(EventName::ObjectCreatedPost), "s3:ObjectCreated:Post" => Ok(EventName::ObjectCreatedPost),
"s3:ObjectCreated:Put" => Ok(EventName::ObjectCreatedPut), "s3:ObjectCreated:Put" => Ok(EventName::ObjectCreatedPut),
@@ -127,25 +125,15 @@ impl EventName {
"s3:ObjectCreated:DeleteTagging" => Ok(EventName::ObjectCreatedDeleteTagging), "s3:ObjectCreated:DeleteTagging" => Ok(EventName::ObjectCreatedDeleteTagging),
"s3:ObjectRemoved:*" => Ok(EventName::ObjectRemovedAll), "s3:ObjectRemoved:*" => Ok(EventName::ObjectRemovedAll),
"s3:ObjectRemoved:Delete" => Ok(EventName::ObjectRemovedDelete), "s3:ObjectRemoved:Delete" => Ok(EventName::ObjectRemovedDelete),
"s3:ObjectRemoved:DeleteMarkerCreated" => { "s3:ObjectRemoved:DeleteMarkerCreated" => Ok(EventName::ObjectRemovedDeleteMarkerCreated),
Ok(EventName::ObjectRemovedDeleteMarkerCreated)
}
"s3:ObjectRemoved:NoOP" => Ok(EventName::ObjectRemovedNoOP), "s3:ObjectRemoved:NoOP" => Ok(EventName::ObjectRemovedNoOP),
"s3:ObjectRemoved:DeleteAllVersions" => Ok(EventName::ObjectRemovedDeleteAllVersions), "s3:ObjectRemoved:DeleteAllVersions" => Ok(EventName::ObjectRemovedDeleteAllVersions),
"s3:LifecycleDelMarkerExpiration:Delete" => { "s3:LifecycleDelMarkerExpiration:Delete" => Ok(EventName::LifecycleDelMarkerExpirationDelete),
Ok(EventName::LifecycleDelMarkerExpirationDelete)
}
"s3:Replication:*" => Ok(EventName::ObjectReplicationAll), "s3:Replication:*" => Ok(EventName::ObjectReplicationAll),
"s3:Replication:OperationFailedReplication" => Ok(EventName::ObjectReplicationFailed), "s3:Replication:OperationFailedReplication" => Ok(EventName::ObjectReplicationFailed),
"s3:Replication:OperationCompletedReplication" => { "s3:Replication:OperationCompletedReplication" => Ok(EventName::ObjectReplicationComplete),
Ok(EventName::ObjectReplicationComplete) "s3:Replication:OperationMissedThreshold" => Ok(EventName::ObjectReplicationMissedThreshold),
} "s3:Replication:OperationReplicatedAfterThreshold" => Ok(EventName::ObjectReplicationReplicatedAfterThreshold),
"s3:Replication:OperationMissedThreshold" => {
Ok(EventName::ObjectReplicationMissedThreshold)
}
"s3:Replication:OperationReplicatedAfterThreshold" => {
Ok(EventName::ObjectReplicationReplicatedAfterThreshold)
}
"s3:Replication:OperationNotTracked" => Ok(EventName::ObjectReplicationNotTracked), "s3:Replication:OperationNotTracked" => Ok(EventName::ObjectReplicationNotTracked),
"s3:ObjectRestore:*" => Ok(EventName::ObjectRestoreAll), "s3:ObjectRestore:*" => Ok(EventName::ObjectRestoreAll),
"s3:ObjectRestore:Post" => Ok(EventName::ObjectRestorePost), "s3:ObjectRestore:Post" => Ok(EventName::ObjectRestorePost),
@@ -156,12 +144,12 @@ impl EventName {
"s3:Scanner:ManyVersions" => Ok(EventName::ScannerManyVersions), "s3:Scanner:ManyVersions" => Ok(EventName::ScannerManyVersions),
"s3:Scanner:LargeVersions" => Ok(EventName::ScannerLargeVersions), "s3:Scanner:LargeVersions" => Ok(EventName::ScannerLargeVersions),
"s3:Scanner:BigPrefix" => Ok(EventName::ScannerBigPrefix), "s3:Scanner:BigPrefix" => Ok(EventName::ScannerBigPrefix),
// ObjectScannerAll Everything 不能从字符串解析,因为 Go 版本也没有定义它们的字符串表示 // ObjectScannerAll and Everything cannot be parsed from strings, because the Go version also does not define their string representation.
_ => Err(ParseEventNameError(s.to_string())), _ => Err(ParseEventNameError(s.to_string())),
} }
} }
/// 返回事件类型的字符串表示。 /// Returns a string representation of the event type.
pub fn as_str(&self) -> &'static str { pub fn as_str(&self) -> &'static str {
match self { match self {
EventName::BucketCreated => "s3:BucketCreated:*", EventName::BucketCreated => "s3:BucketCreated:*",
@@ -173,9 +161,7 @@ impl EventName {
EventName::ObjectAccessedHead => "s3:ObjectAccessed:Head", EventName::ObjectAccessedHead => "s3:ObjectAccessed:Head",
EventName::ObjectAccessedAttributes => "s3:ObjectAccessed:Attributes", EventName::ObjectAccessedAttributes => "s3:ObjectAccessed:Attributes",
EventName::ObjectCreatedAll => "s3:ObjectCreated:*", EventName::ObjectCreatedAll => "s3:ObjectCreated:*",
EventName::ObjectCreatedCompleteMultipartUpload => { EventName::ObjectCreatedCompleteMultipartUpload => "s3:ObjectCreated:CompleteMultipartUpload",
"s3:ObjectCreated:CompleteMultipartUpload"
}
EventName::ObjectCreatedCopy => "s3:ObjectCreated:Copy", EventName::ObjectCreatedCopy => "s3:ObjectCreated:Copy",
EventName::ObjectCreatedPost => "s3:ObjectCreated:Post", EventName::ObjectCreatedPost => "s3:ObjectCreated:Post",
EventName::ObjectCreatedPut => "s3:ObjectCreated:Put", EventName::ObjectCreatedPut => "s3:ObjectCreated:Put",
@@ -188,19 +174,13 @@ impl EventName {
EventName::ObjectRemovedDeleteMarkerCreated => "s3:ObjectRemoved:DeleteMarkerCreated", EventName::ObjectRemovedDeleteMarkerCreated => "s3:ObjectRemoved:DeleteMarkerCreated",
EventName::ObjectRemovedNoOP => "s3:ObjectRemoved:NoOP", EventName::ObjectRemovedNoOP => "s3:ObjectRemoved:NoOP",
EventName::ObjectRemovedDeleteAllVersions => "s3:ObjectRemoved:DeleteAllVersions", EventName::ObjectRemovedDeleteAllVersions => "s3:ObjectRemoved:DeleteAllVersions",
EventName::LifecycleDelMarkerExpirationDelete => { EventName::LifecycleDelMarkerExpirationDelete => "s3:LifecycleDelMarkerExpiration:Delete",
"s3:LifecycleDelMarkerExpiration:Delete"
}
EventName::ObjectReplicationAll => "s3:Replication:*", EventName::ObjectReplicationAll => "s3:Replication:*",
EventName::ObjectReplicationFailed => "s3:Replication:OperationFailedReplication", EventName::ObjectReplicationFailed => "s3:Replication:OperationFailedReplication",
EventName::ObjectReplicationComplete => "s3:Replication:OperationCompletedReplication", EventName::ObjectReplicationComplete => "s3:Replication:OperationCompletedReplication",
EventName::ObjectReplicationNotTracked => "s3:Replication:OperationNotTracked", EventName::ObjectReplicationNotTracked => "s3:Replication:OperationNotTracked",
EventName::ObjectReplicationMissedThreshold => { EventName::ObjectReplicationMissedThreshold => "s3:Replication:OperationMissedThreshold",
"s3:Replication:OperationMissedThreshold" EventName::ObjectReplicationReplicatedAfterThreshold => "s3:Replication:OperationReplicatedAfterThreshold",
}
EventName::ObjectReplicationReplicatedAfterThreshold => {
"s3:Replication:OperationReplicatedAfterThreshold"
}
EventName::ObjectRestoreAll => "s3:ObjectRestore:*", EventName::ObjectRestoreAll => "s3:ObjectRestore:*",
EventName::ObjectRestorePost => "s3:ObjectRestore:Post", EventName::ObjectRestorePost => "s3:ObjectRestore:Post",
EventName::ObjectRestoreCompleted => "s3:ObjectRestore:Completed", EventName::ObjectRestoreCompleted => "s3:ObjectRestore:Completed",
@@ -210,13 +190,13 @@ impl EventName {
EventName::ScannerManyVersions => "s3:Scanner:ManyVersions", EventName::ScannerManyVersions => "s3:Scanner:ManyVersions",
EventName::ScannerLargeVersions => "s3:Scanner:LargeVersions", EventName::ScannerLargeVersions => "s3:Scanner:LargeVersions",
EventName::ScannerBigPrefix => "s3:Scanner:BigPrefix", EventName::ScannerBigPrefix => "s3:Scanner:BigPrefix",
// Go String() ObjectScannerAll Everything 返回 "" // Go's String() returns "" for ObjectScannerAll and Everything
EventName::ObjectScannerAll => "s3:Scanner:*", // 遵循 Go Expand 中的模式 EventName::ObjectScannerAll => "s3:Scanner:*", // Follow the pattern in Go Expand
EventName::Everything => "", // Go String() 对未处理的返回 "" EventName::Everything => "", // Go String() returns "" to unprocessed
} }
} }
/// 返回缩写事件类型的扩展值。 /// Returns the extended value of the abbreviation event type.
pub fn expand(&self) -> Vec<Self> { pub fn expand(&self) -> Vec<Self> {
match self { match self {
EventName::ObjectAccessedAll => vec![ EventName::ObjectAccessedAll => vec![
@@ -249,41 +229,35 @@ impl EventName {
EventName::ObjectReplicationMissedThreshold, EventName::ObjectReplicationMissedThreshold,
EventName::ObjectReplicationReplicatedAfterThreshold, EventName::ObjectReplicationReplicatedAfterThreshold,
], ],
EventName::ObjectRestoreAll => vec![ EventName::ObjectRestoreAll => vec![EventName::ObjectRestorePost, EventName::ObjectRestoreCompleted],
EventName::ObjectRestorePost, EventName::ObjectTransitionAll => vec![EventName::ObjectTransitionFailed, EventName::ObjectTransitionComplete],
EventName::ObjectRestoreCompleted,
],
EventName::ObjectTransitionAll => vec![
EventName::ObjectTransitionFailed,
EventName::ObjectTransitionComplete,
],
EventName::ObjectScannerAll => vec![ EventName::ObjectScannerAll => vec![
// 新增 // New
EventName::ScannerManyVersions, EventName::ScannerManyVersions,
EventName::ScannerLargeVersions, EventName::ScannerLargeVersions,
EventName::ScannerBigPrefix, EventName::ScannerBigPrefix,
], ],
EventName::Everything => { EventName::Everything => {
// 新增 // New
SINGLE_EVENT_NAMES_IN_ORDER.to_vec() SINGLE_EVENT_NAMES_IN_ORDER.to_vec()
} }
// 单一类型直接返回自身 // A single type returns to itself directly
_ => vec![*self], _ => vec![*self],
} }
} }
/// 返回类型的掩码。 /// Returns the mask of type.
/// 复合 "All" 类型会被展开。 /// The compound "All" type will be expanded.
pub fn mask(&self) -> u64 { pub fn mask(&self) -> u64 {
let value = *self as u32; let value = *self as u32;
if value > 0 && value <= LAST_SINGLE_TYPE_VALUE { if value > 0 && value <= LAST_SINGLE_TYPE_VALUE {
// 是单一类型 // It's a single type
1u64 << (value - 1) 1u64 << (value - 1)
} else { } else {
// 是复合类型 // It's a compound type
let mut mask = 0u64; let mut mask = 0u64;
for n in self.expand() { for n in self.expand() {
mask |= n.mask(); // 递归调用 mask mask |= n.mask(); // Recursively call mask
} }
mask mask
} }
@@ -296,7 +270,7 @@ impl fmt::Display for EventName {
} }
} }
/// 根据字符串转换为 `EventName` /// Convert to `EventName` according to string
impl From<&str> for EventName { impl From<&str> for EventName {
fn from(event_str: &str) -> Self { fn from(event_str: &str) -> Self {
EventName::parse(event_str).unwrap_or_else(|e| panic!("{}", e)) EventName::parse(event_str).unwrap_or_else(|e| panic!("{}", e))
@@ -399,28 +373,16 @@ impl Event {
pub fn new_test_event(bucket: &str, key: &str, event_name: EventName) -> Self { pub fn new_test_event(bucket: &str, key: &str, event_name: EventName) -> Self {
let mut user_metadata = HashMap::new(); let mut user_metadata = HashMap::new();
user_metadata.insert("x-amz-meta-test".to_string(), "value".to_string()); user_metadata.insert("x-amz-meta-test".to_string(), "value".to_string());
user_metadata.insert( user_metadata.insert("x-amz-storage-storage-options".to_string(), "value".to_string());
"x-amz-storage-storage-options".to_string(),
"value".to_string(),
);
user_metadata.insert("x-amz-meta-".to_string(), "value".to_string()); user_metadata.insert("x-amz-meta-".to_string(), "value".to_string());
user_metadata.insert("x-rustfs-meta-".to_string(), "rustfs-value".to_string()); user_metadata.insert("x-rustfs-meta-".to_string(), "rustfs-value".to_string());
user_metadata.insert("x-request-id".to_string(), "request-id-123".to_string()); user_metadata.insert("x-request-id".to_string(), "request-id-123".to_string());
user_metadata.insert("x-bucket".to_string(), "bucket".to_string()); user_metadata.insert("x-bucket".to_string(), "bucket".to_string());
user_metadata.insert("x-object".to_string(), "object".to_string()); user_metadata.insert("x-object".to_string(), "object".to_string());
user_metadata.insert( user_metadata.insert("x-rustfs-origin-endpoint".to_string(), "http://127.0.0.1".to_string());
"x-rustfs-origin-endpoint".to_string(),
"http://127.0.0.1".to_string(),
);
user_metadata.insert("x-rustfs-user-metadata".to_string(), "metadata".to_string()); user_metadata.insert("x-rustfs-user-metadata".to_string(), "metadata".to_string());
user_metadata.insert( user_metadata.insert("x-rustfs-deployment-id".to_string(), "deployment-id-123".to_string());
"x-rustfs-deployment-id".to_string(), user_metadata.insert("x-rustfs-origin-endpoint-code".to_string(), "http://127.0.0.1".to_string());
"deployment-id-123".to_string(),
);
user_metadata.insert(
"x-rustfs-origin-endpoint-code".to_string(),
"http://127.0.0.1".to_string(),
);
user_metadata.insert("x-rustfs-bucket-name".to_string(), "bucket".to_string()); user_metadata.insert("x-rustfs-bucket-name".to_string(), "bucket".to_string());
user_metadata.insert("x-rustfs-object-key".to_string(), key.to_string()); user_metadata.insert("x-rustfs-object-key".to_string(), key.to_string());
user_metadata.insert("x-rustfs-object-size".to_string(), "1024".to_string()); user_metadata.insert("x-rustfs-object-size".to_string(), "1024".to_string());
@@ -466,7 +428,7 @@ impl Event {
}, },
} }
} }
/// 返回事件掩码 /// Return event mask
pub fn mask(&self) -> u64 { pub fn mask(&self) -> u64 {
self.event_name.mask() self.event_name.mask()
} }
+2 -2
View File
@@ -1,4 +1,4 @@
//! RustFs Notify - A flexible and extensible event notification system for object storage. //! RustFS Notify - A flexible and extensible event notification system for object storage.
//! //!
//! This library provides a Rust implementation of a storage bucket notification system, //! This library provides a Rust implementation of a storage bucket notification system,
//! similar to RustFS's notification system. It supports sending events to various targets //! similar to RustFS's notification system. It supports sending events to various targets
@@ -35,7 +35,7 @@ use tracing_subscriber::{fmt, prelude::*, util::SubscriberInitExt, EnvFilter};
/// ///
/// # Example /// # Example
/// ``` /// ```
/// notify::init_logger(notify::LogLevel::Info); /// rustfs_notify::init_logger(rustfs_notify::LogLevel::Info);
/// ``` /// ```
pub fn init_logger(level: LogLevel) { pub fn init_logger(level: LogLevel) {
let filter = EnvFilter::default().add_directive(level.into()); let filter = EnvFilter::default().add_directive(level.into());
+3 -9
View File
@@ -39,7 +39,7 @@ impl BucketNotificationConfig {
/// Parses notification configuration from XML. /// Parses notification configuration from XML.
/// `arn_list` is a list of valid ARN strings for validation. /// `arn_list` is a list of valid ARN strings for validation.
pub fn from_xml<R: Read>( pub fn from_xml<R: Read + std::io::BufRead>(
reader: R, reader: R,
current_region: &str, current_region: &str,
arn_list: &[String], arn_list: &[String],
@@ -72,11 +72,7 @@ impl BucketNotificationConfig {
/// However, Go's Config has a Validate method. /// However, Go's Config has a Validate method.
/// The primary validation now happens during `from_xml` via `NotificationConfiguration::validate`. /// The primary validation now happens during `from_xml` via `NotificationConfiguration::validate`.
/// This method could re-check against an updated arn_list or region if needed. /// This method could re-check against an updated arn_list or region if needed.
pub fn validate( pub fn validate(&self, current_region: &str, arn_list: &[String]) -> Result<(), BucketNotificationConfigError> {
&self,
current_region: &str,
arn_list: &[String],
) -> Result<(), BucketNotificationConfigError> {
if self.region != current_region { if self.region != current_region {
return Err(BucketNotificationConfigError::RegionMismatch { return Err(BucketNotificationConfigError::RegionMismatch {
config_region: self.region.clone(), config_region: self.region.clone(),
@@ -93,9 +89,7 @@ impl BucketNotificationConfig {
// Construct the ARN string for this target_id and self.region // Construct the ARN string for this target_id and self.region
let arn_to_check = target_id.to_arn(&self.region); // Assuming TargetID has to_arn let arn_to_check = target_id.to_arn(&self.region); // Assuming TargetID has to_arn
if !arn_list.contains(&arn_to_check.to_arn_string()) { if !arn_list.contains(&arn_to_check.to_arn_string()) {
return Err(BucketNotificationConfigError::ArnNotFound( return Err(BucketNotificationConfigError::ArnNotFound(arn_to_check.to_arn_string()));
arn_to_check.to_arn_string(),
));
} }
} }
} }
+14 -20
View File
@@ -22,12 +22,7 @@ impl RulesMap {
/// target_id: Notify the target. /// target_id: Notify the target.
/// ///
/// This method expands the composite event name. /// This method expands the composite event name.
pub fn add_rule_config( pub fn add_rule_config(&mut self, event_names: &[EventName], pattern: String, target_id: TargetID) {
&mut self,
event_names: &[EventName],
pattern: String,
target_id: TargetID,
) {
let mut effective_pattern = pattern; let mut effective_pattern = pattern;
if effective_pattern.is_empty() { if effective_pattern.is_empty() {
effective_pattern = "*".to_string(); // Match all by default effective_pattern = "*".to_string(); // Match all by default
@@ -55,8 +50,7 @@ impl RulesMap {
} }
} }
/// 从当前 RulesMap 中移除另一个 RulesMap 中定义的规则。 /// Remove another rule defined in the RulesMap from the current RulesMap.
/// 对应 Go 的 `RulesMap.Remove(rulesMap2 RulesMap)`
pub fn remove_map(&mut self, other_map: &Self) { pub fn remove_map(&mut self, other_map: &Self) {
let mut events_to_remove = Vec::new(); let mut events_to_remove = Vec::new();
for (event_name, self_pattern_rules) in &mut self.map { for (event_name, self_pattern_rules) in &mut self.map {
@@ -72,24 +66,24 @@ impl RulesMap {
} }
} }
/// 匹配给定事件名称和对象键的规则,返回所有匹配的 TargetID ///Rules matching the given event name and object key, returning all matching TargetIDs.
pub fn match_rules(&self, event_name: EventName, object_key: &str) -> TargetIdSet { pub fn match_rules(&self, event_name: EventName, object_key: &str) -> TargetIdSet {
// 首先尝试直接匹配事件名称 // First try to directly match the event name
if let Some(pattern_rules) = self.map.get(&event_name) { if let Some(pattern_rules) = self.map.get(&event_name) {
let targets = pattern_rules.match_targets(object_key); let targets = pattern_rules.match_targets(object_key);
if !targets.is_empty() { if !targets.is_empty() {
return targets; return targets;
} }
} }
// Go RulesMap[eventName] 直接获取,如果不存在则为空 Rules // Go's RulesMap[eventName] is directly retrieved, and if it does not exist, it is empty Rules.
// Rust HashMap::get 返回 Option。如果事件名不存在,则没有规则。 // Rust's HashMap::get returns Option. If the event name does not exist, there is no rule.
// 复合事件(如 ObjectCreatedAll)在 add_rule_config 时已展开为单一事件。 // Compound events (such as ObjectCreatedAll) have been expanded as a single event when add_rule_config.
// 因此,查询时应使用单一事件名称。 // Therefore, a single event name should be used when querying.
// 如果 event_name 本身就是单一类型,则直接查找。 // If event_name itself is a single type, look it up directly.
// 如果 event_name 是复合类型,Go 的逻辑是在添加时展开。 // If event_name is a compound type, Go's logic is expanded when added.
// 这里的 match_rules 应该接收已经可能是单一的事件。 // Here match_rules should receive events that may already be single.
// 如果调用者传入的是复合事件,它应该先自行展开或此函数处理。 // If the caller passes in a compound event, it should expand itself or handle this function first.
// 假设 event_name 已经是具体的、可用于查找的事件。 // Assume that event_name is already a specific event that can be used for searching.
self.map self.map
.get(&event_name) .get(&event_name)
.map_or_else(TargetIdSet::new, |pr| pr.match_targets(object_key)) .map_or_else(TargetIdSet::new, |pr| pr.match_targets(object_key))
@@ -99,7 +93,7 @@ impl RulesMap {
self.map.is_empty() self.map.is_empty()
} }
/// 返回内部规则的克隆,用于 BucketNotificationConfig::validate 等场景。 /// Returns a clone of internal rules for use in scenarios such as BucketNotificationConfig::validate.
pub fn inner(&self) -> &HashMap<EventName, PatternRules> { pub fn inner(&self) -> &HashMap<EventName, PatternRules> {
&self.map &self.map
} }
+10 -10
View File
@@ -9,7 +9,7 @@ use thiserror::Error;
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum ParseConfigError { pub enum ParseConfigError {
#[error("XML parsing error:{0}")] #[error("XML parsing error:{0}")]
XmlError(#[from] quick_xml::errors::Error), XmlError(#[from] quick_xml::errors::serialize::DeError),
#[error("Invalid filter value:{0}")] #[error("Invalid filter value:{0}")]
InvalidFilterValue(String), InvalidFilterValue(String),
#[error("Invalid filter name: {0}, only 'prefix' or 'suffix' is allowed")] #[error("Invalid filter name: {0}, only 'prefix' or 'suffix' is allowed")]
@@ -193,10 +193,10 @@ impl QueueConfig {
pub struct LambdaConfigDetail { pub struct LambdaConfigDetail {
#[serde(rename = "CloudFunction")] #[serde(rename = "CloudFunction")]
pub arn: String, pub arn: String,
// 根据 AWS S3 文档,<CloudFunctionConfiguration> 通常还包含 Id, Event, Filter // According to AWS S3 documentation, <CloudFunctionConfiguration> usually also contains Id, Event, Filter
// 但为了严格对应提供的 Go `lambda` 结构体,这里只包含 ARN。 // But in order to strictly correspond to the Go `lambda` structure provided, only ARN is included here.
// 如果需要完整支持,可以添加其他字段。 // If full support is required, additional fields can be added.
// 例如: // For example:
// #[serde(rename = "Id", skip_serializing_if = "Option::is_none")] // #[serde(rename = "Id", skip_serializing_if = "Option::is_none")]
// pub id: Option<String>, // pub id: Option<String>,
// #[serde(rename = "Event", default, skip_serializing_if = "Vec::is_empty")] // #[serde(rename = "Event", default, skip_serializing_if = "Vec::is_empty")]
@@ -211,7 +211,7 @@ pub struct LambdaConfigDetail {
pub struct TopicConfigDetail { pub struct TopicConfigDetail {
#[serde(rename = "Topic")] #[serde(rename = "Topic")]
pub arn: String, pub arn: String,
// 类似于 LambdaConfigDetail,可以根据需要扩展以包含 Id, Event, Filter 等字段。 // Similar to LambdaConfigDetail, it can be extended to include fields such as Id, Event, Filter, etc.
} }
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)] #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
@@ -236,8 +236,8 @@ pub struct NotificationConfiguration {
} }
impl NotificationConfiguration { impl NotificationConfiguration {
pub fn from_reader<R: Read>(reader: R) -> Result<Self, ParseConfigError> { pub fn from_reader<R: Read + std::io::BufRead>(reader: R) -> Result<Self, ParseConfigError> {
let config: NotificationConfiguration = quick_xml::reader::Reader::from_reader(reader)?; let config: NotificationConfiguration = quick_xml::de::from_reader(reader)?;
Ok(config) Ok(config)
} }
@@ -268,7 +268,7 @@ impl NotificationConfiguration {
if self.xmlns.is_none() { if self.xmlns.is_none() {
self.xmlns = Some("http://s3.amazonaws.com/doc/2006-03-01/".to_string()); self.xmlns = Some("http://s3.amazonaws.com/doc/2006-03-01/".to_string());
} }
// 注意:如果 LambdaConfigDetail TopicConfigDetail 将来包含区域等信息, // Note: If LambdaConfigDetail and TopicConfigDetail contain information such as regions in the future,
// 也可能需要在这里设置默认值。但根据当前定义,它们只包含 ARN 字符串。 // You may also need to set the default value here. But according to the current definition, they only contain ARN strings.
} }
} }
+40 -107
View File
@@ -13,7 +13,7 @@ use tracing::{debug, error, info, warn};
/// Streams events from the store to the target /// Streams events from the store to the target
pub async fn stream_events( pub async fn stream_events(
store: &mut (dyn Store<crate::event::Event, Error = StoreError, Key = Key> + Send), store: &mut (dyn Store<Event, Error = StoreError, Key = Key> + Send),
target: &dyn Target, target: &dyn Target,
mut cancel_rx: mpsc::Receiver<()>, mut cancel_rx: mpsc::Receiver<()>,
) { ) {
@@ -42,10 +42,7 @@ pub async fn stream_events(
for key in keys { for key in keys {
// Check for cancellation before processing each event // Check for cancellation before processing each event
if cancel_rx.try_recv().is_ok() { if cancel_rx.try_recv().is_ok() {
info!( info!("Cancellation received during processing for target: {}", target.name());
"Cancellation received during processing for target: {}",
target.name()
);
return; return;
} }
@@ -70,7 +67,7 @@ pub async fn stream_events(
TargetError::Timeout(_) => { TargetError::Timeout(_) => {
warn!("Timeout for target {}, retrying...", target.name()); warn!("Timeout for target {}, retrying...", target.name());
retry_count += 1; retry_count += 1;
sleep(Duration::from_secs((retry_count * 5) as u64)).await; // 指数退避 sleep(Duration::from_secs((retry_count * 5) as u64)).await; // Exponential backoff
} }
_ => { _ => {
// Permanent error, skip this event // Permanent error, skip this event
@@ -84,11 +81,7 @@ pub async fn stream_events(
// Remove event from store if successfully sent // Remove event from store if successfully sent
if retry_count >= MAX_RETRIES && !success { if retry_count >= MAX_RETRIES && !success {
warn!( warn!("Max retries exceeded for event {}, target: {}, skipping", key.to_string(), target.name());
"Max retries exceeded for event {}, target: {}, skipping",
key.to_string(),
target.name()
);
} }
} }
@@ -120,10 +113,7 @@ pub fn start_event_stream_with_batching(
semaphore: Arc<Semaphore>, semaphore: Arc<Semaphore>,
) -> mpsc::Sender<()> { ) -> mpsc::Sender<()> {
let (cancel_tx, cancel_rx) = mpsc::channel(1); let (cancel_tx, cancel_rx) = mpsc::channel(1);
debug!( debug!("Starting event stream with batching for target: {}", target.name());
"Starting event stream with batching for target: {}",
target.name()
);
tokio::spawn(async move { tokio::spawn(async move {
stream_events_with_batching(&mut *store, &*target, cancel_rx, metrics, semaphore).await; stream_events_with_batching(&mut *store, &*target, cancel_rx, metrics, semaphore).await;
info!("Event stream stopped for target: {}", target.name()); info!("Event stream stopped for target: {}", target.name());
@@ -132,7 +122,7 @@ pub fn start_event_stream_with_batching(
cancel_tx cancel_tx
} }
/// 带批处理的事件流处理 /// Event stream processing with batch processing
pub async fn stream_events_with_batching( pub async fn stream_events_with_batching(
store: &mut (dyn Store<Event, Error = StoreError, Key = Key> + Send), store: &mut (dyn Store<Event, Error = StoreError, Key = Key> + Send),
target: &dyn Target, target: &dyn Target,
@@ -140,10 +130,7 @@ pub async fn stream_events_with_batching(
metrics: Arc<NotificationMetrics>, metrics: Arc<NotificationMetrics>,
semaphore: Arc<Semaphore>, semaphore: Arc<Semaphore>,
) { ) {
info!( info!("Starting event stream with batching for target: {}", target.name());
"Starting event stream with batching for target: {}",
target.name()
);
// Configuration parameters // Configuration parameters
const DEFAULT_BATCH_SIZE: usize = 1; const DEFAULT_BATCH_SIZE: usize = 1;
@@ -160,106 +147,64 @@ pub async fn stream_events_with_batching(
let mut last_flush = Instant::now(); let mut last_flush = Instant::now();
loop { loop {
// 检查取消信号 // Check the cancel signal
if cancel_rx.try_recv().is_ok() { if cancel_rx.try_recv().is_ok() {
info!("Cancellation received for target: {}", target.name()); info!("Cancellation received for target: {}", target.name());
return; return;
} }
// 获取存储中的事件列表 // Get a list of events in storage
let keys = store.list(); let keys = store.list();
debug!( debug!("Found {} keys in store for target: {}", keys.len(), target.name());
"Found {} keys in store for target: {}",
keys.len(),
target.name()
);
if keys.is_empty() { if keys.is_empty() {
// 如果批处理中有数据且超时,则刷新批处理 // If there is data in the batch and timeout, refresh the batch
if !batch.is_empty() && last_flush.elapsed() >= BATCH_TIMEOUT { if !batch.is_empty() && last_flush.elapsed() >= BATCH_TIMEOUT {
process_batch( process_batch(&mut batch, &mut batch_keys, target, MAX_RETRIES, BASE_RETRY_DELAY, &metrics, &semaphore).await;
&mut batch,
&mut batch_keys,
target,
MAX_RETRIES,
BASE_RETRY_DELAY,
&metrics,
&semaphore,
)
.await;
last_flush = Instant::now(); last_flush = Instant::now();
} }
// 无事件,等待后再检查 // No event, wait before checking
tokio::time::sleep(Duration::from_millis(500)).await; tokio::time::sleep(Duration::from_millis(500)).await;
continue; continue;
} }
// 处理每个事件 // Handle each event
for key in keys { for key in keys {
// 再次检查取消信号 // Check the cancel signal again
if cancel_rx.try_recv().is_ok() { if cancel_rx.try_recv().is_ok() {
info!( info!("Cancellation received during processing for target: {}", target.name());
"Cancellation received during processing for target: {}",
target.name()
);
// 在退出前处理已收集的批次 // Processing collected batches before exiting
if !batch.is_empty() { if !batch.is_empty() {
process_batch( process_batch(&mut batch, &mut batch_keys, target, MAX_RETRIES, BASE_RETRY_DELAY, &metrics, &semaphore).await;
&mut batch,
&mut batch_keys,
target,
MAX_RETRIES,
BASE_RETRY_DELAY,
&metrics,
&semaphore,
)
.await;
} }
return; return;
} }
// 尝试从存储中获取事件 // Try to get events from storage
match store.get(&key) { match store.get(&key) {
Ok(event) => { Ok(event) => {
// 添加到批处理 // Add to batch
batch.push(event); batch.push(event);
batch_keys.push(key); batch_keys.push(key);
metrics.increment_processing(); metrics.increment_processing();
// 如果批次已满或距离上次刷新已经过了足够时间,则处理批次 // If the batch is full or enough time has passed since the last refresh, the batch will be processed
if batch.len() >= batch_size || last_flush.elapsed() >= BATCH_TIMEOUT { if batch.len() >= batch_size || last_flush.elapsed() >= BATCH_TIMEOUT {
process_batch( process_batch(&mut batch, &mut batch_keys, target, MAX_RETRIES, BASE_RETRY_DELAY, &metrics, &semaphore)
&mut batch, .await;
&mut batch_keys,
target,
MAX_RETRIES,
BASE_RETRY_DELAY,
&metrics,
&semaphore,
)
.await;
last_flush = Instant::now(); last_flush = Instant::now();
} }
} }
Err(e) => { Err(e) => {
error!( error!("Failed to target: {}, get event {} from store: {}", target.name(), key.to_string(), e);
"Failed to target: {}, get event {} from store: {}", // Consider deleting unreadable events to prevent infinite loops from trying to read
target.name(),
key.to_string(),
e
);
// 可以考虑删除无法读取的事件,防止无限循环尝试读取
match store.del(&key) { match store.del(&key) {
Ok(_) => { Ok(_) => {
info!("Deleted corrupted event {} from store", key.to_string()); info!("Deleted corrupted event {} from store", key.to_string());
} }
Err(del_err) => { Err(del_err) => {
error!( error!("Failed to delete corrupted event {}: {}", key.to_string(), del_err);
"Failed to delete corrupted event {}: {}",
key.to_string(),
del_err
);
} }
} }
@@ -268,12 +213,12 @@ pub async fn stream_events_with_batching(
} }
} }
// 小延迟再进行下一轮检查 // A small delay will be conducted to check the next round
tokio::time::sleep(Duration::from_millis(100)).await; tokio::time::sleep(Duration::from_millis(100)).await;
} }
} }
/// 处理事件批次 /// Processing event batches
async fn process_batch( async fn process_batch(
batch: &mut Vec<Event>, batch: &mut Vec<Event>,
batch_keys: &mut Vec<Key>, batch_keys: &mut Vec<Key>,
@@ -283,16 +228,12 @@ async fn process_batch(
metrics: &Arc<NotificationMetrics>, metrics: &Arc<NotificationMetrics>,
semaphore: &Arc<Semaphore>, semaphore: &Arc<Semaphore>,
) { ) {
debug!( debug!("Processing batch of {} events for target: {}", batch.len(), target.name());
"Processing batch of {} events for target: {}",
batch.len(),
target.name()
);
if batch.is_empty() { if batch.is_empty() {
return; return;
} }
// 获取信号量许可,限制并发 // Obtain semaphore permission to limit concurrency
let permit = match semaphore.clone().acquire_owned().await { let permit = match semaphore.clone().acquire_owned().await {
Ok(permit) => permit, Ok(permit) => permit,
Err(e) => { Err(e) => {
@@ -301,30 +242,26 @@ async fn process_batch(
} }
}; };
// 处理批次中的每个事件 // Handle every event in the batch
for (_event, key) in batch.iter().zip(batch_keys.iter()) { for (_event, key) in batch.iter().zip(batch_keys.iter()) {
let mut retry_count = 0; let mut retry_count = 0;
let mut success = false; let mut success = false;
// 重试逻辑 // Retry logic
while retry_count < max_retries && !success { while retry_count < max_retries && !success {
match target.send_from_store(key.clone()).await { match target.send_from_store(key.clone()).await {
Ok(_) => { Ok(_) => {
info!( info!("Successfully sent event for target: {}, Key: {}", target.name(), key.to_string());
"Successfully sent event for target: {}, Key: {}",
target.name(),
key.to_string()
);
success = true; success = true;
metrics.increment_processed(); metrics.increment_processed();
} }
Err(e) => { Err(e) => {
// 根据错误类型采用不同的重试策略 // Different retry strategies are adopted according to the error type
match &e { match &e {
TargetError::NotConnected => { TargetError::NotConnected => {
warn!("Target {} not connected, retrying...", target.name()); warn!("Target {} not connected, retrying...", target.name());
retry_count += 1; retry_count += 1;
tokio::time::sleep(base_delay * (1 << retry_count)).await; // 指数退避 tokio::time::sleep(base_delay * (1 << retry_count)).await; // Exponential backoff
} }
TargetError::Timeout(_) => { TargetError::Timeout(_) => {
warn!("Timeout for target {}, retrying...", target.name()); warn!("Timeout for target {}, retrying...", target.name());
@@ -332,7 +269,7 @@ async fn process_batch(
tokio::time::sleep(base_delay * (1 << retry_count)).await; tokio::time::sleep(base_delay * (1 << retry_count)).await;
} }
_ => { _ => {
// 永久性错误,跳过此事件 // Permanent error, skip this event
error!("Permanent error for target {}: {}", target.name(), e); error!("Permanent error for target {}: {}", target.name(), e);
metrics.increment_failed(); metrics.increment_failed();
break; break;
@@ -342,21 +279,17 @@ async fn process_batch(
} }
} }
// 处理最大重试次数耗尽的情况 // Handle the situation where the maximum number of retry exhaustion is exhausted
if retry_count >= max_retries && !success { if retry_count >= max_retries && !success {
warn!( warn!("Max retries exceeded for event {}, target: {}, skipping", key.to_string(), target.name());
"Max retries exceeded for event {}, target: {}, skipping",
key.to_string(),
target.name()
);
metrics.increment_failed(); metrics.increment_failed();
} }
} }
// 清空已处理的批次 // Clear processed batches
batch.clear(); batch.clear();
batch_keys.clear(); batch_keys.clear();
// 释放信号量许可(通过 drop // Release semaphore permission (via drop)
drop(permit); drop(permit);
} }
+10 -10
View File
@@ -1,22 +1,22 @@
#[allow(dead_code)] #[allow(dead_code)]
const NOTIFY_KAFKA_SUB_SYS: &str = "notify_kafka"; pub const NOTIFY_KAFKA_SUB_SYS: &str = "notify_kafka";
#[allow(dead_code)] #[allow(dead_code)]
const NOTIFY_MQTT_SUB_SYS: &str = "notify_mqtt"; pub const NOTIFY_MQTT_SUB_SYS: &str = "notify_mqtt";
#[allow(dead_code)] #[allow(dead_code)]
const NOTIFY_MY_SQL_SUB_SYS: &str = "notify_mysql"; pub const NOTIFY_MY_SQL_SUB_SYS: &str = "notify_mysql";
#[allow(dead_code)] #[allow(dead_code)]
const NOTIFY_NATS_SUB_SYS: &str = "notify_nats"; pub const NOTIFY_NATS_SUB_SYS: &str = "notify_nats";
#[allow(dead_code)] #[allow(dead_code)]
const NOTIFY_NSQ_SUB_SYS: &str = "notify_nsq"; pub const NOTIFY_NSQ_SUB_SYS: &str = "notify_nsq";
#[allow(dead_code)] #[allow(dead_code)]
const NOTIFY_ES_SUB_SYS: &str = "notify_elasticsearch"; pub const NOTIFY_ES_SUB_SYS: &str = "notify_elasticsearch";
#[allow(dead_code)] #[allow(dead_code)]
const NOTIFY_AMQP_SUB_SYS: &str = "notify_amqp"; pub const NOTIFY_AMQP_SUB_SYS: &str = "notify_amqp";
#[allow(dead_code)] #[allow(dead_code)]
const NOTIFY_POSTGRES_SUB_SYS: &str = "notify_postgres"; pub const NOTIFY_POSTGRES_SUB_SYS: &str = "notify_postgres";
#[allow(dead_code)] #[allow(dead_code)]
const NOTIFY_REDIS_SUB_SYS: &str = "notify_redis"; pub const NOTIFY_REDIS_SUB_SYS: &str = "notify_redis";
const NOTIFY_WEBHOOK_SUB_SYS: &str = "notify_webhook"; pub const NOTIFY_WEBHOOK_SUB_SYS: &str = "notify_webhook";
// Webhook constants // Webhook constants
pub const WEBHOOK_ENDPOINT: &str = "endpoint"; pub const WEBHOOK_ENDPOINT: &str = "endpoint";
+54 -50
View File
@@ -1,17 +1,14 @@
use std::env;
use std::fmt; use std::fmt;
#[cfg(unix)] #[cfg(unix)]
use libc::uname; use std::os::unix::process::ExitStatusExt;
#[cfg(unix)]
use std::ffi::CStr;
#[cfg(windows)] #[cfg(windows)]
use std::process::Command; use std::os::windows::process::ExitStatusExt;
use std::{env, process};
// 定义 Rustfs 版本 // Define Rustfs version
const RUSTFS_VERSION: &str = "1.0.0"; const RUSTFS_VERSION: &str = "1.0.0";
// 业务类型枚举 // Business Type Enumeration
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum ServiceType { pub enum ServiceType {
Basis, Basis,
@@ -33,7 +30,7 @@ impl ServiceType {
} }
} }
// UserAgent 结构体 // UserAgent structure
struct UserAgent { struct UserAgent {
os_platform: String, os_platform: String,
arch: String, arch: String,
@@ -42,7 +39,7 @@ struct UserAgent {
} }
impl UserAgent { impl UserAgent {
// 创建新的 UserAgent 实例,接受业务类型参数 // Create a new UserAgent instance and accept business type parameters
fn new(service: ServiceType) -> Self { fn new(service: ServiceType) -> Self {
let os_platform = Self::get_os_platform(); let os_platform = Self::get_os_platform();
let arch = env::consts::ARCH.to_string(); let arch = env::consts::ARCH.to_string();
@@ -56,7 +53,7 @@ impl UserAgent {
} }
} }
// 获取操作系统平台信息 // Obtain operating system platform information
fn get_os_platform() -> String { fn get_os_platform() -> String {
if cfg!(target_os = "windows") { if cfg!(target_os = "windows") {
Self::get_windows_platform() Self::get_windows_platform()
@@ -69,14 +66,18 @@ impl UserAgent {
} }
} }
// 获取 Windows 平台信息 // Get Windows platform information
#[cfg(windows)] #[cfg(windows)]
fn get_windows_platform() -> String { fn get_windows_platform() -> String {
// 使用 cmd /c ver 获取版本 // Use cmd /c ver to get the version
let output = Command::new("cmd") let output = process::Command::new("cmd")
.args(&["/C", "ver"]) .args(&["/C", "ver"])
.output() .output()
.unwrap_or_default(); .unwrap_or_else(|_| process::Output {
status: process::ExitStatus::from_raw(0),
stdout: Vec::new(),
stderr: Vec::new(),
});
let version = String::from_utf8_lossy(&output.stdout); let version = String::from_utf8_lossy(&output.stdout);
let version = version let version = version
.lines() .lines()
@@ -92,27 +93,29 @@ impl UserAgent {
"N/A".to_string() "N/A".to_string()
} }
// 获取 macOS 平台信息 // Get macOS platform information
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
fn get_macos_platform() -> String { fn get_macos_platform() -> String {
unsafe { let output = process::Command::new("sw_vers")
let mut name = std::mem::zeroed(); .args(&["-productVersion"])
if uname(&mut name) == 0 { .output()
let release = CStr::from_ptr(name.release.as_ptr()).to_string_lossy(); .unwrap_or_else(|_| process::Output {
// 映射内核版本(如 23.5.0)到 User-Agent 格式(如 14_5_0 status: process::ExitStatus::from_raw(0),
let major = release stdout: Vec::new(),
.split('.') stderr: Vec::new(),
.next() });
.unwrap_or("14") let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
.parse::<i32>() let parts: Vec<&str> = version.split('.').collect();
.unwrap_or(14); let major = parts.get(0).unwrap_or(&"10").parse::<i32>().unwrap_or(10);
let minor = if major >= 20 { major - 9 } else { 14 }; let minor = parts.get(1).map_or("15", |&m| m);
let patch = release.split('.').nth(1).unwrap_or("0"); let patch = parts.get(2).map_or("0", |&p| p);
format!("Macintosh; Intel Mac OS X {}_{}_{}", minor, patch, 0)
} else { // Detect whether it is an Apple Silicon chip
"Macintosh; Intel Mac OS X 14_5_0".to_string() let arch = env::consts::ARCH;
} let cpu_info = if arch == "aarch64" { "Apple" } else { "Intel" };
}
// Convert to User-Agent format
format!("Macintosh; {} Mac OS X {}_{}_{}", cpu_info, major, minor, patch)
} }
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
@@ -120,17 +123,22 @@ impl UserAgent {
"N/A".to_string() "N/A".to_string()
} }
// 获取 Linux 平台信息 // Get Linux platform information
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn get_linux_platform() -> String { fn get_linux_platform() -> String {
unsafe { let output = process::Command::new("uname")
let mut name = std::mem::zeroed(); .arg("-r")
if uname(&mut name) == 0 { .output()
let release = CStr::from_ptr(name.release.as_ptr()).to_string_lossy(); .unwrap_or_else(|_| process::Output {
format!("X11; Linux {}", release) status: process::ExitStatus::from_raw(0),
} else { stdout: Vec::new(),
"X11; Linux Unknown".to_string() stderr: Vec::new(),
} });
if output.status.success() {
let release = String::from_utf8_lossy(&output.stdout).trim().to_string();
format!("X11; Linux {}", release)
} else {
"X11; Linux Unknown".to_string()
} }
} }
@@ -140,15 +148,11 @@ impl UserAgent {
} }
} }
// 实现 Display trait 以格式化 User-Agent // Implement Display trait to format User-Agent
impl fmt::Display for UserAgent { impl fmt::Display for UserAgent {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.service == ServiceType::Basis { if self.service == ServiceType::Basis {
return write!( return write!(f, "Mozilla/5.0 ({}; {}) Rustfs/{}", self.os_platform, self.arch, self.version);
f,
"Mozilla/5.0 ({}; {}) Rustfs/{}",
self.os_platform, self.arch, self.version
);
} }
write!( write!(
f, f,
@@ -161,7 +165,7 @@ impl fmt::Display for UserAgent {
} }
} }
// 获取 User-Agent 字符串,接受业务类型参数 // Get the User-Agent string and accept business type parameters
pub fn get_user_agent(service: ServiceType) -> String { pub fn get_user_agent(service: ServiceType) -> String {
UserAgent::new(service).to_string() UserAgent::new(service).to_string()
} }
+60
View File
@@ -0,0 +1,60 @@
use std::env;
use std::path::{Path, PathBuf};
/// Get the absolute path to the current project
///
/// This function will try the following method to get the project path:
/// 1. Use the `CARGO_MANIFEST_DIR` environment variable to get the project root directory.
/// 2. Use `std::env::current_exe()` to get the executable file path and deduce the project root directory.
/// 3. Use `std::env::current_dir()` to get the current working directory and try to deduce the project root directory.
///
/// If all methods fail, an error is returned.
///
/// # Returns
/// - `Ok(PathBuf)`: The absolute path of the project that was successfully obtained.
/// - `Err(String)`: Error message for the failed path.
pub fn get_project_root() -> Result<PathBuf, String> {
// Try to get the project root directory through the CARGO_MANIFEST_DIR environment variable
if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
let project_root = Path::new(&manifest_dir).to_path_buf();
println!("Get the project root directory with CARGO_MANIFEST_DIR:{}", project_root.display());
return Ok(project_root);
}
// Try to deduce the project root directory through the current executable file path
if let Ok(current_exe) = env::current_exe() {
let mut project_root = current_exe;
// Assume that the project root directory is in the parent directory of the parent directory of the executable path (usually target/debug or target/release)
project_root.pop(); // Remove the executable file name
project_root.pop(); // Remove target/debug or target/release
println!("Deduce the project root directory through current_exe:{}", project_root.display());
return Ok(project_root);
}
// Try to deduce the project root directory from the current working directory
if let Ok(mut current_dir) = env::current_dir() {
// Assume that the project root directory is in the parent directory of the current working directory
current_dir.pop();
println!("Deduce the project root directory through current_dir:{}", current_dir.display());
return Ok(current_dir);
}
// If all methods fail, return an error
Err("The project root directory cannot be obtained. Please check the running environment and project structure.".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_project_root() {
match get_project_root() {
Ok(path) => {
assert!(path.exists(), "The project root directory does not exist:{}", path.display());
println!("The test is passed, the project root directory:{}", path.display());
}
Err(e) => panic!("Failed to get the project root directory:{}", e),
}
}
}
+3
View File
@@ -28,6 +28,9 @@ pub mod crypto;
#[cfg(feature = "compress")] #[cfg(feature = "compress")]
pub mod compress; pub mod compress;
#[cfg(feature = "path")]
pub mod dirs;
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
pub use certs::*; pub use certs::*;
#[cfg(feature = "hash")] #[cfg(feature = "hash")]
-1
View File
@@ -8,7 +8,6 @@ use rustfs_utils::path::SLASH_SEPARATOR;
use std::collections::HashSet; use std::collections::HashSet;
use std::sync::Arc; use std::sync::Arc;
use tracing::{error, warn}; use tracing::{error, warn};
use crate::disk::fs::SLASH_SEPARATOR;
pub const CONFIG_PREFIX: &str = "config"; pub const CONFIG_PREFIX: &str = "config";
const CONFIG_FILE: &str = "config.json"; const CONFIG_FILE: &str = "config.json";
+12 -27
View File
@@ -1,9 +1,8 @@
use core::slice::SlicePattern;
use crate::bitrot::{create_bitrot_reader, create_bitrot_writer}; use crate::bitrot::{create_bitrot_reader, create_bitrot_writer};
use crate::disk::error_reduce::{reduce_read_quorum_errs, reduce_write_quorum_errs, OBJECT_OP_IGNORED_ERRS}; use crate::disk::error_reduce::{reduce_read_quorum_errs, reduce_write_quorum_errs, OBJECT_OP_IGNORED_ERRS};
use crate::disk::{ use crate::disk::{
self, conv_part_err_to_int, has_part_err, CHECK_PART_DISK_NOT_FOUND, CHECK_PART_FILE_CORRUPT, CHECK_PART_FILE_NOT_FOUND, self, conv_part_err_to_int, has_part_err, CHECK_PART_DISK_NOT_FOUND, CHECK_PART_FILE_CORRUPT,
CHECK_PART_SUCCESS, CHECK_PART_FILE_NOT_FOUND, CHECK_PART_SUCCESS,
}; };
use crate::erasure_coding; use crate::erasure_coding;
use crate::erasure_coding::bitrot_verify; use crate::erasure_coding::bitrot_verify;
@@ -17,8 +16,8 @@ use crate::{
config::{storageclass, GLOBAL_StorageClass}, config::{storageclass, GLOBAL_StorageClass},
disk::{ disk::{
endpoint::Endpoint, error::DiskError, format::FormatV3, new_disk, CheckPartsResp, DeleteOptions, DiskAPI, DiskInfo, endpoint::Endpoint, error::DiskError, format::FormatV3, new_disk, CheckPartsResp, DeleteOptions, DiskAPI, DiskInfo,
DiskInfoOptions, DiskOption, DiskStore, FileInfoVersions, ReadMultipleReq, ReadMultipleResp, ReadOptions, DiskInfoOptions, DiskOption, DiskStore, FileInfoVersions, ReadMultipleReq, ReadMultipleResp,
UpdateMetadataOpts, RUSTFS_META_BUCKET, RUSTFS_META_MULTIPART_BUCKET, RUSTFS_META_TMP_BUCKET, ReadOptions, UpdateMetadataOpts, RUSTFS_META_BUCKET, RUSTFS_META_MULTIPART_BUCKET, RUSTFS_META_TMP_BUCKET,
}, },
error::{to_object_err, StorageError}, error::{to_object_err, StorageError},
global::{ global::{
@@ -58,10 +57,10 @@ use md5::{Digest as Md5Digest, Md5};
use rand::{seq::SliceRandom, Rng}; use rand::{seq::SliceRandom, Rng};
use rustfs_filemeta::headers::RESERVED_METADATA_PREFIX_LOWER; use rustfs_filemeta::headers::RESERVED_METADATA_PREFIX_LOWER;
use rustfs_filemeta::{ use rustfs_filemeta::{
file_info_from_raw, file_info_from_raw, headers::{AMZ_OBJECT_TAGGING, AMZ_STORAGE_CLASS}, merge_file_meta_versions, FileInfo, FileMeta, FileMetaShallowVersion, MetaCacheEntries,
headers::{AMZ_OBJECT_TAGGING, AMZ_STORAGE_CLASS}, MetaCacheEntry, MetadataResolutionParams,
merge_file_meta_versions, FileInfo, FileMeta, FileMetaShallowVersion, MetaCacheEntries, MetaCacheEntry, ObjectPartInfo,
MetadataResolutionParams, ObjectPartInfo, RawFileInfo, RawFileInfo,
}; };
use rustfs_rio::{EtagResolvable, HashReader, TryGetIndex as _, WarpReader}; use rustfs_rio::{EtagResolvable, HashReader, TryGetIndex as _, WarpReader};
use rustfs_utils::{ use rustfs_utils::{
@@ -80,7 +79,6 @@ use std::{
sync::Arc, sync::Arc,
time::Duration, time::Duration,
}; };
use sha2::digest::HashReader;
use time::OffsetDateTime; use time::OffsetDateTime;
use tokio::{ use tokio::{
io::AsyncWrite, io::AsyncWrite,
@@ -95,7 +93,6 @@ use tracing::error;
use tracing::{debug, info, warn}; use tracing::{debug, info, warn};
use uuid::Uuid; use uuid::Uuid;
use workers::workers::Workers; use workers::workers::Workers;
use crate::disk::fs::SLASH_SEPARATOR;
pub const DEFAULT_READ_BUFFER_SIZE: usize = 1024 * 1024; pub const DEFAULT_READ_BUFFER_SIZE: usize = 1024 * 1024;
@@ -404,11 +401,7 @@ impl SetDisks {
} }
} }
if max >= write_quorum { if max >= write_quorum { data_dir } else { None }
data_dir
} else {
None
}
} }
#[allow(dead_code)] #[allow(dead_code)]
@@ -739,11 +732,7 @@ impl SetDisks {
fn common_time(times: &[Option<OffsetDateTime>], quorum: usize) -> Option<OffsetDateTime> { fn common_time(times: &[Option<OffsetDateTime>], quorum: usize) -> Option<OffsetDateTime> {
let (time, count) = Self::common_time_and_occurrence(times); let (time, count) = Self::common_time_and_occurrence(times);
if count >= quorum { if count >= quorum { time } else { None }
time
} else {
None
}
} }
fn common_time_and_occurrence(times: &[Option<OffsetDateTime>]) -> (Option<OffsetDateTime>, usize) { fn common_time_and_occurrence(times: &[Option<OffsetDateTime>]) -> (Option<OffsetDateTime>, usize) {
@@ -784,11 +773,7 @@ impl SetDisks {
fn common_etag(etags: &[Option<String>], quorum: usize) -> Option<String> { fn common_etag(etags: &[Option<String>], quorum: usize) -> Option<String> {
let (etag, count) = Self::common_etags(etags); let (etag, count) = Self::common_etags(etags);
if count >= quorum { if count >= quorum { etag } else { None }
etag
} else {
None
}
} }
fn common_etags(etags: &[Option<String>]) -> (Option<String>, usize) { fn common_etags(etags: &[Option<String>]) -> (Option<String>, usize) {
@@ -4069,7 +4054,7 @@ impl StorageAPI for SetDisks {
async fn local_storage_info(&self) -> madmin::StorageInfo { async fn local_storage_info(&self) -> madmin::StorageInfo {
let disks = self.get_disks_internal().await; let disks = self.get_disks_internal().await;
let mut local_disks: Vec<Option<Arc<crate::disk::Disk>>> = Vec::new(); let mut local_disks: Vec<Option<Arc<disk::Disk>>> = Vec::new();
let mut local_endpoints = Vec::new(); let mut local_endpoints = Vec::new();
for (i, ep) in self.set_endpoints.iter().enumerate() { for (i, ep) in self.set_endpoints.iter().enumerate() {
+1 -2
View File
@@ -18,7 +18,6 @@ use std::fmt::Debug;
use std::io::Cursor; use std::io::Cursor;
use std::str::FromStr as _; use std::str::FromStr as _;
use std::sync::Arc; use std::sync::Arc;
use sha2::digest::HashReader;
use time::OffsetDateTime; use time::OffsetDateTime;
use tokio::io::{AsyncRead, AsyncReadExt}; use tokio::io::{AsyncRead, AsyncReadExt};
use tracing::warn; use tracing::warn;
@@ -904,7 +903,7 @@ pub trait StorageAPI: ObjectIO {
opts: &HealOpts, opts: &HealOpts,
) -> Result<(HealResultItem, Option<Error>)>; ) -> Result<(HealResultItem, Option<Error>)>;
async fn heal_objects(&self, bucket: &str, prefix: &str, opts: &HealOpts, hs: Arc<HealSequence>, is_meta: bool) async fn heal_objects(&self, bucket: &str, prefix: &str, opts: &HealOpts, hs: Arc<HealSequence>, is_meta: bool)
-> Result<()>; -> Result<()>;
async fn get_pool_and_set(&self, id: &str) -> Result<(Option<usize>, Option<usize>, Option<usize>)>; async fn get_pool_and_set(&self, id: &str) -> Result<(Option<usize>, Option<usize>, Option<usize>)>;
async fn check_abandoned_parts(&self, bucket: &str, object: &str, opts: &HealOpts) -> Result<()>; async fn check_abandoned_parts(&self, bucket: &str, object: &str, opts: &HealOpts) -> Result<()>;
} }
+4 -21
View File
@@ -25,7 +25,6 @@ use tokio::sync::broadcast::{self, Receiver as B_Receiver};
use tokio::sync::mpsc::{self, Receiver, Sender}; use tokio::sync::mpsc::{self, Receiver, Sender};
use tracing::{error, warn}; use tracing::{error, warn};
use uuid::Uuid; use uuid::Uuid;
use crate::disk::fs::SLASH_SEPARATOR;
const MAX_OBJECT_LIST: i32 = 1000; const MAX_OBJECT_LIST: i32 = 1000;
// const MAX_DELETE_LIST: i32 = 1000; // const MAX_DELETE_LIST: i32 = 1000;
@@ -838,11 +837,7 @@ impl ECStore {
if fiter(&fi) { if fiter(&fi) {
let item = ObjectInfoOrErr { let item = ObjectInfoOrErr {
item: Some(ObjectInfo::from_file_info(&fi, &bucket, &fi.name, { item: Some(ObjectInfo::from_file_info(&fi, &bucket, &fi.name, {
if let Some(v) = &vcf { if let Some(v) = &vcf { v.versioned(&fi.name) } else { false }
v.versioned(&fi.name)
} else {
false
}
})), })),
err: None, err: None,
}; };
@@ -854,11 +849,7 @@ impl ECStore {
} else { } else {
let item = ObjectInfoOrErr { let item = ObjectInfoOrErr {
item: Some(ObjectInfo::from_file_info(&fi, &bucket, &fi.name, { item: Some(ObjectInfo::from_file_info(&fi, &bucket, &fi.name, {
if let Some(v) = &vcf { if let Some(v) = &vcf { v.versioned(&fi.name) } else { false }
v.versioned(&fi.name)
} else {
false
}
})), })),
err: None, err: None,
}; };
@@ -894,11 +885,7 @@ impl ECStore {
if fiter(fi) { if fiter(fi) {
let item = ObjectInfoOrErr { let item = ObjectInfoOrErr {
item: Some(ObjectInfo::from_file_info(fi, &bucket, &fi.name, { item: Some(ObjectInfo::from_file_info(fi, &bucket, &fi.name, {
if let Some(v) = &vcf { if let Some(v) = &vcf { v.versioned(&fi.name) } else { false }
v.versioned(&fi.name)
} else {
false
}
})), })),
err: None, err: None,
}; };
@@ -910,11 +897,7 @@ impl ECStore {
} else { } else {
let item = ObjectInfoOrErr { let item = ObjectInfoOrErr {
item: Some(ObjectInfo::from_file_info(fi, &bucket, &fi.name, { item: Some(ObjectInfo::from_file_info(fi, &bucket, &fi.name, {
if let Some(v) = &vcf { if let Some(v) = &vcf { v.versioned(&fi.name) } else { false }
v.versioned(&fi.name)
} else {
false
}
})), })),
err: None, err: None,
}; };