mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-08 06:13:14 +00:00
feat: improve legacy metadata and admin compatibility (#2202)
This commit is contained in:
+293
-27
@@ -13,6 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
collections::HashSet,
|
||||
env,
|
||||
sync::{Mutex, OnceLock},
|
||||
@@ -30,7 +31,7 @@ use tracing::warn;
|
||||
/// - `i8`: The parsed value as i8 if successful, otherwise the default value.
|
||||
///
|
||||
pub fn get_env_i8(key: &str, default: i8) -> i8 {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
parse_env_value(key).unwrap_or(default)
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
|
||||
@@ -43,7 +44,7 @@ pub fn get_env_i8(key: &str, default: i8) -> i8 {
|
||||
/// - `Option<i8>`: The parsed value as i8 if successful, otherwise None
|
||||
///
|
||||
pub fn get_env_opt_i8(key: &str) -> Option<i8> {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok())
|
||||
parse_env_value(key)
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
|
||||
@@ -57,7 +58,7 @@ pub fn get_env_opt_i8(key: &str) -> Option<i8> {
|
||||
/// - `u8`: The parsed value as u8 if successful, otherwise the default value.
|
||||
///
|
||||
pub fn get_env_u8(key: &str, default: u8) -> u8 {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
parse_env_value(key).unwrap_or(default)
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
|
||||
@@ -70,7 +71,7 @@ pub fn get_env_u8(key: &str, default: u8) -> u8 {
|
||||
/// - `Option<u8>`: The parsed value as u8 if successful, otherwise None
|
||||
///
|
||||
pub fn get_env_opt_u8(key: &str) -> Option<u8> {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok())
|
||||
parse_env_value(key)
|
||||
}
|
||||
|
||||
static WARNED_ENV_MESSAGES: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
|
||||
@@ -86,19 +87,182 @@ fn log_once(key: &str, message: impl FnOnce() -> String) {
|
||||
}
|
||||
}
|
||||
|
||||
fn external_alias_for_key(key: &str) -> Option<String> {
|
||||
let suffix = key.strip_prefix("RUSTFS_")?;
|
||||
if is_external_compatible_suffix(suffix) {
|
||||
Some(format!("{}{}", external_env_prefix(), suffix))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_env_with_aliases(key: &str, deprecated: &[&str]) -> Option<(String, String)> {
|
||||
if let Ok(value) = env::var(key) {
|
||||
return Some((key.to_string(), value));
|
||||
}
|
||||
|
||||
let (alias, value) = deprecated
|
||||
if let Some((alias, value)) = deprecated
|
||||
.iter()
|
||||
.find_map(|alias| env::var(alias).ok().map(|value| (*alias, value)))?;
|
||||
.find_map(|alias| env::var(alias).ok().map(|value| (*alias, value)))
|
||||
{
|
||||
let deprecated_key = format!("env_alias:{alias}->{key}");
|
||||
log_once(&deprecated_key, || {
|
||||
format!("Environment variable {alias} is deprecated, use {key} instead")
|
||||
});
|
||||
return Some((alias.to_string(), value));
|
||||
}
|
||||
|
||||
let alias = external_alias_for_key(key)?;
|
||||
let value = env::var(&alias).ok()?;
|
||||
let deprecated_key = format!("env_alias:{alias}->{key}");
|
||||
log_once(&deprecated_key, || {
|
||||
format!("Environment variable {alias} is deprecated, use {key} instead")
|
||||
});
|
||||
Some((alias.to_string(), value))
|
||||
Some((alias, value))
|
||||
}
|
||||
|
||||
const EXTERNAL_ENV_PREFIX_BYTES: [u8; 6] = [77, 73, 78, 73, 79, 95];
|
||||
|
||||
const EXTERNAL_COMPATIBLE_SUFFIXES: &[&str] = &[
|
||||
"ACCESS_KEY",
|
||||
"ACCESS_KEY_FILE",
|
||||
"ADDRESS",
|
||||
"API_XFF_HEADER",
|
||||
"AUDIT_WEBHOOK_AUTH_TOKEN",
|
||||
"AUDIT_WEBHOOK_CLIENT_CERT",
|
||||
"AUDIT_WEBHOOK_CLIENT_KEY",
|
||||
"AUDIT_WEBHOOK_ENABLE",
|
||||
"AUDIT_WEBHOOK_ENDPOINT",
|
||||
"AUDIT_WEBHOOK_QUEUE_DIR",
|
||||
"COMPRESS_ENABLE",
|
||||
"COMPRESS_EXTENSIONS",
|
||||
"COMPRESS_MIME_TYPES",
|
||||
"CONSOLE_ADDRESS",
|
||||
"DRIVE_ACTIVE_MONITORING",
|
||||
"ERASURE_SET_DRIVE_COUNT",
|
||||
"IDENTITY_OPENID_CLAIM_NAME",
|
||||
"IDENTITY_OPENID_CLAIM_PREFIX",
|
||||
"IDENTITY_OPENID_CLIENT_ID",
|
||||
"IDENTITY_OPENID_CLIENT_SECRET",
|
||||
"IDENTITY_OPENID_CONFIG_URL",
|
||||
"IDENTITY_OPENID_DISPLAY_NAME",
|
||||
"IDENTITY_OPENID_REDIRECT_URI",
|
||||
"IDENTITY_OPENID_SCOPES",
|
||||
"ILM_EXPIRATION_WORKERS",
|
||||
"LICENSE",
|
||||
"NOTIFY_MQTT_BROKER",
|
||||
"NOTIFY_MQTT_ENABLE",
|
||||
"NOTIFY_MQTT_KEEP_ALIVE_INTERVAL",
|
||||
"NOTIFY_MQTT_PASSWORD",
|
||||
"NOTIFY_MQTT_QOS",
|
||||
"NOTIFY_MQTT_QUEUE_DIR",
|
||||
"NOTIFY_MQTT_QUEUE_LIMIT",
|
||||
"NOTIFY_MQTT_RECONNECT_INTERVAL",
|
||||
"NOTIFY_MQTT_TOPIC",
|
||||
"NOTIFY_MQTT_USERNAME",
|
||||
"NOTIFY_WEBHOOK_AUTH_TOKEN",
|
||||
"NOTIFY_WEBHOOK_CLIENT_CERT",
|
||||
"NOTIFY_WEBHOOK_CLIENT_KEY",
|
||||
"NOTIFY_WEBHOOK_ENABLE",
|
||||
"NOTIFY_WEBHOOK_ENDPOINT",
|
||||
"NOTIFY_WEBHOOK_QUEUE_DIR",
|
||||
"NOTIFY_WEBHOOK_QUEUE_LIMIT",
|
||||
"POLICY_PLUGIN_AUTH_TOKEN",
|
||||
"POLICY_PLUGIN_URL",
|
||||
"PORT",
|
||||
"REGION",
|
||||
"ROOT_PASSWORD",
|
||||
"ROOT_USER",
|
||||
"SECRET_KEY",
|
||||
"SECRET_KEY_FILE",
|
||||
"STORAGE_CLASS_INLINE_BLOCK",
|
||||
"STORAGE_CLASS_OPTIMIZE",
|
||||
"STORAGE_CLASS_RRS",
|
||||
"STORAGE_CLASS_STANDARD",
|
||||
"VERSION",
|
||||
"VOLUMES",
|
||||
];
|
||||
|
||||
const EXTERNAL_DYNAMIC_COMPATIBLE_PREFIXES: &[&str] = &["AUDIT_MQTT_", "AUDIT_WEBHOOK_", "NOTIFY_MQTT_", "NOTIFY_WEBHOOK_"];
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct ExternalEnvCompatReport {
|
||||
pub mapped_pairs: Vec<(String, String)>,
|
||||
pub conflict_keys: Vec<String>,
|
||||
}
|
||||
|
||||
impl ExternalEnvCompatReport {
|
||||
pub fn mapped_count(&self) -> usize {
|
||||
self.mapped_pairs.len()
|
||||
}
|
||||
|
||||
pub fn conflict_count(&self) -> usize {
|
||||
self.conflict_keys.len()
|
||||
}
|
||||
}
|
||||
|
||||
fn external_env_prefix() -> &'static str {
|
||||
static PREFIX: OnceLock<String> = OnceLock::new();
|
||||
PREFIX
|
||||
.get_or_init(|| EXTERNAL_ENV_PREFIX_BYTES.iter().map(|&byte| char::from(byte)).collect())
|
||||
.as_str()
|
||||
}
|
||||
|
||||
fn is_external_compatible_suffix(suffix: &str) -> bool {
|
||||
EXTERNAL_COMPATIBLE_SUFFIXES.contains(&suffix)
|
||||
|| EXTERNAL_DYNAMIC_COMPATIBLE_PREFIXES
|
||||
.iter()
|
||||
.any(|prefix| suffix.starts_with(prefix))
|
||||
}
|
||||
|
||||
fn build_external_env_compat_report_from_entries<I>(entries: I) -> ExternalEnvCompatReport
|
||||
where
|
||||
I: IntoIterator<Item = (String, String)>,
|
||||
{
|
||||
let env_map: std::collections::BTreeMap<String, String> = entries.into_iter().collect();
|
||||
let mut mapped_pairs = BTreeSet::new();
|
||||
let mut conflict_keys = BTreeSet::new();
|
||||
let source_prefix = external_env_prefix();
|
||||
|
||||
for (source_key, source_value) in env_map.iter() {
|
||||
let Some(suffix) = source_key.strip_prefix(source_prefix) else {
|
||||
continue;
|
||||
};
|
||||
if !is_external_compatible_suffix(suffix) {
|
||||
continue;
|
||||
}
|
||||
let rustfs_key = format!("RUSTFS_{suffix}");
|
||||
match env_map.get(&rustfs_key) {
|
||||
None => {
|
||||
mapped_pairs.insert((source_key.clone(), rustfs_key));
|
||||
}
|
||||
Some(rustfs_value) if rustfs_value != source_value => {
|
||||
conflict_keys.insert(rustfs_key);
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
ExternalEnvCompatReport {
|
||||
mapped_pairs: mapped_pairs.into_iter().collect(),
|
||||
conflict_keys: conflict_keys.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build compatibility plan between source-prefixed variables and `RUSTFS_*`.
|
||||
///
|
||||
/// Precedence rule:
|
||||
/// - If both `RUSTFS_*` and source-prefixed variables exist, keep `RUSTFS_*` and record a conflict.
|
||||
/// - If only source-prefixed variables exist, mark them as mappable to `RUSTFS_*`.
|
||||
pub fn build_external_env_compat_report() -> ExternalEnvCompatReport {
|
||||
build_external_env_compat_report_from_entries(env::vars())
|
||||
}
|
||||
|
||||
fn parse_env_value<T>(key: &str) -> Option<T>
|
||||
where
|
||||
T: std::str::FromStr,
|
||||
{
|
||||
resolve_env_with_aliases(key, &[]).and_then(|(_, value)| value.parse().ok())
|
||||
}
|
||||
|
||||
pub fn get_env_str_with_aliases(key: &str, deprecated: &[&str], default: &str) -> String {
|
||||
@@ -134,7 +298,7 @@ pub fn get_env_bool_with_aliases(key: &str, deprecated: &[&str], default: bool)
|
||||
/// - `i16`: The parsed value as i16 if successful, otherwise the default value.
|
||||
///
|
||||
pub fn get_env_i16(key: &str, default: i16) -> i16 {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
parse_env_value(key).unwrap_or(default)
|
||||
}
|
||||
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
|
||||
/// 16-bit type: signed i16
|
||||
@@ -146,7 +310,7 @@ pub fn get_env_i16(key: &str, default: i16) -> i16 {
|
||||
/// - `Option<i16>`: The parsed value as i16 if successful, otherwise None
|
||||
///
|
||||
pub fn get_env_opt_i16(key: &str) -> Option<i16> {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok())
|
||||
parse_env_value(key)
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
|
||||
@@ -160,7 +324,7 @@ pub fn get_env_opt_i16(key: &str) -> Option<i16> {
|
||||
/// - `u16`: The parsed value as u16 if successful, otherwise the default value.
|
||||
///
|
||||
pub fn get_env_u16(key: &str, default: u16) -> u16 {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
parse_env_value(key).unwrap_or(default)
|
||||
}
|
||||
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
|
||||
/// 16-bit type: unsigned u16
|
||||
@@ -172,7 +336,7 @@ pub fn get_env_u16(key: &str, default: u16) -> u16 {
|
||||
/// - `Option<u16>`: The parsed value as u16 if successful, otherwise None
|
||||
///
|
||||
pub fn get_env_u16_opt(key: &str) -> Option<u16> {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok())
|
||||
parse_env_value(key)
|
||||
}
|
||||
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
|
||||
/// 16-bit type: unsigned u16
|
||||
@@ -197,7 +361,7 @@ pub fn get_env_opt_u16(key: &str) -> Option<u16> {
|
||||
/// - `i32`: The parsed value as i32 if successful, otherwise the default value.
|
||||
///
|
||||
pub fn get_env_i32(key: &str, default: i32) -> i32 {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
parse_env_value(key).unwrap_or(default)
|
||||
}
|
||||
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
|
||||
/// 32-bit type: signed i32
|
||||
@@ -209,7 +373,7 @@ pub fn get_env_i32(key: &str, default: i32) -> i32 {
|
||||
/// - `Option<i32>`: The parsed value as i32 if successful, otherwise None
|
||||
///
|
||||
pub fn get_env_opt_i32(key: &str) -> Option<i32> {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok())
|
||||
parse_env_value(key)
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
|
||||
@@ -223,7 +387,7 @@ pub fn get_env_opt_i32(key: &str) -> Option<i32> {
|
||||
/// - `u32`: The parsed value as u32 if successful, otherwise the default value.
|
||||
///
|
||||
pub fn get_env_u32(key: &str, default: u32) -> u32 {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
parse_env_value(key).unwrap_or(default)
|
||||
}
|
||||
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
|
||||
/// 32-bit type: unsigned u32
|
||||
@@ -235,7 +399,7 @@ pub fn get_env_u32(key: &str, default: u32) -> u32 {
|
||||
/// - `Option<u32>`: The parsed value as u32 if successful, otherwise None
|
||||
///
|
||||
pub fn get_env_opt_u32(key: &str) -> Option<u32> {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok())
|
||||
parse_env_value(key)
|
||||
}
|
||||
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
|
||||
///
|
||||
@@ -247,7 +411,7 @@ pub fn get_env_opt_u32(key: &str) -> Option<u32> {
|
||||
/// - `f32`: The parsed value as f32 if successful, otherwise the default value
|
||||
///
|
||||
pub fn get_env_f32(key: &str, default: f32) -> f32 {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
parse_env_value(key).unwrap_or(default)
|
||||
}
|
||||
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
|
||||
///
|
||||
@@ -258,7 +422,7 @@ pub fn get_env_f32(key: &str, default: f32) -> f32 {
|
||||
/// - `Option<f32>`: The parsed value as f32 if successful, otherwise None
|
||||
///
|
||||
pub fn get_env_opt_f32(key: &str) -> Option<f32> {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok())
|
||||
parse_env_value(key)
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
|
||||
@@ -271,7 +435,7 @@ pub fn get_env_opt_f32(key: &str) -> Option<f32> {
|
||||
/// - `i64`: The parsed value as i64 if successful, otherwise the default value
|
||||
///
|
||||
pub fn get_env_i64(key: &str, default: i64) -> i64 {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
parse_env_value(key).unwrap_or(default)
|
||||
}
|
||||
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
|
||||
///
|
||||
@@ -282,7 +446,7 @@ pub fn get_env_i64(key: &str, default: i64) -> i64 {
|
||||
/// - `Option<i64>`: The parsed value as i64 if successful, otherwise None
|
||||
///
|
||||
pub fn get_env_opt_i64(key: &str) -> Option<i64> {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok())
|
||||
parse_env_value(key)
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as a specific type, returning Option<Option<i64>> if not set or parsing fails.
|
||||
@@ -294,7 +458,7 @@ pub fn get_env_opt_i64(key: &str) -> Option<i64> {
|
||||
/// - `Option<Option<i64>>`: The parsed value as i64 if successful, otherwise None
|
||||
///
|
||||
pub fn get_env_opt_opt_i64(key: &str) -> Option<Option<i64>> {
|
||||
env::var(key).ok().map(|v| v.parse().ok())
|
||||
resolve_env_with_aliases(key, &[]).map(|(_, value)| value.parse().ok())
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
|
||||
@@ -307,7 +471,7 @@ pub fn get_env_opt_opt_i64(key: &str) -> Option<Option<i64>> {
|
||||
/// - `u64`: The parsed value as u64 if successful, otherwise the default value.
|
||||
///
|
||||
pub fn get_env_u64(key: &str, default: u64) -> u64 {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
parse_env_value(key).unwrap_or(default)
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as an unsigned 64-bit integer, returning `None` if not set or parsing fails.
|
||||
@@ -341,7 +505,7 @@ pub fn get_env_opt_u64_with_aliases(key: &str, deprecated: &[&str]) -> Option<u6
|
||||
/// - `Option<u64>`: The parsed value as u64 if successful, otherwise None
|
||||
///
|
||||
pub fn get_env_opt_u64(key: &str) -> Option<u64> {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok())
|
||||
parse_env_value(key)
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
|
||||
@@ -354,7 +518,7 @@ pub fn get_env_opt_u64(key: &str) -> Option<u64> {
|
||||
/// - `f64`: The parsed value as f64 if successful, otherwise the default value.
|
||||
///
|
||||
pub fn get_env_f64(key: &str, default: f64) -> f64 {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
parse_env_value(key).unwrap_or(default)
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
|
||||
@@ -366,7 +530,7 @@ pub fn get_env_f64(key: &str, default: f64) -> f64 {
|
||||
/// - `Option<f64>`: The parsed value as f64 if successful, otherwise None
|
||||
///
|
||||
pub fn get_env_opt_f64(key: &str) -> Option<f64> {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok())
|
||||
parse_env_value(key)
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
|
||||
@@ -379,7 +543,7 @@ pub fn get_env_opt_f64(key: &str) -> Option<f64> {
|
||||
/// - `usize`: The parsed value as usize if successful, otherwise the default value.
|
||||
///
|
||||
pub fn get_env_usize(key: &str, default: usize) -> usize {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
parse_env_value(key).unwrap_or(default)
|
||||
}
|
||||
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
|
||||
///
|
||||
@@ -390,7 +554,7 @@ pub fn get_env_usize(key: &str, default: usize) -> usize {
|
||||
/// - `Option<usize>`: The parsed value as usize if successful, otherwise None
|
||||
///
|
||||
pub fn get_env_usize_opt(key: &str) -> Option<usize> {
|
||||
env::var(key).ok().and_then(|v| v.parse().ok())
|
||||
parse_env_value(key)
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
|
||||
@@ -427,7 +591,7 @@ pub fn get_env_str(key: &str, default: &str) -> String {
|
||||
/// - `Option<String>`: The environment variable value if set, otherwise None.
|
||||
///
|
||||
pub fn get_env_opt_str(key: &str) -> Option<String> {
|
||||
env::var(key).ok()
|
||||
resolve_env_with_aliases(key, &[]).map(|(_, value)| value)
|
||||
}
|
||||
|
||||
/// Retrieve an environment variable as a boolean, with a default value if not set or parsing fails.
|
||||
@@ -476,3 +640,105 @@ pub fn get_env_opt_bool(key: &str) -> Option<bool> {
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// Copy supported external-prefix variables such as `MINIO_*` into their
|
||||
/// canonical `RUSTFS_*` names in the current process when the canonical key is
|
||||
/// missing.
|
||||
#[allow(unsafe_code)]
|
||||
pub fn apply_external_env_compat() -> ExternalEnvCompatReport {
|
||||
let report = build_external_env_compat_report();
|
||||
for (source_key, rustfs_key) in &report.mapped_pairs {
|
||||
if let Ok(value) = env::var(source_key) {
|
||||
// Safety: this helper is intended for early startup bootstrap
|
||||
// before any background threads are created.
|
||||
unsafe {
|
||||
env::set_var(rustfs_key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
report
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{apply_external_env_compat, build_external_env_compat_report_from_entries, get_env_str};
|
||||
|
||||
fn source_key(suffix: &str) -> String {
|
||||
let mut key = super::external_env_prefix().to_string();
|
||||
key.push_str(suffix);
|
||||
key
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_value_is_mapped_when_rustfs_missing() {
|
||||
let report =
|
||||
build_external_env_compat_report_from_entries(vec![(source_key("STORAGE_CLASS_STANDARD"), "EC:2".to_string())]);
|
||||
assert_eq!(report.mapped_count(), 1);
|
||||
assert!(
|
||||
report
|
||||
.mapped_pairs
|
||||
.iter()
|
||||
.any(|(input_key, rustfs_key)| input_key == &source_key("STORAGE_CLASS_STANDARD")
|
||||
&& rustfs_key == "RUSTFS_STORAGE_CLASS_STANDARD")
|
||||
);
|
||||
assert_eq!(report.conflict_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rustfs_value_takes_precedence_on_conflict() {
|
||||
let report = build_external_env_compat_report_from_entries(vec![
|
||||
("RUSTFS_ERASURE_SET_DRIVE_COUNT".to_string(), "8".to_string()),
|
||||
(source_key("ERASURE_SET_DRIVE_COUNT"), "16".to_string()),
|
||||
]);
|
||||
assert_eq!(report.mapped_count(), 0);
|
||||
assert_eq!(report.conflict_count(), 1);
|
||||
assert!(report.conflict_keys.iter().any(|key| key == "RUSTFS_ERASURE_SET_DRIVE_COUNT"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynamic_notify_suffix_is_mapped() {
|
||||
let report =
|
||||
build_external_env_compat_report_from_entries(vec![(source_key("NOTIFY_WEBHOOK_ENABLE_PRIMARY"), "on".to_string())]);
|
||||
assert_eq!(report.mapped_count(), 1);
|
||||
assert!(
|
||||
report
|
||||
.mapped_pairs
|
||||
.iter()
|
||||
.any(|(input_key, rustfs_key)| input_key == &source_key("NOTIFY_WEBHOOK_ENABLE_PRIMARY")
|
||||
&& rustfs_key == "RUSTFS_NOTIFY_WEBHOOK_ENABLE_PRIMARY")
|
||||
);
|
||||
assert_eq!(report.conflict_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unrelated_source_key_is_ignored() {
|
||||
let report = build_external_env_compat_report_from_entries(vec![(source_key("UNKNOWN_COMPAT_TEST"), "1".to_string())]);
|
||||
assert_eq!(report.mapped_count(), 0);
|
||||
assert_eq!(report.conflict_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minio_alias_is_used_for_rustfs_reads() {
|
||||
temp_env::with_var("MINIO_ROOT_USER", Some("compat-admin"), || {
|
||||
temp_env::with_var_unset("RUSTFS_ROOT_USER", || {
|
||||
assert_eq!(get_env_str("RUSTFS_ROOT_USER", "default-user"), "compat-admin");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_external_env_compat_copies_missing_rustfs_keys() {
|
||||
temp_env::with_var("MINIO_ROOT_USER", Some("compat-admin"), || {
|
||||
temp_env::with_var_unset("RUSTFS_ROOT_USER", || {
|
||||
let report = apply_external_env_compat();
|
||||
assert!(
|
||||
report
|
||||
.mapped_pairs
|
||||
.iter()
|
||||
.any(|(source_key, rustfs_key)| source_key == "MINIO_ROOT_USER" && rustfs_key == "RUSTFS_ROOT_USER")
|
||||
);
|
||||
assert_eq!(std::env::var("RUSTFS_ROOT_USER").as_deref(), Ok("compat-admin"));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+101
-11
@@ -12,13 +12,30 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use blake2::{Blake2b512, Digest as Blake2Digest};
|
||||
use highway::{HighwayHash, HighwayHasher, Key};
|
||||
use md5::{Digest, Md5};
|
||||
use md5::Md5;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
|
||||
/// The fixed key for HighwayHash256. DO NOT change for compatibility.
|
||||
const HIGHWAY_HASH256_KEY: [u64; 4] = [3, 4, 2, 1];
|
||||
/// Magic HH-256 key: HH-256 hash of first 100 decimals of π as utf-8 with zero key.
|
||||
const MAGIC_HIGHWAY_HASH256_KEY: [u8; 32] = [
|
||||
0x4b, 0xe7, 0x34, 0xfa, 0x8e, 0x23, 0x8a, 0xcd, 0x26, 0x3e, 0x83, 0xe6, 0xbb, 0x96, 0x85, 0x52, 0x04, 0x0f, 0x93, 0x5d, 0xa3,
|
||||
0x9f, 0x44, 0x14, 0x97, 0xe0, 0x9d, 0x13, 0x22, 0xde, 0x36, 0xa0,
|
||||
];
|
||||
|
||||
/// Legacy HH-256 key (main branch): fixed [3,4,2,1] as u64 LE.
|
||||
const LEGACY_HIGHWAY_HASH256_KEY: [u8; 32] = [
|
||||
3, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
|
||||
];
|
||||
|
||||
fn highway_key_from_bytes(bytes: &[u8; 32]) -> [u64; 4] {
|
||||
let mut key = [0u64; 4];
|
||||
for (i, chunk) in bytes.chunks_exact(8).enumerate() {
|
||||
key[i] = u64::from_le_bytes(chunk.try_into().unwrap());
|
||||
}
|
||||
key
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Default, Clone, Eq, Hash)]
|
||||
/// Supported hash algorithms for bitrot protection.
|
||||
@@ -30,6 +47,8 @@ pub enum HashAlgorithm {
|
||||
// HighwayHash256S represents the Streaming HighwayHash-256 hash function
|
||||
#[default]
|
||||
HighwayHash256S,
|
||||
/// Legacy HighwayHash256S (main branch) with fixed key [3,4,2,1]
|
||||
HighwayHash256SLegacy,
|
||||
// BLAKE2b512 represents the BLAKE2b-512 hash function
|
||||
BLAKE2b512,
|
||||
/// MD5 (128-bit)
|
||||
@@ -43,7 +62,8 @@ enum HashEncoded {
|
||||
Sha256([u8; 32]),
|
||||
HighwayHash256([u8; 32]),
|
||||
HighwayHash256S([u8; 32]),
|
||||
Blake2b512(blake3::Hash),
|
||||
HighwayHash256SLegacy([u8; 32]),
|
||||
Blake2b512([u8; 64]),
|
||||
None,
|
||||
}
|
||||
|
||||
@@ -55,7 +75,8 @@ impl AsRef<[u8]> for HashEncoded {
|
||||
HashEncoded::Sha256(hash) => hash.as_ref(),
|
||||
HashEncoded::HighwayHash256(hash) => hash.as_ref(),
|
||||
HashEncoded::HighwayHash256S(hash) => hash.as_ref(),
|
||||
HashEncoded::Blake2b512(hash) => hash.as_bytes(),
|
||||
HashEncoded::HighwayHash256SLegacy(hash) => hash.as_ref(),
|
||||
HashEncoded::Blake2b512(hash) => hash.as_ref(),
|
||||
HashEncoded::None => &[],
|
||||
}
|
||||
}
|
||||
@@ -83,17 +104,30 @@ impl HashAlgorithm {
|
||||
match self {
|
||||
HashAlgorithm::Md5 => HashEncoded::Md5(Md5::digest(data).into()),
|
||||
HashAlgorithm::HighwayHash256 => {
|
||||
let mut hasher = HighwayHasher::new(Key(HIGHWAY_HASH256_KEY));
|
||||
let key = Key(highway_key_from_bytes(&MAGIC_HIGHWAY_HASH256_KEY));
|
||||
let mut hasher = HighwayHasher::new(key);
|
||||
hasher.append(data);
|
||||
HashEncoded::HighwayHash256(u8x32_from_u64x4(hasher.finalize256()))
|
||||
}
|
||||
HashAlgorithm::SHA256 => HashEncoded::Sha256(Sha256::digest(data).into()),
|
||||
HashAlgorithm::HighwayHash256S => {
|
||||
let mut hasher = HighwayHasher::new(Key(HIGHWAY_HASH256_KEY));
|
||||
let key = Key(highway_key_from_bytes(&MAGIC_HIGHWAY_HASH256_KEY));
|
||||
let mut hasher = HighwayHasher::new(key);
|
||||
hasher.append(data);
|
||||
HashEncoded::HighwayHash256S(u8x32_from_u64x4(hasher.finalize256()))
|
||||
}
|
||||
HashAlgorithm::BLAKE2b512 => HashEncoded::Blake2b512(blake3::hash(data)),
|
||||
HashAlgorithm::HighwayHash256SLegacy => {
|
||||
let key = Key(highway_key_from_bytes(&LEGACY_HIGHWAY_HASH256_KEY));
|
||||
let mut hasher = HighwayHasher::new(key);
|
||||
hasher.append(data);
|
||||
HashEncoded::HighwayHash256SLegacy(u8x32_from_u64x4(hasher.finalize256()))
|
||||
}
|
||||
HashAlgorithm::BLAKE2b512 => {
|
||||
let hash = Blake2b512::digest(data);
|
||||
let mut out = [0u8; 64];
|
||||
out.copy_from_slice(hash.as_ref());
|
||||
HashEncoded::Blake2b512(out)
|
||||
}
|
||||
HashAlgorithm::None => HashEncoded::None,
|
||||
}
|
||||
}
|
||||
@@ -108,7 +142,8 @@ impl HashAlgorithm {
|
||||
HashAlgorithm::SHA256 => 32,
|
||||
HashAlgorithm::HighwayHash256 => 32,
|
||||
HashAlgorithm::HighwayHash256S => 32,
|
||||
HashAlgorithm::BLAKE2b512 => 32, // blake3 outputs 32 bytes by default
|
||||
HashAlgorithm::HighwayHash256SLegacy => 32,
|
||||
HashAlgorithm::BLAKE2b512 => 64,
|
||||
HashAlgorithm::Md5 => 16,
|
||||
HashAlgorithm::None => 0,
|
||||
}
|
||||
@@ -167,7 +202,7 @@ mod tests {
|
||||
assert_eq!(HashAlgorithm::HighwayHash256.size(), 32);
|
||||
assert_eq!(HashAlgorithm::HighwayHash256S.size(), 32);
|
||||
assert_eq!(HashAlgorithm::SHA256.size(), 32);
|
||||
assert_eq!(HashAlgorithm::BLAKE2b512.size(), 32);
|
||||
assert_eq!(HashAlgorithm::BLAKE2b512.size(), 64);
|
||||
assert_eq!(HashAlgorithm::None.size(), 0);
|
||||
}
|
||||
|
||||
@@ -220,13 +255,68 @@ mod tests {
|
||||
let data = b"test data";
|
||||
let hash = HashAlgorithm::BLAKE2b512.hash_encode(data);
|
||||
let hash = hash.as_ref();
|
||||
assert_eq!(hash.len(), 32); // blake3 outputs 32 bytes by default
|
||||
assert_eq!(hash.len(), 64);
|
||||
// BLAKE2b512 should be deterministic
|
||||
let hash2 = HashAlgorithm::BLAKE2b512.hash_encode(data);
|
||||
let hash2 = hash2.as_ref();
|
||||
assert_eq!(hash, hash2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bitrot_selftest() {
|
||||
let checksums: [(HashAlgorithm, &str); 5] = [
|
||||
(HashAlgorithm::SHA256, "a7677ff19e0182e4d52e3a3db727804abc82a5818749336369552e54b838b004"),
|
||||
(
|
||||
HashAlgorithm::BLAKE2b512,
|
||||
"e519b7d84b1c3c917985f544773a35cf265dcab10948be3550320d156bab612124a5ae2ae5a8c73c0eea360f68b0e28136f26e858756dbfe7375a7389f26c669",
|
||||
),
|
||||
(
|
||||
HashAlgorithm::HighwayHash256,
|
||||
"39c0407ed3f01b18d22c85db4aeff11e060ca5f43131b0126731ca197cd42313",
|
||||
),
|
||||
(
|
||||
HashAlgorithm::HighwayHash256S,
|
||||
"39c0407ed3f01b18d22c85db4aeff11e060ca5f43131b0126731ca197cd42313",
|
||||
),
|
||||
(
|
||||
HashAlgorithm::HighwayHash256SLegacy,
|
||||
"a5592a831588836b0f61bff43da4bd957c376d9b6412a9ecbbd144a3ecf34649",
|
||||
),
|
||||
];
|
||||
for (algo, expected_hex) in checksums {
|
||||
let block_size = match algo {
|
||||
HashAlgorithm::SHA256 => 64,
|
||||
HashAlgorithm::BLAKE2b512 => 128,
|
||||
HashAlgorithm::HighwayHash256 | HashAlgorithm::HighwayHash256S | HashAlgorithm::HighwayHash256SLegacy => 32,
|
||||
_ => continue,
|
||||
};
|
||||
let mut msg = Vec::new();
|
||||
let mut sum = Vec::new();
|
||||
for _ in 0..block_size {
|
||||
sum = algo.hash_encode(&msg).as_ref().to_vec();
|
||||
msg.extend_from_slice(&sum);
|
||||
}
|
||||
let got = hex_simd::encode_to_string(&sum, hex_simd::AsciiCase::Lower);
|
||||
assert_eq!(got, expected_hex, "{:?} selftest mismatch: got {} want {}", algo, got, expected_hex);
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates 7557 bytes
|
||||
/// Pattern: (i*7+13)%256 for each byte.
|
||||
fn generate_compat_test_data(size: usize) -> Vec<u8> {
|
||||
(0..size).map(|i| ((i * 7 + 13) % 256) as u8).collect()
|
||||
}
|
||||
|
||||
/// Run: cargo test -p rustfs-utils test_highwayhash_compat
|
||||
#[test]
|
||||
fn test_highwayhash_compat() {
|
||||
let data = generate_compat_test_data(7557);
|
||||
let hash = HashAlgorithm::HighwayHash256S.hash_encode(&data);
|
||||
let got = hex_simd::encode_to_string(hash.as_ref(), hex_simd::AsciiCase::Lower);
|
||||
let expected = "06543bf1c637e67386922a43b71cca08e5faa0f9131105a2bf96dec880529551";
|
||||
assert_eq!(got, expected, "HighwayHash256S must match: got {} want {}", got, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_data_different_hashes() {
|
||||
let data1 = b"test data 1";
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! HTTP header compatibility: read both x-rustfs-* and x-minio-* headers for MinIO
|
||||
//! interoperability. Write both when sending replication requests.
|
||||
//!
|
||||
//! Use suffix-based API: `get_header(headers, SUFFIX_FORCE_DELETE)` queries both
|
||||
//! x-rustfs-force-delete and x-minio-force-delete.
|
||||
|
||||
use http::{HeaderMap, HeaderValue};
|
||||
use std::borrow::Cow;
|
||||
|
||||
const RUSTFS_PREFIX: &str = "x-rustfs-";
|
||||
const MINIO_PREFIX: &str = "x-minio-";
|
||||
const MINIO_ENCRYPTION_PREFIX: &str = "x-minio-encryption-";
|
||||
const RUSTFS_ENCRYPTION_PREFIX: &str = "x-rustfs-encryption-";
|
||||
|
||||
// Suffix constants (part after x-rustfs- or x-minio-). Use with get_header/insert_header.
|
||||
pub const SUFFIX_FORCE_DELETE: &str = "force-delete";
|
||||
pub const SUFFIX_INCLUDE_DELETED: &str = "include-deleted";
|
||||
pub const SUFFIX_REPLICATION_RESET_STATUS: &str = "replication-reset-status";
|
||||
pub const SUFFIX_REPLICATION_ACTUAL_OBJECT_SIZE: &str = "replication-actual-object-size";
|
||||
pub const SUFFIX_SOURCE_VERSION_ID: &str = "source-version-id";
|
||||
pub const SUFFIX_SOURCE_MTIME: &str = "source-mtime";
|
||||
pub const SUFFIX_SOURCE_ETAG: &str = "source-etag";
|
||||
pub const SUFFIX_SOURCE_DELETEMARKER: &str = "source-deletemarker";
|
||||
pub const SUFFIX_SOURCE_PROXY_REQUEST: &str = "source-proxy-request";
|
||||
pub const SUFFIX_SOURCE_REPLICATION_REQUEST: &str = "source-replication-request";
|
||||
pub const SUFFIX_SOURCE_REPLICATION_CHECK: &str = "source-replication-check";
|
||||
pub const SUFFIX_REPLICATION_SSEC_CRC: &str = "replication-ssec-crc";
|
||||
|
||||
/// Returns true if the key is an internal encryption metadata key (x-rustfs-encryption-* or
|
||||
/// x-minio-encryption-*). Case-insensitive for metadata filtering.
|
||||
pub fn is_encryption_metadata_key(key: &str) -> bool {
|
||||
let lower = key.to_lowercase();
|
||||
lower.starts_with(RUSTFS_ENCRYPTION_PREFIX) || lower.starts_with(MINIO_ENCRYPTION_PREFIX)
|
||||
}
|
||||
|
||||
fn rustfs_key(suffix: &str) -> String {
|
||||
format!("{RUSTFS_PREFIX}{suffix}")
|
||||
}
|
||||
|
||||
fn minio_key(suffix: &str) -> String {
|
||||
format!("{MINIO_PREFIX}{suffix}")
|
||||
}
|
||||
|
||||
/// Get header value: tries x-rustfs-{suffix} first, then x-minio-{suffix}. Case-insensitive.
|
||||
pub fn get_header<'a>(headers: &'a HeaderMap, suffix: &str) -> Option<Cow<'a, str>> {
|
||||
let rk = rustfs_key(suffix);
|
||||
let mk = minio_key(suffix);
|
||||
headers
|
||||
.get(&rk)
|
||||
.or_else(|| headers.get(&mk))
|
||||
.and_then(|v| v.to_str().ok().map(Cow::Borrowed))
|
||||
}
|
||||
|
||||
/// Insert header with both x-rustfs-{suffix} and x-minio-{suffix}.
|
||||
pub fn insert_header(headers: &mut HeaderMap, suffix: &str, value: impl AsRef<[u8]>) {
|
||||
if let Ok(v) = HeaderValue::from_bytes(value.as_ref()) {
|
||||
if let Ok(k1) = rustfs_key(suffix).parse::<http::HeaderName>() {
|
||||
headers.insert(k1, v.clone());
|
||||
}
|
||||
if let Ok(k2) = minio_key(suffix).parse::<http::HeaderName>() {
|
||||
headers.insert(k2, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get from HashMap: tries x-rustfs-{suffix} first, then x-minio-{suffix}.
|
||||
pub fn get_header_map(map: &std::collections::HashMap<String, String>, suffix: &str) -> Option<String> {
|
||||
let rk = rustfs_key(suffix);
|
||||
let mk = minio_key(suffix);
|
||||
map.get(&rk).cloned().or_else(|| map.get(&mk).cloned())
|
||||
}
|
||||
|
||||
/// Insert into HashMap with both x-rustfs-{suffix} and x-minio-{suffix}.
|
||||
pub fn insert_header_map(map: &mut std::collections::HashMap<String, String>, suffix: &str, value: impl Into<String>) {
|
||||
let v = value.into();
|
||||
map.insert(rustfs_key(suffix), v.clone());
|
||||
map.insert(minio_key(suffix), v);
|
||||
}
|
||||
|
||||
/// Remove from HashMap both x-rustfs-{suffix} and x-minio-{suffix}.
|
||||
pub fn remove_header_map(map: &mut std::collections::HashMap<String, String>, suffix: &str) {
|
||||
map.remove(&rustfs_key(suffix));
|
||||
map.remove(&minio_key(suffix));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_is_encryption_metadata_key() {
|
||||
assert!(is_encryption_metadata_key("x-rustfs-encryption-iv"));
|
||||
assert!(is_encryption_metadata_key("X-Rustfs-Encryption-Key"));
|
||||
assert!(is_encryption_metadata_key("x-minio-encryption-iv"));
|
||||
assert!(!is_encryption_metadata_key("x-amz-meta-custom"));
|
||||
assert!(!is_encryption_metadata_key("x-rustfs-internal-healing"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_header() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("x-minio-force-delete", HeaderValue::from_static("true"));
|
||||
assert_eq!(get_header(&headers, SUFFIX_FORCE_DELETE).as_deref(), Some("true"));
|
||||
|
||||
let mut headers2 = HeaderMap::new();
|
||||
headers2.insert("X-Rustfs-Force-Delete", HeaderValue::from_static("true"));
|
||||
assert_eq!(get_header(&headers2, SUFFIX_FORCE_DELETE).as_deref(), Some("true"));
|
||||
}
|
||||
}
|
||||
@@ -148,41 +148,9 @@ pub const AMZ_META_NAME: &str = "X-Amz-Meta-Name";
|
||||
|
||||
pub const AMZ_META_UNENCRYPTED_CONTENT_LENGTH: &str = "X-Amz-Meta-X-Amz-Unencrypted-Content-Length";
|
||||
pub const AMZ_META_UNENCRYPTED_CONTENT_MD5: &str = "X-Amz-Meta-X-Amz-Unencrypted-Content-Md5";
|
||||
pub const RUSTFS_ENCRYPTION: &str = "X-Rustfs-Encryption-";
|
||||
pub const RUSTFS_ENCRYPTION_LOWER: &str = "x-rustfs-encryption-";
|
||||
|
||||
pub const RESERVED_METADATA_PREFIX: &str = "X-RustFS-Internal-";
|
||||
pub const RESERVED_METADATA_PREFIX_LOWER: &str = "x-rustfs-internal-";
|
||||
|
||||
pub const RUSTFS_HEALING: &str = "X-Rustfs-Internal-healing";
|
||||
// pub const RUSTFS_DATA_MOVE: &str = "X-Rustfs-Internal-data-mov";
|
||||
|
||||
// pub const X_RUSTFS_INLINE_DATA: &str = "x-rustfs-inline-data";
|
||||
|
||||
pub const VERSION_PURGE_STATUS_KEY: &str = "X-Rustfs-Internal-purgestatus";
|
||||
|
||||
pub const X_RUSTFS_HEALING: &str = "X-Rustfs-Internal-healing";
|
||||
pub const X_RUSTFS_DATA_MOV: &str = "X-Rustfs-Internal-data-mov";
|
||||
|
||||
pub const AMZ_TAGGING_DIRECTIVE: &str = "X-Amz-Tagging-Directive";
|
||||
|
||||
pub const RUSTFS_DATA_MOVE: &str = "X-Rustfs-Internal-data-mov";
|
||||
|
||||
pub const RUSTFS_FORCE_DELETE: &str = "X-Rustfs-Force-Delete";
|
||||
pub const RUSTFS_INCLUDE_DELETED: &str = "X-Rustfs-Include-Deleted";
|
||||
|
||||
pub const RUSTFS_REPLICATION_RESET_STATUS: &str = "X-Rustfs-Replication-Reset-Status";
|
||||
pub const RUSTFS_REPLICATION_ACTUAL_OBJECT_SIZE: &str = "X-Rustfs-Replication-Actual-Object-Size";
|
||||
|
||||
pub const RUSTFS_BUCKET_SOURCE_VERSION_ID: &str = "X-Rustfs-Source-Version-Id";
|
||||
pub const RUSTFS_BUCKET_SOURCE_MTIME: &str = "X-RustFS-Source-Mtime";
|
||||
pub const RUSTFS_BUCKET_SOURCE_ETAG: &str = "X-Rustfs-Source-Etag";
|
||||
pub const RUSTFS_BUCKET_REPLICATION_DELETE_MARKER: &str = "X-Rustfs-Source-DeleteMarker";
|
||||
pub const RUSTFS_BUCKET_REPLICATION_PROXY_REQUEST: &str = "X-Rustfs-Source-Proxy-Request";
|
||||
pub const RUSTFS_BUCKET_REPLICATION_REQUEST: &str = "X-Rustfs-Source-Replication-Request";
|
||||
pub const RUSTFS_BUCKET_REPLICATION_CHECK: &str = "X-Rustfs-Source-Replication-Check";
|
||||
pub const RUSTFS_BUCKET_REPLICATION_SSEC_CHECKSUM: &str = "X-Rustfs-Source-Replication-Ssec-Crc";
|
||||
|
||||
// SSEC encryption header constants
|
||||
pub const SSEC_ALGORITHM_HEADER: &str = "x-amz-server-side-encryption-customer-algorithm";
|
||||
pub const SSEC_KEY_HEADER: &str = "x-amz-server-side-encryption-customer-key";
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! System metadata compatibility: write both x-rustfs-internal-* and x-minio-internal-*
|
||||
//! for MinIO interoperability. Read prefers RustFS, fallback to MinIO.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub const RUSTFS_INTERNAL_PREFIX: &str = "x-rustfs-internal-";
|
||||
pub const MINIO_INTERNAL_PREFIX: &str = "x-minio-internal-";
|
||||
|
||||
// Key suffixes (lowercase, no prefix)
|
||||
pub const SUFFIX_INLINE_DATA: &str = "inline-data";
|
||||
pub const SUFFIX_DATA_MOVED: &str = "data-moved";
|
||||
/// Transient flag for data movement
|
||||
pub const SUFFIX_DATA_MOV: &str = "data-mov";
|
||||
/// Transient flag for healing
|
||||
pub const SUFFIX_HEALING: &str = "healing";
|
||||
pub const SUFFIX_COMPRESSION: &str = "compression";
|
||||
pub const SUFFIX_COMPRESSION_SIZE: &str = "compression-size";
|
||||
pub const SUFFIX_ACTUAL_SIZE: &str = "actual-size";
|
||||
pub const SUFFIX_ACTUAL_OBJECT_SIZE: &str = "actual-object-size";
|
||||
/// Used by replication; key stored with capital A
|
||||
pub const SUFFIX_ACTUAL_OBJECT_SIZE_CAP: &str = "Actual-Object-Size";
|
||||
pub const SUFFIX_CRC: &str = "crc";
|
||||
pub const SUFFIX_TRANSITION_STATUS: &str = "transition-status";
|
||||
pub const SUFFIX_TRANSITIONED_OBJECTNAME: &str = "transitioned-object";
|
||||
pub const SUFFIX_TRANSITIONED_VERSION_ID: &str = "transitioned-versionID";
|
||||
pub const SUFFIX_TRANSITION_TIER: &str = "transition-tier";
|
||||
pub const SUFFIX_FREE_VERSION: &str = "free-version";
|
||||
pub const SUFFIX_PURGESTATUS: &str = "purgestatus";
|
||||
pub const SUFFIX_REPLICA_STATUS: &str = "replica-status";
|
||||
pub const SUFFIX_REPLICA_TIMESTAMP: &str = "replica-timestamp";
|
||||
pub const SUFFIX_REPLICATION_STATUS: &str = "replication-status";
|
||||
pub const SUFFIX_REPLICATION_TIMESTAMP: &str = "replication-timestamp";
|
||||
pub const SUFFIX_TAGGING_TIMESTAMP: &str = "tagging-timestamp";
|
||||
pub const SUFFIX_OBJECTLOCK_RETENTION_TIMESTAMP: &str = "objectlock-retention-timestamp";
|
||||
pub const SUFFIX_OBJECTLOCK_LEGALHOLD_TIMESTAMP: &str = "objectlock-legalhold-timestamp";
|
||||
pub const SUFFIX_REPLICATION_RESET: &str = "replication-reset";
|
||||
/// Prefix for replication-reset-{arn} keys; use with internal_key_strip_suffix_prefix to extract arn.
|
||||
pub const SUFFIX_REPLICATION_RESET_ARN_PREFIX: &str = "replication-reset-";
|
||||
pub const SUFFIX_TIER_FV_ID: &str = "tier-free-versionID";
|
||||
pub const SUFFIX_TIER_FV_MARKER: &str = "tier-free-marker";
|
||||
pub const SUFFIX_TIER_SKIP_FV_ID: &str = "tier-skip-fvid";
|
||||
|
||||
/// Returns true if the key is an internal metadata key (x-rustfs-internal-* or x-minio-internal-*)
|
||||
/// for xl.meta compatibility. Case-insensitive.
|
||||
pub fn is_internal_key(key: &str) -> bool {
|
||||
let lower = key.to_lowercase();
|
||||
lower.starts_with(RUSTFS_INTERNAL_PREFIX) || lower.starts_with(MINIO_INTERNAL_PREFIX)
|
||||
}
|
||||
|
||||
/// Returns true if the key matches the given suffix for either x-rustfs-internal-* or x-minio-internal-*.
|
||||
pub fn has_internal_suffix(key: &str, suffix: &str) -> bool {
|
||||
let lower = key.to_lowercase();
|
||||
let rustfs_key = format!("{RUSTFS_INTERNAL_PREFIX}{suffix}");
|
||||
let minio_key = format!("{MINIO_INTERNAL_PREFIX}{suffix}");
|
||||
lower == rustfs_key || lower == minio_key
|
||||
}
|
||||
|
||||
/// Strips x-rustfs-internal- or x-minio-internal- prefix from key. Returns the suffix part.
|
||||
/// Case-insensitive. Returns None if key is not an internal key.
|
||||
pub fn strip_internal_prefix(key: &str) -> Option<String> {
|
||||
let lower = key.to_lowercase();
|
||||
lower
|
||||
.strip_prefix(RUSTFS_INTERNAL_PREFIX)
|
||||
.or_else(|| lower.strip_prefix(MINIO_INTERNAL_PREFIX))
|
||||
.map(|s| s.to_string())
|
||||
}
|
||||
|
||||
/// Returns true if key is internal and its suffix part starts with the given suffix_prefix.
|
||||
/// E.g. internal_key_starts_with("x-rustfs-internal-replication-reset-arn1", "replication-reset") == true.
|
||||
pub fn internal_key_starts_with(key: &str, suffix_prefix: &str) -> bool {
|
||||
strip_internal_prefix(key).is_some_and(|s| s.starts_with(suffix_prefix))
|
||||
}
|
||||
|
||||
/// For keys like x-rustfs-internal-replication-reset-{arn}, strips the internal prefix and suffix_prefix,
|
||||
/// returning the remainder (e.g. "arn1"). Returns None if key does not match.
|
||||
pub fn internal_key_strip_suffix_prefix(key: &str, suffix_prefix: &str) -> Option<String> {
|
||||
let rest = strip_internal_prefix(key)?;
|
||||
rest.strip_prefix(suffix_prefix).map(|s| s.to_string())
|
||||
}
|
||||
|
||||
fn both_keys(suffix: &str) -> (String, String) {
|
||||
(format!("{RUSTFS_INTERNAL_PREFIX}{suffix}"), format!("{MINIO_INTERNAL_PREFIX}{suffix}"))
|
||||
}
|
||||
|
||||
/// Builds the RustFS internal key for the given suffix. Use when a single key is needed (e.g. for
|
||||
/// backward compat). Prefer insert_str/get_str when both keys should be written/read.
|
||||
pub fn internal_key_rustfs(suffix: &str) -> String {
|
||||
format!("{RUSTFS_INTERNAL_PREFIX}{suffix}")
|
||||
}
|
||||
|
||||
// === String type (FileInfo.metadata, user_defined) ===
|
||||
|
||||
pub fn insert_str(map: &mut HashMap<String, String>, suffix: &str, value: String) {
|
||||
let (k1, k2) = both_keys(suffix);
|
||||
map.insert(k1, value.clone());
|
||||
map.insert(k2, value);
|
||||
}
|
||||
|
||||
pub fn get_str(map: &HashMap<String, String>, suffix: &str) -> Option<String> {
|
||||
let (k1, k2) = both_keys(suffix);
|
||||
map.get(&k1).cloned().or_else(|| map.get(&k2).cloned())
|
||||
}
|
||||
|
||||
pub fn contains_key_str(map: &HashMap<String, String>, suffix: &str) -> bool {
|
||||
let (k1, k2) = both_keys(suffix);
|
||||
map.contains_key(&k1) || map.contains_key(&k2)
|
||||
}
|
||||
|
||||
pub fn remove_str(map: &mut HashMap<String, String>, suffix: &str) {
|
||||
let (k1, k2) = both_keys(suffix);
|
||||
map.remove(&k1);
|
||||
map.remove(&k2);
|
||||
}
|
||||
|
||||
// === Vec<u8> type (meta_sys) ===
|
||||
|
||||
pub fn insert_bytes(map: &mut HashMap<String, Vec<u8>>, suffix: &str, value: Vec<u8>) {
|
||||
let (k1, k2) = both_keys(suffix);
|
||||
let v = value.clone();
|
||||
map.insert(k1, value);
|
||||
map.insert(k2, v);
|
||||
}
|
||||
|
||||
pub fn get_bytes(map: &HashMap<String, Vec<u8>>, suffix: &str) -> Option<Vec<u8>> {
|
||||
let (k1, k2) = both_keys(suffix);
|
||||
map.get(&k1).cloned().or_else(|| map.get(&k2).cloned())
|
||||
}
|
||||
|
||||
pub fn contains_key_bytes(map: &HashMap<String, Vec<u8>>, suffix: &str) -> bool {
|
||||
let (k1, k2) = both_keys(suffix);
|
||||
map.contains_key(&k1) || map.contains_key(&k2)
|
||||
}
|
||||
|
||||
pub fn remove_bytes(map: &mut HashMap<String, Vec<u8>>, suffix: &str) {
|
||||
let (k1, k2) = both_keys(suffix);
|
||||
map.remove(&k1);
|
||||
map.remove(&k2);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_is_internal_key() {
|
||||
assert!(is_internal_key("x-rustfs-internal-healing"));
|
||||
assert!(is_internal_key("x-rustfs-internal-purgestatus"));
|
||||
assert!(is_internal_key("X-RustFS-Internal-purgestatus"));
|
||||
assert!(is_internal_key("x-minio-internal-compression"));
|
||||
assert!(is_internal_key("x-minio-internal-replication-status"));
|
||||
assert!(is_internal_key("X-Minio-Internal-Compression"));
|
||||
assert!(!is_internal_key("x-amz-meta-custom"));
|
||||
assert!(!is_internal_key("content-type"));
|
||||
assert!(!is_internal_key("x-rustfs-meta-custom"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_has_internal_suffix() {
|
||||
assert!(has_internal_suffix("x-rustfs-internal-purgestatus", SUFFIX_PURGESTATUS));
|
||||
assert!(has_internal_suffix("X-Minio-Internal-purgestatus", SUFFIX_PURGESTATUS));
|
||||
assert!(has_internal_suffix("x-minio-internal-compression", SUFFIX_COMPRESSION));
|
||||
assert!(has_internal_suffix("x-rustfs-internal-healing", SUFFIX_HEALING));
|
||||
assert!(has_internal_suffix("x-minio-internal-data-mov", SUFFIX_DATA_MOV));
|
||||
assert!(!has_internal_suffix("x-rustfs-internal-purgestatus", SUFFIX_HEALING));
|
||||
assert!(!has_internal_suffix("x-amz-meta-custom", SUFFIX_PURGESTATUS));
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,11 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
pub mod header_compat;
|
||||
pub mod headers;
|
||||
pub mod ip;
|
||||
pub mod metadata_compat;
|
||||
pub use header_compat::*;
|
||||
pub use headers::*;
|
||||
pub use ip::*;
|
||||
pub use metadata_compat::*;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::http::{RESERVED_METADATA_PREFIX_LOWER, is_minio_header, is_rustfs_header};
|
||||
use crate::http::{is_internal_key, is_minio_header, is_rustfs_header};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Extract user-defined metadata keys from object metadata.
|
||||
@@ -80,7 +80,7 @@ pub fn extract_user_defined_metadata(metadata: &HashMap<String, String>) -> Hash
|
||||
for (key, value) in metadata {
|
||||
let lower_key = key.to_ascii_lowercase();
|
||||
|
||||
if lower_key.starts_with(RESERVED_METADATA_PREFIX_LOWER) {
|
||||
if is_internal_key(key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -188,7 +188,6 @@ mod tests {
|
||||
let mut metadata: HashMap<String, String> = HashMap::new();
|
||||
metadata.insert("x-rustfs-internal-healing".to_string(), "true".to_string());
|
||||
metadata.insert("x-rustfs-internal-data-mov".to_string(), "value".to_string());
|
||||
metadata.insert("X-RustFS-Internal-purgestatus".to_string(), "status".to_string());
|
||||
metadata.insert("x-rustfs-meta-custom".to_string(), "custom-value".to_string());
|
||||
metadata.insert("my-key".to_string(), "my-value".to_string());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user