mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-19 09:05:56 +00:00
refactor(ecstore): read persisted object-metadata keys from the filemeta authority (A3b) (#7770)
Migrate ecstore and rustfs-lifecycle consumers of the persisted xl.meta meta_user keys (object lock, restore, replication status, storage class, SSE read) from s3s::header / rustfs_utils header constants to rustfs_filemeta::metadata_keys. HTTP header production is unchanged and every lookup keeps its previous exact / case-insensitive mode. - SSE key: persisted only as lowercase; the mixed-case spelling is outbound replication user metadata. Keep the case-insensitive read and pin it against the pre-A3a fixture. - warm tier: strip promoted keys case-insensitively so the persisted X-Amz-Replication-Status is no longer forwarded to the tier. - fix stale HashReader::add_checksum_from_s3s call in ecstore tests. Refs rustfs/backlog#1735
This commit is contained in:
@@ -82,6 +82,7 @@ use rustfs_config::{
|
||||
ENV_TRANSITION_WORKERS, ENV_TRANSITION_WORKERS_ABSOLUTE_MAX,
|
||||
};
|
||||
use rustfs_data_usage::TierStats;
|
||||
use rustfs_filemeta::metadata_keys;
|
||||
use rustfs_filemeta::{
|
||||
FileInfo, FileInfoOpts, NULL_VERSION_ID, RestoreStatusOps, TRANSITION_COMPLETE, get_file_info, is_restored_object_on_disk,
|
||||
};
|
||||
@@ -93,7 +94,6 @@ use s3s::dto::{
|
||||
BucketLifecycleConfiguration, ExpirationStatus, ObjectLockConfiguration, RestoreRequest, RestoreRequestType, RestoreStatus,
|
||||
Timestamp,
|
||||
};
|
||||
use s3s::header::X_AMZ_RESTORE;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::any::Any;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
@@ -5174,7 +5174,7 @@ pub async fn put_restore_opts(
|
||||
}
|
||||
let restore_expiry = lifecycle::expected_expiry_time(OffsetDateTime::now_utc(), rreq.days.unwrap_or(1));
|
||||
meta.insert(
|
||||
X_AMZ_RESTORE.as_str().to_string(),
|
||||
metadata_keys::RESTORE.to_string(),
|
||||
RestoreStatus {
|
||||
is_restore_in_progress: Some(false),
|
||||
restore_expiry_date: Some(Timestamp::from(restore_expiry)),
|
||||
@@ -5912,6 +5912,7 @@ mod tests {
|
||||
use rustfs_config::ENV_MAX_EXPIRY_WORKERS;
|
||||
use rustfs_config::ENV_TRANSITION_WORKERS_ABSOLUTE_MAX;
|
||||
use rustfs_data_usage::TierStats;
|
||||
use rustfs_filemeta::metadata_keys;
|
||||
use rustfs_filemeta::{FileInfo, FileMeta};
|
||||
#[cfg(feature = "test-util")]
|
||||
use rustfs_s3_client::transition_api::ReaderImpl;
|
||||
@@ -5921,7 +5922,6 @@ mod tests {
|
||||
NoncurrentVersionExpiration, ObjectLockConfiguration, ObjectLockEnabled, ObjectLockRetentionMode, ObjectLockRule,
|
||||
OutputLocation, RestoreRequest, RestoreRequestType, S3Location, Timestamp, Transition, TransitionStorageClass,
|
||||
};
|
||||
use s3s::header::{X_AMZ_OBJECT_LOCK_LEGAL_HOLD, X_AMZ_OBJECT_LOCK_MODE, X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE};
|
||||
use serial_test::serial;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::HashMap;
|
||||
@@ -12173,7 +12173,7 @@ mod tests {
|
||||
"locked historical null",
|
||||
ObjectInfo {
|
||||
user_defined: Arc::new(HashMap::from([(
|
||||
X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_LEGAL_HOLD.to_string(),
|
||||
"ON".to_string(),
|
||||
)])),
|
||||
..historical_null.clone()
|
||||
@@ -12951,7 +12951,7 @@ mod tests {
|
||||
let lc = latest_expiration_lifecycle();
|
||||
let object = current_object_with_metadata(
|
||||
ReplicationStatusType::Completed,
|
||||
HashMap::from([(X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str().to_string(), "ON".to_string())]),
|
||||
HashMap::from([(metadata_keys::OBJECT_LOCK_LEGAL_HOLD.to_string(), "ON".to_string())]),
|
||||
);
|
||||
|
||||
let event = eval_action_from_lifecycle(&lc, None, &object).await;
|
||||
@@ -12969,10 +12969,10 @@ mod tests {
|
||||
ReplicationStatusType::Completed,
|
||||
HashMap::from([
|
||||
(
|
||||
X_AMZ_OBJECT_LOCK_MODE.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_MODE.to_string(),
|
||||
s3s::dto::ObjectLockRetentionMode::COMPLIANCE.to_string(),
|
||||
),
|
||||
(X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str().to_string(), retain_until),
|
||||
(metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(), retain_until),
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -12994,10 +12994,10 @@ mod tests {
|
||||
ReplicationStatusType::Completed,
|
||||
HashMap::from([
|
||||
(
|
||||
X_AMZ_OBJECT_LOCK_MODE.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_MODE.to_string(),
|
||||
ObjectLockRetentionMode::COMPLIANCE.to_string(),
|
||||
),
|
||||
(X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str().to_string(), retain_until),
|
||||
(metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(), retain_until),
|
||||
]),
|
||||
);
|
||||
object.transitioned_object.status = TRANSITION_COMPLETE.to_string();
|
||||
|
||||
@@ -37,7 +37,7 @@ mod tests {
|
||||
#[test]
|
||||
fn is_object_locked_by_metadata_preserves_object_lock_parser_behavior() {
|
||||
let mut user_defined = HashMap::new();
|
||||
user_defined.insert("x-amz-object-lock-legal-hold".to_string(), "ON".to_string());
|
||||
user_defined.insert(rustfs_filemeta::metadata_keys::OBJECT_LOCK_LEGAL_HOLD.to_string(), "ON".to_string());
|
||||
|
||||
assert!(is_object_locked_by_metadata(&user_defined, false));
|
||||
assert!(!is_object_locked_by_metadata(&user_defined, true));
|
||||
|
||||
@@ -13,9 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use super::types::{LegalHoldStatus, ObjectLegalHold, ObjectRetention, RetentionMode};
|
||||
use rustfs_utils::http::headers::{
|
||||
AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER, AMZ_OBJECT_LOCK_MODE_LOWER, AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER,
|
||||
};
|
||||
use rustfs_filemeta::metadata_keys;
|
||||
use std::collections::HashMap;
|
||||
use time::{OffsetDateTime, format_description};
|
||||
|
||||
@@ -35,7 +33,7 @@ pub fn utc_now_ntp() -> OffsetDateTime {
|
||||
|
||||
pub fn get_object_retention_meta(meta: &HashMap<String, String>) -> ObjectRetention {
|
||||
// The persisted metadata keys are the lowercase wire header names.
|
||||
let mode_str = meta.get(AMZ_OBJECT_LOCK_MODE_LOWER);
|
||||
let mode_str = meta.get(metadata_keys::OBJECT_LOCK_MODE);
|
||||
|
||||
let Some(mode_str) = mode_str else {
|
||||
return ObjectRetention::default();
|
||||
@@ -46,7 +44,7 @@ pub fn get_object_retention_meta(meta: &HashMap<String, String>) -> ObjectRetent
|
||||
return ObjectRetention::default();
|
||||
};
|
||||
|
||||
let till_str = meta.get(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER);
|
||||
let till_str = meta.get(metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE);
|
||||
|
||||
let retain_until_date =
|
||||
till_str.and_then(|s| OffsetDateTime::parse(s, &format_description::well_known::Iso8601::DEFAULT).ok());
|
||||
@@ -58,7 +56,7 @@ pub fn get_object_retention_meta(meta: &HashMap<String, String>) -> ObjectRetent
|
||||
}
|
||||
|
||||
pub fn get_object_legalhold_meta(meta: &HashMap<String, String>) -> ObjectLegalHold {
|
||||
let hold_str = meta.get(AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER);
|
||||
let hold_str = meta.get(metadata_keys::OBJECT_LOCK_LEGAL_HOLD);
|
||||
|
||||
ObjectLegalHold {
|
||||
status: hold_str.and_then(|s| parse_legalhold_status(s)),
|
||||
|
||||
@@ -19,9 +19,7 @@ use crate::bucket::object_lock::objectlock;
|
||||
use crate::bucket::object_lock::types::{DefaultRetention, LegalHoldStatus, RetentionMode};
|
||||
use crate::error::{Error, Result, StorageError};
|
||||
use crate::object_api::{ObjectInfo, ObjectOptions};
|
||||
use rustfs_utils::http::headers::{
|
||||
AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER, AMZ_OBJECT_LOCK_MODE_LOWER, AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER,
|
||||
};
|
||||
use rustfs_filemeta::metadata_keys;
|
||||
use std::sync::Arc;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
@@ -317,7 +315,7 @@ fn persisted_lock_value<'a>(obj_info: &'a ObjectInfo, key: &str) -> Option<&'a S
|
||||
/// Whether the version's persisted legal hold is ON. Any other non-empty
|
||||
/// value than ON/OFF is malformed metadata and fails closed.
|
||||
fn legal_hold_locks(obj_info: &ObjectInfo) -> Result<bool> {
|
||||
let Some(status) = persisted_lock_value(obj_info, AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER) else {
|
||||
let Some(status) = persisted_lock_value(obj_info, metadata_keys::OBJECT_LOCK_LEGAL_HOLD) else {
|
||||
return Ok(false);
|
||||
};
|
||||
match LegalHoldStatus::parse(status) {
|
||||
@@ -335,8 +333,8 @@ fn active_retention(
|
||||
default_retention: Option<&DefaultRetention>,
|
||||
obj_info: &ObjectInfo,
|
||||
) -> Result<Option<(RetentionMode, OffsetDateTime)>> {
|
||||
let mode = persisted_lock_value(obj_info, AMZ_OBJECT_LOCK_MODE_LOWER);
|
||||
let retain_until = persisted_lock_value(obj_info, AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER);
|
||||
let mode = persisted_lock_value(obj_info, metadata_keys::OBJECT_LOCK_MODE);
|
||||
let retain_until = persisted_lock_value(obj_info, metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE);
|
||||
match (mode, retain_until) {
|
||||
(None, None) => {}
|
||||
(Some(mode), Some(retain_until)) => {
|
||||
@@ -413,9 +411,6 @@ pub async fn check_object_lock_for_deletion(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::bucket::metadata_sys::configured_object_lock_state_for_tests;
|
||||
use rustfs_utils::http::headers::{
|
||||
AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER, AMZ_OBJECT_LOCK_MODE_LOWER, AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER,
|
||||
};
|
||||
use time::{Date, Month, PrimitiveDateTime, Time};
|
||||
|
||||
fn make_datetime(year: i32, month: u8, day: u8) -> OffsetDateTime {
|
||||
@@ -509,7 +504,7 @@ mod tests {
|
||||
#[test]
|
||||
fn deletion_rejects_incomplete_persisted_retention_metadata() {
|
||||
let mut user_defined = std::collections::HashMap::new();
|
||||
user_defined.insert(AMZ_OBJECT_LOCK_MODE_LOWER.to_string(), RetentionMode::COMPLIANCE.to_string());
|
||||
user_defined.insert(metadata_keys::OBJECT_LOCK_MODE.to_string(), RetentionMode::COMPLIANCE.to_string());
|
||||
let obj_info = ObjectInfo {
|
||||
user_defined: Arc::new(user_defined),
|
||||
..Default::default()
|
||||
@@ -535,10 +530,10 @@ mod tests {
|
||||
for (case, mode, retain_until, expected) in cases {
|
||||
let mut user_defined = std::collections::HashMap::new();
|
||||
if let Some(mode) = mode {
|
||||
user_defined.insert(AMZ_OBJECT_LOCK_MODE_LOWER.to_string(), mode.to_string());
|
||||
user_defined.insert(metadata_keys::OBJECT_LOCK_MODE.to_string(), mode.to_string());
|
||||
}
|
||||
if let Some(retain_until) = retain_until {
|
||||
user_defined.insert(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER.to_string(), retain_until.to_string());
|
||||
user_defined.insert(metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(), retain_until.to_string());
|
||||
}
|
||||
let obj_info = ObjectInfo {
|
||||
user_defined: Arc::new(user_defined),
|
||||
@@ -579,14 +574,14 @@ mod tests {
|
||||
/// source timestamp of every category that currently locks the version.
|
||||
#[test]
|
||||
fn replication_write_passes_worm_gate_only_with_every_locking_category_timestamp() {
|
||||
let hold = [(AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER, "ON")];
|
||||
let hold = [(metadata_keys::OBJECT_LOCK_LEGAL_HOLD, "ON")];
|
||||
let retention = [
|
||||
(AMZ_OBJECT_LOCK_MODE_LOWER, "GOVERNANCE"),
|
||||
(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER, "2099-01-01T00:00:00Z"),
|
||||
(metadata_keys::OBJECT_LOCK_MODE, "GOVERNANCE"),
|
||||
(metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE, "2099-01-01T00:00:00Z"),
|
||||
];
|
||||
let expired = [
|
||||
(AMZ_OBJECT_LOCK_MODE_LOWER, "COMPLIANCE"),
|
||||
(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER, "2000-01-01T00:00:00Z"),
|
||||
(metadata_keys::OBJECT_LOCK_MODE, "COMPLIANCE"),
|
||||
(metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE, "2000-01-01T00:00:00Z"),
|
||||
];
|
||||
let absent = ObjectLockConfigState::ConfirmedAbsent;
|
||||
let passes = |state: &ObjectLockConfigState, entries: &[&[(&str, &str)]], opts: &ObjectOptions| {
|
||||
@@ -606,7 +601,7 @@ mod tests {
|
||||
// Expired retention and a released hold no longer lock anything.
|
||||
assert!(passes(
|
||||
&absent,
|
||||
&[&expired, &[(AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER, "OFF")]],
|
||||
&[&expired, &[(metadata_keys::OBJECT_LOCK_LEGAL_HOLD, "OFF")]],
|
||||
&replication_opts(false, false)
|
||||
));
|
||||
|
||||
@@ -651,7 +646,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Default retention plus a legal hold: both categories need a timestamp.
|
||||
let held = lock_object_info(lock_metadata(&[&[(AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER, "ON")]]));
|
||||
let held = lock_object_info(lock_metadata(&[&[(metadata_keys::OBJECT_LOCK_LEGAL_HOLD, "ON")]]));
|
||||
assert!(!replication_write_may_pass_worm_gate(&state, &held, &replication_opts(false, true)).expect("judged"));
|
||||
assert!(!replication_write_may_pass_worm_gate(&state, &held, &replication_opts(true, false)).expect("judged"));
|
||||
assert!(replication_write_may_pass_worm_gate(&state, &held, &replication_opts(true, true)).expect("judged"));
|
||||
@@ -672,7 +667,7 @@ mod tests {
|
||||
assert!(replication_write_may_pass_worm_gate(&state, &delete_marker, &tagging_only).expect("judged"));
|
||||
|
||||
// Cleared (empty) explicit keys fall back to the bucket default.
|
||||
let cleared = lock_object_info(lock_metadata(&[&[(AMZ_OBJECT_LOCK_MODE_LOWER, "")]]));
|
||||
let cleared = lock_object_info(lock_metadata(&[&[(metadata_keys::OBJECT_LOCK_MODE, "")]]));
|
||||
assert!(!replication_write_may_pass_worm_gate(&state, &cleared, &tagging_only).expect("judged"));
|
||||
}
|
||||
}
|
||||
@@ -690,7 +685,7 @@ mod tests {
|
||||
.expect_err("fabricated bucket lock metadata must not be judged");
|
||||
assert!(err.to_string().contains("not authoritative"));
|
||||
|
||||
let malformed = lock_object_info(lock_metadata(&[&[(AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER, "MAYBE")]]));
|
||||
let malformed = lock_object_info(lock_metadata(&[&[(metadata_keys::OBJECT_LOCK_LEGAL_HOLD, "MAYBE")]]));
|
||||
let err = replication_write_may_pass_worm_gate(&ObjectLockConfigState::ConfirmedAbsent, &malformed, &opts)
|
||||
.expect_err("malformed legal hold must not be judged");
|
||||
assert!(err.to_string().contains("legal-hold"));
|
||||
@@ -738,15 +733,15 @@ mod tests {
|
||||
let cases: [(&str, &[&str]); 3] = [
|
||||
(
|
||||
"cleared retention",
|
||||
&[AMZ_OBJECT_LOCK_MODE_LOWER, AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER],
|
||||
&[metadata_keys::OBJECT_LOCK_MODE, metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE],
|
||||
),
|
||||
("cleared legal hold", &[AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER]),
|
||||
("cleared legal hold", &[metadata_keys::OBJECT_LOCK_LEGAL_HOLD]),
|
||||
(
|
||||
"all cleared",
|
||||
&[
|
||||
AMZ_OBJECT_LOCK_MODE_LOWER,
|
||||
AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER,
|
||||
AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER,
|
||||
metadata_keys::OBJECT_LOCK_MODE,
|
||||
metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE,
|
||||
metadata_keys::OBJECT_LOCK_LEGAL_HOLD,
|
||||
],
|
||||
),
|
||||
];
|
||||
@@ -766,7 +761,7 @@ mod tests {
|
||||
#[test]
|
||||
fn deletion_rejects_invalid_persisted_legal_hold_metadata() {
|
||||
let mut user_defined = std::collections::HashMap::new();
|
||||
user_defined.insert(AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER.to_string(), "INVALID".to_string());
|
||||
user_defined.insert(metadata_keys::OBJECT_LOCK_LEGAL_HOLD.to_string(), "INVALID".to_string());
|
||||
let obj_info = ObjectInfo {
|
||||
user_defined: Arc::new(user_defined),
|
||||
..Default::default()
|
||||
|
||||
@@ -14,6 +14,19 @@
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use rustfs_filemeta::ObjectPartInfo;
|
||||
/// Persisted object-metadata keys (`meta_user`); filemeta owns the on-disk spelling.
|
||||
pub(crate) use rustfs_filemeta::metadata_keys;
|
||||
|
||||
/// `FileInfo.metadata` of the xl.meta fixture written before `metadata_keys` existed.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn pre_metadata_keys_fixture_metadata() -> std::collections::HashMap<String, String> {
|
||||
rustfs_filemeta::FileMeta::load(&rustfs_filemeta::test_data::create_pre_metadata_keys_xlmeta().expect("decode fixture hex"))
|
||||
.expect("load fixture xl.meta")
|
||||
.into_fileinfo("bucket", "object", "0b1e5a3a-1735-4a3a-8000-00000000a3a0", false, false, false)
|
||||
.expect("fixture version to FileInfo")
|
||||
.metadata
|
||||
}
|
||||
|
||||
pub use rustfs_replication::{MrfOpKind, MrfReplicateEntry};
|
||||
pub(crate) use rustfs_replication::{
|
||||
REPLICATE_EXISTING, REPLICATE_HEAL_DELETE, ReplicateTargetDecision, ReplicatedInfos, ReplicatedTargetInfo, ReplicationAction,
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
|
||||
use std::{collections::HashMap, fmt, sync::Arc};
|
||||
|
||||
use super::replication_filemeta_boundary::metadata_keys;
|
||||
use crate::bucket::metadata::BucketMetadata;
|
||||
use rustfs_utils::http::AMZ_BUCKET_REPLICATION_STATUS;
|
||||
use s3s::dto::{BucketVersioningStatus, ReplicationConfiguration, ReplicationRuleStatus, VersioningConfiguration};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
@@ -334,7 +334,7 @@ impl ReplicationConfig {
|
||||
}
|
||||
|
||||
let mut user_defined = (*oi.user_defined).clone();
|
||||
user_defined.remove(AMZ_BUCKET_REPLICATION_STATUS);
|
||||
user_defined.remove(metadata_keys::REPLICATION_STATUS);
|
||||
|
||||
let dsc = must_replicate(
|
||||
oi.bucket.as_str(),
|
||||
|
||||
@@ -19,6 +19,7 @@ use super::replication_error_boundary::{Error, Result, is_err_object_not_found,
|
||||
use super::replication_event_sink::{EventArgs, send_event, send_local_event};
|
||||
#[cfg(test)]
|
||||
use super::replication_filemeta_boundary::ReplicationGenerationSnapshot;
|
||||
use super::replication_filemeta_boundary::metadata_keys;
|
||||
use super::replication_filemeta_boundary::{
|
||||
REPLICATE_EXISTING, ReplicateDecision, ReplicateObjectInfo, ReplicatedInfos, ReplicatedTargetInfo, ReplicationAction,
|
||||
ReplicationState, ReplicationStatusType, ReplicationType, VersionPurgeStatusType, get_replication_state,
|
||||
@@ -85,9 +86,9 @@ use metrics::counter;
|
||||
use rmp_serde;
|
||||
use rustfs_s3_types::EventName;
|
||||
use rustfs_utils::http::{
|
||||
AMZ_BUCKET_REPLICATION_STATUS, AMZ_OBJECT_LOCK_LEGAL_HOLD, AMZ_OBJECT_LOCK_MODE, AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE,
|
||||
AMZ_TAGGING_DIRECTIVE, SUFFIX_REPLICATION_RESET, SUFFIX_REPLICATION_STATUS, SUFFIX_REPLICATION_TARGET_VERSION_ARN_PREFIX,
|
||||
has_internal_suffix, insert_str, replication_target_versions,
|
||||
AMZ_OBJECT_LOCK_LEGAL_HOLD, AMZ_OBJECT_LOCK_MODE, AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE, AMZ_TAGGING_DIRECTIVE,
|
||||
SUFFIX_REPLICATION_RESET, SUFFIX_REPLICATION_STATUS, SUFFIX_REPLICATION_TARGET_VERSION_ARN_PREFIX, has_internal_suffix,
|
||||
insert_str, replication_target_versions,
|
||||
};
|
||||
use rustfs_utils::{DEFAULT_SIP_HASH_KEY, get_env_usize, sip_hash};
|
||||
#[cfg(test)]
|
||||
@@ -202,7 +203,7 @@ fn metadata_requires_existing_target(op_type: ReplicationType, object_info: &Obj
|
||||
op_type == ReplicationType::Metadata
|
||||
&& object_info
|
||||
.user_defined
|
||||
.get(AMZ_BUCKET_REPLICATION_STATUS)
|
||||
.get(metadata_keys::REPLICATION_STATUS)
|
||||
.is_some_and(|status| status.eq_ignore_ascii_case(ReplicationStatusType::Replica.as_str()))
|
||||
}
|
||||
|
||||
@@ -6269,7 +6270,7 @@ mod tests {
|
||||
version_id: roi.version_id,
|
||||
etag: Some("source-etag".to_string()),
|
||||
user_defined: Arc::new(HashMap::from([(
|
||||
AMZ_BUCKET_REPLICATION_STATUS.to_string(),
|
||||
metadata_keys::REPLICATION_STATUS.to_string(),
|
||||
ReplicationStatusType::Replica.as_str().to_string(),
|
||||
)])),
|
||||
..Default::default()
|
||||
|
||||
@@ -52,6 +52,7 @@ pub use rustfs_replication::{VersionIdentityCapability, version_identity_capabil
|
||||
|
||||
use super::replication_config_store::ReplicationConfigStore;
|
||||
use super::replication_error_boundary::{Error, Result};
|
||||
use super::replication_filemeta_boundary::metadata_keys;
|
||||
use super::replication_filemeta_boundary::{ReplicationAction, ReplicationStatusType, ReplicationType};
|
||||
use super::replication_storage_boundary::ObjectInfo;
|
||||
use super::replication_tagging_boundary::ReplicationTagFilter;
|
||||
@@ -94,7 +95,7 @@ fn metadata_value<'a>(metadata: &'a HashMap<String, String>, name: &str) -> Opti
|
||||
|
||||
fn classify_replication_source_encryption(metadata: &HashMap<String, String>) -> ReplicationSourceEncryption {
|
||||
let is_ssec = replication_object_is_ssec_encrypted(metadata);
|
||||
let sse = metadata_value(metadata, AMZ_SERVER_SIDE_ENCRYPTION);
|
||||
let sse = metadata_value(metadata, metadata_keys::SERVER_SIDE_ENCRYPTION);
|
||||
let kms_key_id = metadata_value(metadata, AMZ_SERVER_SIDE_ENCRYPTION_KMS_ID);
|
||||
let kms_context = metadata_value(metadata, AMZ_SERVER_SIDE_ENCRYPTION_KMS_CONTEXT);
|
||||
|
||||
@@ -577,6 +578,52 @@ mod tests {
|
||||
use time::Duration;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// backlog#1735 A3b SSE key-case finding. Every RustFS writer persists the
|
||||
/// SSE intent as lowercase `x-amz-server-side-encryption`
|
||||
/// (`encryption_material_to_metadata`; header-derived keys come from a
|
||||
/// lowercase `http::HeaderMap`). The mixed-case `X-Amz-Server-Side-Encryption`
|
||||
/// spelling only appears in outbound replication user metadata, which the
|
||||
/// S3 client re-lowercases on the wire. This reader has always matched
|
||||
/// the key ASCII-case-insensitively; keep that, so the old lowercase bytes
|
||||
/// and any title-case spelling both classify, while any non-case drift of
|
||||
/// the key reads as "no SSE intent".
|
||||
#[test]
|
||||
fn replication_sse_classification_reads_pre_module_xlmeta_key() {
|
||||
let metadata = super::super::replication_filemeta_boundary::pre_metadata_keys_fixture_metadata();
|
||||
assert_eq!(metadata.get("x-amz-server-side-encryption").map(String::as_str), Some("AES256"));
|
||||
assert_eq!(classify_replication_source_encryption(&metadata), ReplicationSourceEncryption::SseS3);
|
||||
|
||||
let sse_only = |key: String| HashMap::from([(key, "AES256".to_string())]);
|
||||
let key = metadata_keys::SERVER_SIDE_ENCRYPTION;
|
||||
for spelling in [
|
||||
key.to_string(),
|
||||
"X-Amz-Server-Side-Encryption".to_string(),
|
||||
key.to_ascii_uppercase(),
|
||||
] {
|
||||
assert_eq!(
|
||||
classify_replication_source_encryption(&sse_only(spelling.clone())),
|
||||
ReplicationSourceEncryption::SseS3,
|
||||
"{spelling:?}"
|
||||
);
|
||||
}
|
||||
for idx in 0..key.len() {
|
||||
let mut bytes = key.as_bytes().to_vec();
|
||||
bytes[idx] = if bytes[idx] == b'z' {
|
||||
b'y'
|
||||
} else if bytes[idx].is_ascii_alphabetic() {
|
||||
b'z'
|
||||
} else {
|
||||
b'_'
|
||||
};
|
||||
let mutated = String::from_utf8(bytes).expect("ascii");
|
||||
assert_eq!(
|
||||
classify_replication_source_encryption(&sse_only(mutated.clone())),
|
||||
ReplicationSourceEncryption::Plaintext,
|
||||
"{mutated:?} must not read as the SSE key"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize an object-level checksum record the way
|
||||
/// `complete_multipart_upload` persists it for a **full-object** checksum:
|
||||
/// the record carries the plain algorithm type, without the MULTIPART
|
||||
|
||||
@@ -2110,8 +2110,8 @@ async fn migrate_object_inner(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::bucket::replication::{ReplicationStatusType, VersionPurgeStatusType};
|
||||
use rustfs_filemeta::metadata_keys;
|
||||
use rustfs_rio::{Checksum, ChecksumType};
|
||||
use s3s::header::{X_AMZ_OBJECT_LOCK_LEGAL_HOLD, X_AMZ_OBJECT_LOCK_MODE, X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE};
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
@@ -2169,16 +2169,16 @@ mod tests {
|
||||
assert_eq!(source.version_purge_status_internal, target.version_purge_status_internal);
|
||||
assert_eq!(source.version_purge_status, target.version_purge_status);
|
||||
assert_eq!(
|
||||
source.user_defined.get(X_AMZ_OBJECT_LOCK_MODE.as_str()),
|
||||
target.user_defined.get(X_AMZ_OBJECT_LOCK_MODE.as_str())
|
||||
source.user_defined.get(metadata_keys::OBJECT_LOCK_MODE),
|
||||
target.user_defined.get(metadata_keys::OBJECT_LOCK_MODE)
|
||||
);
|
||||
assert_eq!(
|
||||
source.user_defined.get(X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str()),
|
||||
target.user_defined.get(X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str())
|
||||
source.user_defined.get(metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE),
|
||||
target.user_defined.get(metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE)
|
||||
);
|
||||
assert_eq!(
|
||||
source.user_defined.get(X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str()),
|
||||
target.user_defined.get(X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str())
|
||||
source.user_defined.get(metadata_keys::OBJECT_LOCK_LEGAL_HOLD),
|
||||
target.user_defined.get(metadata_keys::OBJECT_LOCK_LEGAL_HOLD)
|
||||
);
|
||||
assert_eq!(source.parts.len(), target.parts.len());
|
||||
for (source_part, target_part) in source.parts.iter().zip(target.parts.iter()) {
|
||||
@@ -2818,13 +2818,13 @@ mod tests {
|
||||
let mod_time = OffsetDateTime::UNIX_EPOCH;
|
||||
let metadata = Arc::new(HashMap::from([
|
||||
("x-amz-meta-key".to_string(), "value".to_string()),
|
||||
(rustfs_utils::http::AMZ_STORAGE_CLASS.to_string(), "STANDARD_IA".to_string()),
|
||||
(X_AMZ_OBJECT_LOCK_MODE.as_str().to_string(), "GOVERNANCE".to_string()),
|
||||
(rustfs_filemeta::metadata_keys::STORAGE_CLASS.to_string(), "STANDARD_IA".to_string()),
|
||||
(metadata_keys::OBJECT_LOCK_MODE.to_string(), "GOVERNANCE".to_string()),
|
||||
(
|
||||
X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(),
|
||||
"2030-01-01T00:00:00Z".to_string(),
|
||||
),
|
||||
(X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str().to_string(), "ON".to_string()),
|
||||
(metadata_keys::OBJECT_LOCK_LEGAL_HOLD.to_string(), "ON".to_string()),
|
||||
]));
|
||||
let part = ObjectPartInfo {
|
||||
number: 1,
|
||||
@@ -2864,12 +2864,12 @@ mod tests {
|
||||
rustfs_utils::http::SUFFIX_REPLICATION_STATUS.to_string(),
|
||||
"arn:minio:target=PENDING;".to_string(),
|
||||
),
|
||||
(X_AMZ_OBJECT_LOCK_MODE.as_str().to_string(), "COMPLIANCE".to_string()),
|
||||
(metadata_keys::OBJECT_LOCK_MODE.to_string(), "COMPLIANCE".to_string()),
|
||||
(
|
||||
X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(),
|
||||
"2031-01-01T00:00:00Z".to_string(),
|
||||
),
|
||||
(X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str().to_string(), "ON".to_string()),
|
||||
(metadata_keys::OBJECT_LOCK_LEGAL_HOLD.to_string(), "ON".to_string()),
|
||||
])),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -2882,15 +2882,15 @@ mod tests {
|
||||
Some(&"arn:minio:target=PENDING;".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
new_multipart_opts.user_defined.get(X_AMZ_OBJECT_LOCK_MODE.as_str()),
|
||||
new_multipart_opts.user_defined.get(metadata_keys::OBJECT_LOCK_MODE),
|
||||
Some(&"COMPLIANCE".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
put_opts.user_defined.get(X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str()),
|
||||
put_opts.user_defined.get(metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE),
|
||||
Some(&"2031-01-01T00:00:00Z".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
new_multipart_opts.user_defined.get(X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str()),
|
||||
new_multipart_opts.user_defined.get(metadata_keys::OBJECT_LOCK_LEGAL_HOLD),
|
||||
Some(&"ON".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -560,7 +560,7 @@ mod tests {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("x-amz-trailer", HeaderValue::from_static("x-amz-checksum-crc32"));
|
||||
reader
|
||||
.add_checksum_from_s3s(&headers, None, false)
|
||||
.add_checksum(&headers, None, false)
|
||||
.expect("attach trailing checksum metadata");
|
||||
|
||||
let transformed = WritePlan::new()
|
||||
|
||||
@@ -34,13 +34,12 @@ use crate::store::utils::clean_metadata;
|
||||
use crate::{bucket::lifecycle::bucket_lifecycle_audit::LcAuditEvent, bucket::lifecycle::lifecycle::TransitionOptions};
|
||||
use bytes::Bytes;
|
||||
use http::{HeaderMap, HeaderValue};
|
||||
use rustfs_filemeta::metadata_keys;
|
||||
use rustfs_filemeta::{FileInfo, MetaCacheEntriesSorted, ObjectPartInfo, RestoreStatusOps as _, parse_restore_obj_status};
|
||||
use rustfs_rio::Checksum;
|
||||
use rustfs_utils::CompressionAlgorithm;
|
||||
use rustfs_utils::http::headers::AMZ_OBJECT_TAGGING;
|
||||
use rustfs_utils::http::{
|
||||
AMZ_BUCKET_REPLICATION_STATUS, AMZ_RESTORE, AMZ_STORAGE_CLASS, SUFFIX_PLAINTEXT_CHECKSUM, get_consistent_str,
|
||||
};
|
||||
use rustfs_utils::http::{SUFFIX_PLAINTEXT_CHECKSUM, get_consistent_str};
|
||||
use rustfs_utils::path::decode_dir_object;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
@@ -106,6 +105,9 @@ mod object_mutation_hook;
|
||||
mod readers;
|
||||
mod types;
|
||||
|
||||
#[cfg(test)]
|
||||
mod persisted_metadata_keys_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use body_cache_hook::clear_get_object_body_cache_hook;
|
||||
pub use body_cache_hook::{
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
// 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.
|
||||
|
||||
//! Old-bytes coverage for the ecstore/lifecycle readers of the persisted
|
||||
//! `meta_user` keys (backlog#1735 A3b).
|
||||
//!
|
||||
//! The fixture is xl.meta written by the code that predates
|
||||
//! `rustfs_filemeta::metadata_keys`, with each key taken from its historical
|
||||
//! source. Every ecstore reader migrated to the authority must still find its
|
||||
//! key in those bytes, and a single-character drift of any key (ASCII case
|
||||
//! included, because these lookups are exact) must make the reader miss.
|
||||
|
||||
use super::*;
|
||||
use crate::bucket::object_lock::objectlock::{get_object_legalhold_meta, get_object_retention_meta};
|
||||
use crate::bucket::object_lock::objectlock_sys::{ObjectLockBlockReason, check_object_lock_for_deletion_with_default_retention};
|
||||
use crate::bucket::object_lock::types::{LegalHoldStatus, RetentionMode};
|
||||
use crate::bucket::replication::ReplicationStatusType;
|
||||
use crate::store::utils::remove_standard_storage_class;
|
||||
use rustfs_filemeta::FileMeta;
|
||||
use rustfs_filemeta::metadata_keys;
|
||||
use rustfs_filemeta::test_data::create_pre_metadata_keys_xlmeta;
|
||||
|
||||
const FIXTURE_VERSION_ID: &str = "0b1e5a3a-1735-4a3a-8000-00000000a3a0";
|
||||
|
||||
fn fixture_file_info() -> FileInfo {
|
||||
FileMeta::load(&create_pre_metadata_keys_xlmeta().expect("decode fixture hex"))
|
||||
.expect("load fixture xl.meta")
|
||||
.into_fileinfo("bucket", "object", FIXTURE_VERSION_ID, false, false, false)
|
||||
.expect("fixture version to FileInfo")
|
||||
}
|
||||
|
||||
fn object_info(fi: &FileInfo) -> ObjectInfo {
|
||||
ObjectInfo::from_file_info(fi, "bucket", "object", true)
|
||||
}
|
||||
|
||||
/// Replaces the byte at `idx` with a different one of the same class:
|
||||
/// ASCII letters flip case, anything else becomes `_`.
|
||||
fn mutate_at(key: &str, idx: usize) -> String {
|
||||
let mut bytes = key.as_bytes().to_vec();
|
||||
let b = bytes[idx];
|
||||
bytes[idx] = if b.is_ascii_lowercase() {
|
||||
b.to_ascii_uppercase()
|
||||
} else if b.is_ascii_uppercase() {
|
||||
b.to_ascii_lowercase()
|
||||
} else {
|
||||
b'_'
|
||||
};
|
||||
String::from_utf8(bytes).expect("ascii mutation stays utf-8")
|
||||
}
|
||||
|
||||
/// The fixture version with `key` stored under `mutated` instead.
|
||||
fn file_info_with_key_renamed(key: &str, mutated: &str) -> FileInfo {
|
||||
let mut fi = fixture_file_info();
|
||||
let value = fi.metadata.remove(key).expect("fixture carries the key");
|
||||
fi.metadata.insert(mutated.to_string(), value);
|
||||
fi
|
||||
}
|
||||
|
||||
fn locked_by_object_lock(oi: &ObjectInfo) -> bool {
|
||||
let retention = get_object_retention_meta(&oi.user_defined);
|
||||
let hold = get_object_legalhold_meta(&oi.user_defined);
|
||||
retention.mode.is_some()
|
||||
|| hold.status.is_some()
|
||||
|| rustfs_lifecycle::object_lock::is_object_locked_by_metadata(&oi.user_defined, false)
|
||||
|| check_object_lock_for_deletion_with_default_retention(None, oi, false)
|
||||
.expect("object-lock check")
|
||||
.is_some()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_module_xlmeta_object_lock_is_enforced_by_ecstore_and_lifecycle() {
|
||||
let oi = object_info(&fixture_file_info());
|
||||
|
||||
let retention = get_object_retention_meta(&oi.user_defined);
|
||||
assert!(matches!(retention.mode, Some(RetentionMode::Compliance)));
|
||||
assert_eq!(retention.retain_until_date.map(|date| date.year()), Some(2099));
|
||||
assert!(matches!(get_object_legalhold_meta(&oi.user_defined).status, Some(LegalHoldStatus::On)));
|
||||
assert!(rustfs_lifecycle::object_lock::is_object_locked_by_metadata(&oi.user_defined, false));
|
||||
assert!(matches!(
|
||||
check_object_lock_for_deletion_with_default_retention(None, &oi, true),
|
||||
Ok(Some(ObjectLockBlockReason::LegalHold))
|
||||
));
|
||||
|
||||
let mut without_hold = fixture_file_info();
|
||||
without_hold.metadata.remove(metadata_keys::OBJECT_LOCK_LEGAL_HOLD);
|
||||
assert!(matches!(
|
||||
check_object_lock_for_deletion_with_default_retention(None, &object_info(&without_hold), true),
|
||||
Ok(Some(ObjectLockBlockReason::Retention {
|
||||
mode: RetentionMode::Compliance,
|
||||
..
|
||||
}))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_module_xlmeta_restore_replication_and_storage_class_read_back() {
|
||||
let fi = fixture_file_info();
|
||||
let oi = object_info(&fi);
|
||||
assert!(!oi.restore_ongoing);
|
||||
assert_eq!(oi.restore_expires.map(|date| date.year()), Some(9999));
|
||||
|
||||
let mut replica = fixture_file_info();
|
||||
replica.metadata.insert(
|
||||
metadata_keys::REPLICATION_STATUS.to_string(),
|
||||
ReplicationStatusType::Replica.as_str().to_string(),
|
||||
);
|
||||
assert_eq!(object_info(&replica).replication_status, ReplicationStatusType::Replica);
|
||||
|
||||
let mut metadata = fi.metadata;
|
||||
assert_eq!(metadata.get(metadata_keys::STORAGE_CLASS).map(String::as_str), Some("GLACIER"));
|
||||
remove_standard_storage_class(&mut metadata);
|
||||
assert!(metadata.contains_key(metadata_keys::STORAGE_CLASS), "non-STANDARD class must stay");
|
||||
metadata.insert(
|
||||
metadata_keys::STORAGE_CLASS.to_string(),
|
||||
crate::config::storageclass::STANDARD.to_string(),
|
||||
);
|
||||
remove_standard_storage_class(&mut metadata);
|
||||
assert!(!metadata.contains_key(metadata_keys::STORAGE_CLASS), "STANDARD must be dropped");
|
||||
}
|
||||
|
||||
/// Per-character mutation over the persisted keys the migrated ecstore and
|
||||
/// lifecycle readers look up exactly: each drifted spelling must be missed.
|
||||
#[test]
|
||||
fn single_character_key_mutation_is_missed_by_ecstore_readers() {
|
||||
// Keep a single object-lock category per object so each mutation is
|
||||
// judged on its own: the legal hold alone, then the retention pair alone.
|
||||
for key in [
|
||||
metadata_keys::OBJECT_LOCK_LEGAL_HOLD,
|
||||
metadata_keys::OBJECT_LOCK_MODE,
|
||||
metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE,
|
||||
] {
|
||||
for idx in 0..key.len() {
|
||||
let mutated = mutate_at(key, idx);
|
||||
let mut fi = file_info_with_key_renamed(key, &mutated);
|
||||
if key == metadata_keys::OBJECT_LOCK_LEGAL_HOLD {
|
||||
fi.metadata.remove(metadata_keys::OBJECT_LOCK_MODE);
|
||||
fi.metadata.remove(metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE);
|
||||
assert!(!locked_by_object_lock(&object_info(&fi)), "legal hold under {mutated:?} must not lock");
|
||||
} else {
|
||||
fi.metadata.remove(metadata_keys::OBJECT_LOCK_LEGAL_HOLD);
|
||||
let oi = object_info(&fi);
|
||||
let retention = get_object_retention_meta(&oi.user_defined);
|
||||
if key == metadata_keys::OBJECT_LOCK_MODE {
|
||||
assert!(retention.mode.is_none(), "mode under {mutated:?} must not be read");
|
||||
} else {
|
||||
assert!(retention.retain_until_date.is_none(), "retain-until under {mutated:?} must not be read");
|
||||
}
|
||||
assert!(
|
||||
!rustfs_lifecycle::object_lock::is_object_locked_by_metadata(&oi.user_defined, false),
|
||||
"retention with {mutated:?} must not read as locked"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for idx in 0..metadata_keys::RESTORE.len() {
|
||||
let mutated = mutate_at(metadata_keys::RESTORE, idx);
|
||||
let oi = object_info(&file_info_with_key_renamed(metadata_keys::RESTORE, &mutated));
|
||||
assert!(oi.restore_expires.is_none(), "restore status under {mutated:?} must not be read");
|
||||
}
|
||||
|
||||
for idx in 0..metadata_keys::REPLICATION_STATUS.len() {
|
||||
let mutated = mutate_at(metadata_keys::REPLICATION_STATUS, idx);
|
||||
let mut fi = fixture_file_info();
|
||||
fi.metadata.remove(metadata_keys::REPLICATION_STATUS);
|
||||
fi.metadata
|
||||
.insert(mutated.clone(), ReplicationStatusType::Replica.as_str().to_string());
|
||||
assert_ne!(
|
||||
object_info(&fi).replication_status,
|
||||
ReplicationStatusType::Replica,
|
||||
"replica status under {mutated:?} must not be read"
|
||||
);
|
||||
}
|
||||
|
||||
for idx in 0..metadata_keys::STORAGE_CLASS.len() {
|
||||
let mutated = mutate_at(metadata_keys::STORAGE_CLASS, idx);
|
||||
let mut metadata = HashMap::from([(mutated.clone(), crate::config::storageclass::STANDARD.to_string())]);
|
||||
remove_standard_storage_class(&mut metadata);
|
||||
assert!(metadata.contains_key(&mutated), "storage class under {mutated:?} must not be matched");
|
||||
}
|
||||
}
|
||||
@@ -1562,8 +1562,7 @@ impl ObjectInfo {
|
||||
.user_defined
|
||||
.iter()
|
||||
.filter(|(key, _)| {
|
||||
!rustfs_utils::http::is_internal_key(key)
|
||||
&& !key.eq_ignore_ascii_case(rustfs_utils::http::AMZ_BUCKET_REPLICATION_STATUS)
|
||||
!rustfs_utils::http::is_internal_key(key) && !key.eq_ignore_ascii_case(metadata_keys::REPLICATION_STATUS)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
user_metadata.sort_unstable_by(|left, right| left.0.cmp(right.0).then_with(|| left.1.cmp(right.1)));
|
||||
@@ -1783,7 +1782,7 @@ impl ObjectInfo {
|
||||
|
||||
let mut replication_status = replication_status_from_filemeta(fi.replication_status());
|
||||
if replication_status.is_empty()
|
||||
&& let Some(status) = fi.metadata.get(AMZ_BUCKET_REPLICATION_STATUS).cloned()
|
||||
&& let Some(status) = fi.metadata.get(metadata_keys::REPLICATION_STATUS).cloned()
|
||||
&& status == ReplicationStatusType::Replica.as_str()
|
||||
{
|
||||
replication_status = ReplicationStatusType::Replica;
|
||||
@@ -1811,7 +1810,7 @@ impl ObjectInfo {
|
||||
|
||||
let storage_class = Some(
|
||||
storageclass::effective_class(
|
||||
fi.metadata.get(AMZ_STORAGE_CLASS).map(String::as_str),
|
||||
fi.metadata.get(metadata_keys::STORAGE_CLASS).map(String::as_str),
|
||||
(fi.transition_status == rustfs_filemeta::TRANSITION_COMPLETE && !fi.transition_tier.is_empty())
|
||||
.then_some(fi.transition_tier.as_str()),
|
||||
)
|
||||
@@ -1820,7 +1819,7 @@ impl ObjectInfo {
|
||||
|
||||
let mut restore_ongoing = false;
|
||||
let mut restore_expires = None;
|
||||
if let Some(restore_status) = fi.metadata.get(AMZ_RESTORE).cloned()
|
||||
if let Some(restore_status) = fi.metadata.get(metadata_keys::RESTORE).cloned()
|
||||
&& let Ok(restore_status) = parse_restore_obj_status(&restore_status)
|
||||
{
|
||||
restore_ongoing = restore_status.on_going();
|
||||
@@ -2725,7 +2724,7 @@ mod tests {
|
||||
storageclass::GLACIER,
|
||||
] {
|
||||
let fi = FileInfo {
|
||||
metadata: HashMap::from([(AMZ_STORAGE_CLASS.to_string(), legacy_label.to_string())]),
|
||||
metadata: HashMap::from([(metadata_keys::STORAGE_CLASS.to_string(), legacy_label.to_string())]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -2742,7 +2741,7 @@ mod tests {
|
||||
#[test]
|
||||
fn from_file_info_preserves_transitioned_tier_storage_class() {
|
||||
let fi = FileInfo {
|
||||
metadata: HashMap::from([(AMZ_STORAGE_CLASS.to_string(), storageclass::STANDARD_IA.to_string())]),
|
||||
metadata: HashMap::from([(metadata_keys::STORAGE_CLASS.to_string(), storageclass::STANDARD_IA.to_string())]),
|
||||
transition_tier: "WARM-TIER".to_string(),
|
||||
transition_status: TRANSITION_COMPLETE.to_string(),
|
||||
..Default::default()
|
||||
@@ -2757,7 +2756,7 @@ mod tests {
|
||||
#[test]
|
||||
fn from_file_info_ignores_a_tier_name_without_a_completed_transition() {
|
||||
let fi = FileInfo {
|
||||
metadata: HashMap::from([(AMZ_STORAGE_CLASS.to_string(), storageclass::STANDARD_IA.to_string())]),
|
||||
metadata: HashMap::from([(metadata_keys::STORAGE_CLASS.to_string(), storageclass::STANDARD_IA.to_string())]),
|
||||
transition_tier: "WARM-TIER".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -35,6 +35,7 @@ use crate::services::tier::{
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use http::StatusCode;
|
||||
use rustfs_filemeta::metadata_keys;
|
||||
use rustfs_s3_client::credentials::{Credentials, SignatureType, Static, Value};
|
||||
use rustfs_s3_client::transition_api::{BucketLookupType, Options, TransitionClient, TransitionClientTimeouts, TransitionCore};
|
||||
use rustfs_s3_client::{
|
||||
@@ -48,10 +49,6 @@ use rustfs_utils::egress::validate_outbound_url;
|
||||
use rustfs_utils::http::headers::{
|
||||
CACHE_CONTROL, CONTENT_DISPOSITION, CONTENT_ENCODING, CONTENT_LANGUAGE, CONTENT_TYPE, EXPIRES, HeaderExt as _,
|
||||
};
|
||||
use s3s::header::{
|
||||
X_AMZ_OBJECT_LOCK_LEGAL_HOLD, X_AMZ_OBJECT_LOCK_MODE, X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE, X_AMZ_REPLICATION_STATUS,
|
||||
X_AMZ_STORAGE_CLASS,
|
||||
};
|
||||
use s3s::{
|
||||
S3ErrorCode,
|
||||
dto::{ObjectLockLegalHoldStatus, ObjectLockRetentionMode, ReplicationStatus},
|
||||
@@ -253,36 +250,39 @@ pub fn build_transition_put_options(storage_class: String, mut metadata: HashMap
|
||||
opts.expires = expires;
|
||||
}
|
||||
|
||||
if let Some(mode) = metadata.lookup(X_AMZ_OBJECT_LOCK_MODE.as_str()) {
|
||||
if let Some(mode) = metadata.lookup(metadata_keys::OBJECT_LOCK_MODE) {
|
||||
opts.mode = ObjectLockRetentionMode::from(mode.to_ascii_uppercase());
|
||||
}
|
||||
|
||||
if let Some(retain_until_date) = metadata
|
||||
.lookup(X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str())
|
||||
.lookup(metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE)
|
||||
.and_then(parse_http_timestamp)
|
||||
{
|
||||
opts.retain_until_date = retain_until_date;
|
||||
}
|
||||
|
||||
if let Some(legalhold) = metadata.lookup(X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str()) {
|
||||
if let Some(legalhold) = metadata.lookup(metadata_keys::OBJECT_LOCK_LEGAL_HOLD) {
|
||||
opts.legalhold = ObjectLockLegalHoldStatus::from(legalhold.to_ascii_uppercase());
|
||||
}
|
||||
|
||||
for key in [
|
||||
// Promoted keys are read above through `lookup`, which accepts more than
|
||||
// one spelling, so strip every ASCII-case spelling here. The replication
|
||||
// status is persisted as `X-Amz-Replication-Status`; the former exact
|
||||
// lowercase removal left that spelling in the forwarded user metadata.
|
||||
const PROMOTED_KEYS: [&str; 11] = [
|
||||
CONTENT_TYPE,
|
||||
CONTENT_ENCODING,
|
||||
CONTENT_LANGUAGE,
|
||||
CONTENT_DISPOSITION,
|
||||
CACHE_CONTROL,
|
||||
EXPIRES,
|
||||
X_AMZ_OBJECT_LOCK_MODE.as_str(),
|
||||
X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str(),
|
||||
X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str(),
|
||||
X_AMZ_REPLICATION_STATUS.as_str(),
|
||||
X_AMZ_STORAGE_CLASS.as_str(),
|
||||
] {
|
||||
metadata.remove(key);
|
||||
}
|
||||
metadata_keys::OBJECT_LOCK_MODE,
|
||||
metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE,
|
||||
metadata_keys::OBJECT_LOCK_LEGAL_HOLD,
|
||||
metadata_keys::REPLICATION_STATUS,
|
||||
metadata_keys::STORAGE_CLASS,
|
||||
];
|
||||
metadata.retain(|key, _| !PROMOTED_KEYS.iter().any(|promoted| key.eq_ignore_ascii_case(promoted)));
|
||||
|
||||
for suffix in [
|
||||
rustfs_utils::http::metadata_compat::SUFFIX_TRANSITION_TRANSACTION_ID,
|
||||
@@ -2068,9 +2068,18 @@ mod tests {
|
||||
#[test]
|
||||
fn build_transition_put_options_preserves_object_lock_headers_when_present() {
|
||||
let mut metadata = HashMap::new();
|
||||
metadata.insert(X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(), "2026-03-23T00:00:00Z".to_string());
|
||||
metadata.insert(X_AMZ_OBJECT_LOCK_LEGAL_HOLD.to_string(), ObjectLockLegalHoldStatus::ON.to_string());
|
||||
metadata.insert(X_AMZ_OBJECT_LOCK_MODE.to_string(), ObjectLockRetentionMode::GOVERNANCE.to_string());
|
||||
metadata.insert(
|
||||
metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(),
|
||||
"2026-03-23T00:00:00Z".to_string(),
|
||||
);
|
||||
metadata.insert(
|
||||
metadata_keys::OBJECT_LOCK_LEGAL_HOLD.to_string(),
|
||||
ObjectLockLegalHoldStatus::ON.to_string(),
|
||||
);
|
||||
metadata.insert(
|
||||
metadata_keys::OBJECT_LOCK_MODE.to_string(),
|
||||
ObjectLockRetentionMode::GOVERNANCE.to_string(),
|
||||
);
|
||||
|
||||
let opts = build_transition_put_options("COLD".to_string(), metadata);
|
||||
|
||||
@@ -2084,15 +2093,35 @@ mod tests {
|
||||
let mut metadata = HashMap::new();
|
||||
metadata.insert("name".to_string(), "object".to_string());
|
||||
metadata.insert(CONTENT_TYPE.to_string(), "text/plain".to_string());
|
||||
metadata.insert(X_AMZ_OBJECT_LOCK_LEGAL_HOLD.to_string(), ObjectLockLegalHoldStatus::ON.to_string());
|
||||
metadata.insert(X_AMZ_REPLICATION_STATUS.to_string(), "PENDING".to_string());
|
||||
metadata.insert(
|
||||
metadata_keys::OBJECT_LOCK_LEGAL_HOLD.to_string(),
|
||||
ObjectLockLegalHoldStatus::ON.to_string(),
|
||||
);
|
||||
metadata.insert("x-amz-replication-status".to_string(), "PENDING".to_string());
|
||||
|
||||
let opts = build_transition_put_options("COLD".to_string(), metadata);
|
||||
|
||||
assert_eq!(opts.user_metadata.get("name"), Some(&"object".to_string()));
|
||||
assert!(!opts.user_metadata.contains_key(CONTENT_TYPE));
|
||||
assert!(!opts.user_metadata.contains_key(X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str()));
|
||||
assert!(!opts.user_metadata.contains_key(X_AMZ_REPLICATION_STATUS.as_str()));
|
||||
assert!(!opts.user_metadata.contains_key(metadata_keys::OBJECT_LOCK_LEGAL_HOLD));
|
||||
assert!(!opts.user_metadata.contains_key("x-amz-replication-status"));
|
||||
}
|
||||
|
||||
/// Object metadata read back from xl.meta carries the replication status
|
||||
/// under its persisted mixed-case key; it must not be forwarded to the
|
||||
/// tier as user metadata either.
|
||||
#[test]
|
||||
fn build_transition_put_options_filters_persisted_replication_status_key() {
|
||||
let metadata = HashMap::from([
|
||||
("name".to_string(), "object".to_string()),
|
||||
(metadata_keys::REPLICATION_STATUS.to_string(), "COMPLETED".to_string()),
|
||||
(metadata_keys::STORAGE_CLASS.to_string(), "STANDARD".to_string()),
|
||||
]);
|
||||
|
||||
let opts = build_transition_put_options("COLD".to_string(), metadata);
|
||||
|
||||
assert_eq!(opts.user_metadata.len(), 1, "{:?}", opts.user_metadata);
|
||||
assert_eq!(opts.user_metadata.get("name"), Some(&"object".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -23,6 +23,7 @@ use crate::disk::DiskOption;
|
||||
use crate::disk::endpoint::Endpoint;
|
||||
#[cfg(test)]
|
||||
use crate::disk::new_disk;
|
||||
use rustfs_filemeta::metadata_keys;
|
||||
use rustfs_utils::http;
|
||||
use sha2::Digest;
|
||||
|
||||
@@ -586,7 +587,7 @@ impl SetDisks {
|
||||
}
|
||||
|
||||
fn is_replication_quorum_metadata_key(name: &str) -> bool {
|
||||
if name.eq_ignore_ascii_case(http::AMZ_BUCKET_REPLICATION_STATUS) {
|
||||
if name.eq_ignore_ascii_case(metadata_keys::REPLICATION_STATUS) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,7 @@ use http::HeaderMap;
|
||||
use md5::{Digest as Md5Digest, Md5};
|
||||
use regex::Regex;
|
||||
use rustfs_config::MI_B;
|
||||
use rustfs_filemeta::metadata_keys;
|
||||
use rustfs_filemeta::{
|
||||
FileInfo, FileMeta, FileMetaShallowVersion, MetaCacheEntries, MetaCacheEntry, ObjectPartInfo, RawFileInfo,
|
||||
merge_file_meta_versions,
|
||||
@@ -147,7 +148,6 @@ use rustfs_s3_types::EventName;
|
||||
#[cfg(test)]
|
||||
use rustfs_utils::http::SSEC_ALGORITHM_HEADER;
|
||||
use rustfs_utils::http::headers::AMZ_OBJECT_TAGGING;
|
||||
use rustfs_utils::http::headers::AMZ_STORAGE_CLASS;
|
||||
use rustfs_utils::http::headers::{
|
||||
CACHE_CONTROL, CONTENT_DISPOSITION, CONTENT_ENCODING, CONTENT_LANGUAGE, CONTENT_TYPE, EXPIRES,
|
||||
};
|
||||
@@ -161,7 +161,6 @@ use rustfs_utils::{
|
||||
crypto::hex,
|
||||
path::{SLASH_SEPARATOR, encode_dir_object, has_suffix, path_join_buf},
|
||||
};
|
||||
use s3s::header::{X_AMZ_OBJECT_LOCK_LEGAL_HOLD, X_AMZ_OBJECT_LOCK_MODE, X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE, X_AMZ_RESTORE};
|
||||
use sha2::Sha256;
|
||||
use std::hash::{BuildHasher, Hash, Hasher};
|
||||
use std::mem::{self};
|
||||
@@ -213,7 +212,7 @@ pub(super) fn require_restore_operation_id(metadata: &HashMap<String, String>, e
|
||||
}
|
||||
|
||||
pub(super) fn restore_commit_operation_id_from_metadata(metadata: &HashMap<String, String>) -> Result<Option<Uuid>> {
|
||||
if !metadata.contains_key(X_AMZ_RESTORE.as_str()) {
|
||||
if !metadata.contains_key(metadata_keys::RESTORE) {
|
||||
return Ok(None);
|
||||
}
|
||||
restore_operation_id_from_metadata(metadata)
|
||||
@@ -5917,7 +5916,7 @@ impl SetDisks {
|
||||
}
|
||||
|
||||
let disks = self.disks.read().await.clone();
|
||||
let storage_class = opts.user_defined.get(AMZ_STORAGE_CLASS).map(String::as_str);
|
||||
let storage_class = opts.user_defined.get(metadata_keys::STORAGE_CLASS).map(String::as_str);
|
||||
let layout = resolve_write_layout(
|
||||
&storage_class_config,
|
||||
self.pool_index,
|
||||
@@ -10365,10 +10364,7 @@ mod tests {
|
||||
);
|
||||
assert!(meta_a.replication_state_internal.is_some());
|
||||
assert_eq!(
|
||||
meta_a
|
||||
.metadata
|
||||
.get(rustfs_utils::http::AMZ_BUCKET_REPLICATION_STATUS)
|
||||
.map(String::as_str),
|
||||
meta_a.metadata.get(metadata_keys::REPLICATION_STATUS).map(String::as_str),
|
||||
Some("COMPLETED")
|
||||
);
|
||||
|
||||
@@ -11659,11 +11655,11 @@ mod tests {
|
||||
|
||||
let mut user_defined = HashMap::new();
|
||||
user_defined.insert(
|
||||
X_AMZ_OBJECT_LOCK_MODE.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_MODE.to_string(),
|
||||
s3s::dto::ObjectLockRetentionMode::COMPLIANCE.to_string(),
|
||||
);
|
||||
user_defined.insert(
|
||||
X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(),
|
||||
existing_until.format(&time::format_description::well_known::Rfc3339).unwrap(),
|
||||
);
|
||||
|
||||
@@ -11694,11 +11690,11 @@ mod tests {
|
||||
|
||||
let mut user_defined = HashMap::new();
|
||||
user_defined.insert(
|
||||
X_AMZ_OBJECT_LOCK_MODE.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_MODE.to_string(),
|
||||
s3s::dto::ObjectLockRetentionMode::GOVERNANCE.to_string(),
|
||||
);
|
||||
user_defined.insert(
|
||||
X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(),
|
||||
existing_until.format(&time::format_description::well_known::Rfc3339).unwrap(),
|
||||
);
|
||||
|
||||
@@ -11724,11 +11720,11 @@ mod tests {
|
||||
let retain_until = OffsetDateTime::now_utc() + Duration::from_secs(60 * 60 * 24 * 60);
|
||||
let mut user_defined = HashMap::new();
|
||||
user_defined.insert(
|
||||
X_AMZ_OBJECT_LOCK_MODE.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_MODE.to_string(),
|
||||
s3s::dto::ObjectLockRetentionMode::COMPLIANCE.to_string(),
|
||||
);
|
||||
user_defined.insert(
|
||||
X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(),
|
||||
retain_until.format(&time::format_description::well_known::Rfc3339).unwrap(),
|
||||
);
|
||||
|
||||
@@ -11769,11 +11765,11 @@ mod tests {
|
||||
restore_expires: Some(restore_expiry),
|
||||
user_defined: Arc::new(HashMap::from([
|
||||
(
|
||||
X_AMZ_OBJECT_LOCK_MODE.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_MODE.to_string(),
|
||||
s3s::dto::ObjectLockRetentionMode::COMPLIANCE.to_string(),
|
||||
),
|
||||
(
|
||||
X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(),
|
||||
retain_until.format(&time::format_description::well_known::Rfc3339).unwrap(),
|
||||
),
|
||||
])),
|
||||
@@ -11843,11 +11839,11 @@ mod tests {
|
||||
let retain_until = OffsetDateTime::now_utc() + Duration::from_secs(60 * 60 * 24 * 60);
|
||||
let mut user_defined = HashMap::new();
|
||||
user_defined.insert(
|
||||
X_AMZ_OBJECT_LOCK_MODE.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_MODE.to_string(),
|
||||
s3s::dto::ObjectLockRetentionMode::COMPLIANCE.to_string(),
|
||||
);
|
||||
user_defined.insert(
|
||||
X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(),
|
||||
retain_until.format(&time::format_description::well_known::Rfc3339).unwrap(),
|
||||
);
|
||||
|
||||
@@ -11871,11 +11867,11 @@ mod tests {
|
||||
let retain_until = OffsetDateTime::now_utc() + Duration::from_secs(60 * 60 * 24 * 60);
|
||||
let mut user_defined = HashMap::new();
|
||||
user_defined.insert(
|
||||
X_AMZ_OBJECT_LOCK_MODE.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_MODE.to_string(),
|
||||
s3s::dto::ObjectLockRetentionMode::GOVERNANCE.to_string(),
|
||||
);
|
||||
user_defined.insert(
|
||||
X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(),
|
||||
retain_until.format(&time::format_description::well_known::Rfc3339).unwrap(),
|
||||
);
|
||||
ObjectInfo {
|
||||
@@ -11924,11 +11920,11 @@ mod tests {
|
||||
let retain_until = OffsetDateTime::now_utc() + Duration::from_secs(60 * 60 * 24 * 60);
|
||||
let mut user_defined = HashMap::new();
|
||||
user_defined.insert(
|
||||
X_AMZ_OBJECT_LOCK_MODE.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_MODE.to_string(),
|
||||
s3s::dto::ObjectLockRetentionMode::COMPLIANCE.to_string(),
|
||||
);
|
||||
user_defined.insert(
|
||||
X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(),
|
||||
retain_until.format(&time::format_description::well_known::Rfc3339).unwrap(),
|
||||
);
|
||||
let obj_info = ObjectInfo {
|
||||
@@ -11947,7 +11943,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_check_object_lock_delete_blocks_replicated_legal_hold_version_purge() {
|
||||
let mut user_defined = HashMap::new();
|
||||
user_defined.insert(X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str().to_string(), "ON".to_string());
|
||||
user_defined.insert(metadata_keys::OBJECT_LOCK_LEGAL_HOLD.to_string(), "ON".to_string());
|
||||
let obj_info = ObjectInfo {
|
||||
user_defined: Arc::new(user_defined),
|
||||
..Default::default()
|
||||
@@ -14560,7 +14556,7 @@ mod tests {
|
||||
let object = "object.txt";
|
||||
let mod_time = OffsetDateTime::from_unix_timestamp(1_717_171_717).expect("fixed timestamp should parse");
|
||||
let mut user_defined = HashMap::new();
|
||||
user_defined.insert(AMZ_STORAGE_CLASS.to_string(), storageclass::STANDARD.to_string());
|
||||
user_defined.insert(metadata_keys::STORAGE_CLASS.to_string(), storageclass::STANDARD.to_string());
|
||||
user_defined.insert(SUFFIX_COMPRESSION.to_string(), "zstd".to_string());
|
||||
let mut eval_metadata = HashMap::new();
|
||||
eval_metadata.insert("x-amz-meta-evaluated".to_string(), "yes".to_string());
|
||||
@@ -14584,7 +14580,7 @@ mod tests {
|
||||
assert_eq!(written.etag.as_deref(), Some("preserved-etag"));
|
||||
assert_eq!(written.mod_time, Some(mod_time));
|
||||
assert_eq!(written.user_defined.get("x-amz-meta-evaluated").map(String::as_str), Some("yes"));
|
||||
assert!(!written.user_defined.contains_key(AMZ_STORAGE_CLASS));
|
||||
assert!(!written.user_defined.contains_key(metadata_keys::STORAGE_CLASS));
|
||||
|
||||
let info = set_disks
|
||||
.get_object_info(bucket, object, &opts)
|
||||
@@ -14593,7 +14589,7 @@ mod tests {
|
||||
assert_eq!(info.etag.as_deref(), Some("preserved-etag"));
|
||||
assert_eq!(info.mod_time, Some(mod_time));
|
||||
assert_eq!(info.user_defined.get("x-amz-meta-evaluated").map(String::as_str), Some("yes"));
|
||||
assert!(!info.user_defined.contains_key(AMZ_STORAGE_CLASS));
|
||||
assert!(!info.user_defined.contains_key(metadata_keys::STORAGE_CLASS));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -29,23 +29,22 @@ use super::super::MetadataCacheInvalidationProbe;
|
||||
#[cfg(test)]
|
||||
use super::super::capacity_scope_from_disks;
|
||||
use super::super::{
|
||||
AMZ_STORAGE_CLASS, Arc, Bytes, CompletePart, Cursor, DATA_MOVEMENT_MULTIPART_PREFIX, DiskError, DiskStore,
|
||||
EVENT_SET_DISK_MULTIPART, Error, FileInfo, GLOBAL_MIN_PART_SIZE, HashAlgorithm, HashMap, HashReader, HashSet,
|
||||
HealChannelPriority, Instant, LOG_COMPONENT_ECSTORE, LOG_SUBSYSTEM_SET_DISK, ListMultipartsInfo, ListPartsInfo,
|
||||
MAX_PARTS_COUNT, MULTIPART_WRITE_QUORUM_RENAME_PART, MULTIPART_WRITE_QUORUM_UPLOAD_METADATA,
|
||||
MULTIPART_WRITE_QUORUM_WRITER_SETUP, MultipartInfo, MultipartUploadResult, MultipartWriteQuorumContext, NamespaceLockFence,
|
||||
OBJECT_OP_IGNORED_ERRS, ObjectInfo, ObjectLockDiagGuard, ObjectOptions, ObjectPartInfo, OffsetDateTime, PartInfo,
|
||||
PutObjReader, RUSTFS_META_MULTIPART_BUCKET, RUSTFS_META_TMP_BUCKET, RUSTFS_MULTIPART_BUCKET_KEY, RUSTFS_MULTIPART_OBJECT_KEY,
|
||||
Result, SLASH_SEPARATOR, SUFFIX_ACTUAL_OBJECT_SIZE_CAP, SUFFIX_ACTUAL_SIZE, SUFFIX_BUCKET_INCARNATION_ID,
|
||||
SUFFIX_COMPRESSION_SIZE, SUFFIX_REPLICATION_SSEC_CRC, SUFFIX_RESTORE_OPERATION_ID, SUFFIX_RESTORE_WORKER_LOCK, SetDisks,
|
||||
SmallWritePath, StorageError, Uuid, WriteLayout, check_object_lock_for_deletion_with_state,
|
||||
classify_multipart_part_write_path, coding, complete_multipart_part_error, complete_multipart_part_error_result,
|
||||
complete_part_checksum, completed_multipart_object_part, contains_key_str, create_bitrot_writer, debug, disk, error,
|
||||
get_complete_multipart_md5, get_header_map, get_str, insert_str, is_err_object_not_found, is_err_version_not_found,
|
||||
is_min_allowed_part_size, log_multipart_write_quorum_failure, parts_after_marker, path_join_buf,
|
||||
record_compression_total_memory, reduce_read_quorum_errs, reduce_write_quorum_errs, remove_header_map, resolve_write_layout,
|
||||
restore_commit_operation_id_from_metadata, should_persist_encryption_original_size, strip_internal_multipart_metadata,
|
||||
to_object_err, warn,
|
||||
Arc, Bytes, CompletePart, Cursor, DATA_MOVEMENT_MULTIPART_PREFIX, DiskError, DiskStore, EVENT_SET_DISK_MULTIPART, Error,
|
||||
FileInfo, GLOBAL_MIN_PART_SIZE, HashAlgorithm, HashMap, HashReader, HashSet, HealChannelPriority, Instant,
|
||||
LOG_COMPONENT_ECSTORE, LOG_SUBSYSTEM_SET_DISK, ListMultipartsInfo, ListPartsInfo, MAX_PARTS_COUNT,
|
||||
MULTIPART_WRITE_QUORUM_RENAME_PART, MULTIPART_WRITE_QUORUM_UPLOAD_METADATA, MULTIPART_WRITE_QUORUM_WRITER_SETUP,
|
||||
MultipartInfo, MultipartUploadResult, MultipartWriteQuorumContext, NamespaceLockFence, OBJECT_OP_IGNORED_ERRS, ObjectInfo,
|
||||
ObjectLockDiagGuard, ObjectOptions, ObjectPartInfo, OffsetDateTime, PartInfo, PutObjReader, RUSTFS_META_MULTIPART_BUCKET,
|
||||
RUSTFS_META_TMP_BUCKET, RUSTFS_MULTIPART_BUCKET_KEY, RUSTFS_MULTIPART_OBJECT_KEY, Result, SLASH_SEPARATOR,
|
||||
SUFFIX_ACTUAL_OBJECT_SIZE_CAP, SUFFIX_ACTUAL_SIZE, SUFFIX_BUCKET_INCARNATION_ID, SUFFIX_COMPRESSION_SIZE,
|
||||
SUFFIX_REPLICATION_SSEC_CRC, SUFFIX_RESTORE_OPERATION_ID, SUFFIX_RESTORE_WORKER_LOCK, SetDisks, SmallWritePath, StorageError,
|
||||
Uuid, WriteLayout, check_object_lock_for_deletion_with_state, classify_multipart_part_write_path, coding,
|
||||
complete_multipart_part_error, complete_multipart_part_error_result, complete_part_checksum, completed_multipart_object_part,
|
||||
contains_key_str, create_bitrot_writer, debug, disk, error, get_complete_multipart_md5, get_header_map, get_str, insert_str,
|
||||
is_err_object_not_found, is_err_version_not_found, is_min_allowed_part_size, log_multipart_write_quorum_failure,
|
||||
parts_after_marker, path_join_buf, record_compression_total_memory, reduce_read_quorum_errs, reduce_write_quorum_errs,
|
||||
remove_header_map, resolve_write_layout, restore_commit_operation_id_from_metadata, should_persist_encryption_original_size,
|
||||
strip_internal_multipart_metadata, to_object_err, warn,
|
||||
};
|
||||
use super::bitrot_self_verify::{BitrotSelfVerifyTarget, drop_failed_writer_disks, verify_written_bitrot_shards};
|
||||
#[cfg(test)]
|
||||
@@ -81,6 +80,7 @@ use crate::storage_api_contracts::object::ObjectOperations;
|
||||
use futures::{StreamExt, stream};
|
||||
#[cfg(test)]
|
||||
use http::HeaderMap;
|
||||
use rustfs_filemeta::metadata_keys;
|
||||
use rustfs_rio::EtagResolvable;
|
||||
use rustfs_rio::TryGetIndex;
|
||||
#[cfg(test)]
|
||||
@@ -1913,7 +1913,7 @@ impl crate::storage_api_contracts::multipart::MultipartOperations for SetDisks {
|
||||
// Extract storage class from metadata, default to STANDARD if not found
|
||||
let storage_class = fi
|
||||
.metadata
|
||||
.get(AMZ_STORAGE_CLASS)
|
||||
.get(metadata_keys::STORAGE_CLASS)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| storageclass::STANDARD.to_string());
|
||||
|
||||
@@ -2103,10 +2103,10 @@ impl crate::storage_api_contracts::multipart::MultipartOperations for SetDisks {
|
||||
user_defined.insert("etag".to_owned(), etag.clone());
|
||||
}
|
||||
|
||||
if let Some(sc) = user_defined.get(AMZ_STORAGE_CLASS)
|
||||
if let Some(sc) = user_defined.get(metadata_keys::STORAGE_CLASS)
|
||||
&& sc == storageclass::STANDARD
|
||||
{
|
||||
let _ = user_defined.remove(AMZ_STORAGE_CLASS);
|
||||
let _ = user_defined.remove(metadata_keys::STORAGE_CLASS);
|
||||
}
|
||||
|
||||
let WriteLayout {
|
||||
@@ -2118,7 +2118,7 @@ impl crate::storage_api_contracts::multipart::MultipartOperations for SetDisks {
|
||||
self.pool_index,
|
||||
disks.len(),
|
||||
self.default_parity_count,
|
||||
user_defined.get(AMZ_STORAGE_CLASS).map(String::as_str),
|
||||
user_defined.get(metadata_keys::STORAGE_CLASS).map(String::as_str),
|
||||
opts.max_parity,
|
||||
)?;
|
||||
|
||||
@@ -2160,10 +2160,10 @@ impl crate::storage_api_contracts::multipart::MultipartOperations for SetDisks {
|
||||
// TODO(backlog): detect content-type from part data when header is missing
|
||||
}
|
||||
|
||||
if let Some(sc) = user_defined.get(AMZ_STORAGE_CLASS)
|
||||
if let Some(sc) = user_defined.get(metadata_keys::STORAGE_CLASS)
|
||||
&& sc == storageclass::STANDARD
|
||||
{
|
||||
let _ = user_defined.remove(AMZ_STORAGE_CLASS);
|
||||
let _ = user_defined.remove(metadata_keys::STORAGE_CLASS);
|
||||
}
|
||||
|
||||
if let Some(checksum) = &opts.want_checksum {
|
||||
@@ -2398,7 +2398,7 @@ impl crate::storage_api_contracts::multipart::MultipartOperations for SetDisks {
|
||||
opts.replication_request || opts.delete_marker_replication_status() == ReplicationStatusType::Replica;
|
||||
if !authorized_inbound_replica {
|
||||
fi.metadata
|
||||
.retain(|key, _| !key.eq_ignore_ascii_case(rustfs_utils::http::AMZ_BUCKET_REPLICATION_STATUS));
|
||||
.retain(|key, _| !key.eq_ignore_ascii_case(metadata_keys::REPLICATION_STATUS));
|
||||
for suffix in [
|
||||
rustfs_utils::http::SUFFIX_REPLICA_STATUS,
|
||||
rustfs_utils::http::SUFFIX_REPLICA_TIMESTAMP,
|
||||
@@ -8783,8 +8783,7 @@ mod tests {
|
||||
rustfs_utils::http::SUFFIX_REPLICA_TIMESTAMP,
|
||||
"foreign-replica-time".to_string(),
|
||||
);
|
||||
create_replication_metadata
|
||||
.insert(rustfs_utils::http::AMZ_BUCKET_REPLICATION_STATUS.to_string(), "REPLICA".to_string());
|
||||
create_replication_metadata.insert(metadata_keys::REPLICATION_STATUS.to_string(), "REPLICA".to_string());
|
||||
let (upload_id, parts) = stage_upload_with_create_opts(
|
||||
&set_disks,
|
||||
bucket,
|
||||
@@ -8855,7 +8854,7 @@ mod tests {
|
||||
completed
|
||||
.user_defined
|
||||
.iter()
|
||||
.filter(|(key, _)| key.eq_ignore_ascii_case(rustfs_utils::http::AMZ_BUCKET_REPLICATION_STATUS))
|
||||
.filter(|(key, _)| key.eq_ignore_ascii_case(metadata_keys::REPLICATION_STATUS))
|
||||
.all(|(_, value)| value != "REPLICA")
|
||||
);
|
||||
|
||||
@@ -8911,7 +8910,7 @@ mod tests {
|
||||
rustfs_utils::http::SUFFIX_REPLICA_TIMESTAMP,
|
||||
"authorized-inbound-time".to_string(),
|
||||
);
|
||||
inbound_replica_metadata.insert(rustfs_utils::http::AMZ_BUCKET_REPLICATION_STATUS.to_string(), "REPLICA".to_string());
|
||||
inbound_replica_metadata.insert(metadata_keys::REPLICATION_STATUS.to_string(), "REPLICA".to_string());
|
||||
let (upload_id, parts) = stage_upload_with_create_opts(
|
||||
&set_disks,
|
||||
bucket,
|
||||
@@ -8952,7 +8951,7 @@ mod tests {
|
||||
inbound
|
||||
.user_defined
|
||||
.iter()
|
||||
.find(|(key, _)| key.eq_ignore_ascii_case(rustfs_utils::http::AMZ_BUCKET_REPLICATION_STATUS))
|
||||
.find(|(key, _)| key.eq_ignore_ascii_case(metadata_keys::REPLICATION_STATUS))
|
||||
.map(|(_, value)| value.as_str()),
|
||||
Some("REPLICA")
|
||||
);
|
||||
|
||||
@@ -20,27 +20,27 @@
|
||||
//! SetDisks core (io_primitives) via inherent calls.
|
||||
|
||||
use crate::core::pools::DecommissionCapacityAdmission;
|
||||
use rustfs_filemeta::metadata_keys;
|
||||
|
||||
#[cfg(test)]
|
||||
use super::super::MetadataCacheInvalidationProbe;
|
||||
use super::super::{
|
||||
AMZ_OBJECT_TAGGING, AMZ_STORAGE_CLASS, Arc, AsyncWrite, AtomicU64, BufReader, Bytes, CACHE_CONTROL, CONTENT_DISPOSITION,
|
||||
CONTENT_ENCODING, CONTENT_LANGUAGE, CONTENT_TYPE, CompletePart, Cursor, DeleteAccounting, DeleteOptions, DeletedObject,
|
||||
DiskError, DiskStore, EVENT_SET_DISK_COMMIT_TAIL_SLOW, EVENT_SET_DISK_PUT_OBJECT_STAGE_SUMMARY, EVENT_SET_DISK_WRITE,
|
||||
EXPIRES, Error, EventArgs, EventName, FastLockGuard, FileInfo, FileInfoVersions,
|
||||
GET_CODEC_STREAMING_OBJECT_CLASS_PLAIN_SINGLE_PART, GET_OBJECT_PATH_BODY_CACHE, GET_OBJECT_PATH_CODEC_STREAMING,
|
||||
GET_OBJECT_PATH_DIRECT_MEMORY, GET_OBJECT_PATH_EMPTY, GET_OBJECT_PATH_INLINE_DIRECT, GET_OBJECT_PATH_INTERNAL_META,
|
||||
GET_OBJECT_PATH_LEGACY_DUPLEX, GET_OBJECT_PATH_REMOTE_TRANSITION, GET_OBJECT_PATH_SET_DISK, GET_STAGE_DECODE, GET_STAGE_EMIT,
|
||||
GET_STAGE_INLINE_PREPARE, GET_STAGE_LOCK_ACQUIRE, GET_STAGE_METADATA, GET_STAGE_OBJECT_INFO, GET_STAGE_PATH_DECISION,
|
||||
GET_STAGE_READER_SETUP, GenericError, GetCodecStreamingDecision, GetCodecStreamingFallbackReason, GetDirectMemoryDecision,
|
||||
GetObjectReader, HTTPRangeSpec, HashAlgorithm, HashMap, HashReader, HashSet, HeaderMap, HealChannelPriority, InstanceContext,
|
||||
Instant, LOG_COMPONENT_ECSTORE, LOG_SUBSYSTEM_SET_DISK, OBJECT_OP_IGNORED_ERRS, ObjectApiError, ObjectInfo, ObjectKey,
|
||||
AMZ_OBJECT_TAGGING, Arc, AsyncWrite, AtomicU64, BufReader, Bytes, CACHE_CONTROL, CONTENT_DISPOSITION, CONTENT_ENCODING,
|
||||
CONTENT_LANGUAGE, CONTENT_TYPE, CompletePart, Cursor, DeleteAccounting, DeleteOptions, DeletedObject, DiskError, DiskStore,
|
||||
EVENT_SET_DISK_COMMIT_TAIL_SLOW, EVENT_SET_DISK_PUT_OBJECT_STAGE_SUMMARY, EVENT_SET_DISK_WRITE, EXPIRES, Error, EventArgs,
|
||||
EventName, FastLockGuard, FileInfo, FileInfoVersions, GET_CODEC_STREAMING_OBJECT_CLASS_PLAIN_SINGLE_PART,
|
||||
GET_OBJECT_PATH_BODY_CACHE, GET_OBJECT_PATH_CODEC_STREAMING, GET_OBJECT_PATH_DIRECT_MEMORY, GET_OBJECT_PATH_EMPTY,
|
||||
GET_OBJECT_PATH_INLINE_DIRECT, GET_OBJECT_PATH_INTERNAL_META, GET_OBJECT_PATH_LEGACY_DUPLEX,
|
||||
GET_OBJECT_PATH_REMOTE_TRANSITION, GET_OBJECT_PATH_SET_DISK, GET_STAGE_DECODE, GET_STAGE_EMIT, GET_STAGE_INLINE_PREPARE,
|
||||
GET_STAGE_LOCK_ACQUIRE, GET_STAGE_METADATA, GET_STAGE_OBJECT_INFO, GET_STAGE_PATH_DECISION, GET_STAGE_READER_SETUP,
|
||||
GenericError, GetCodecStreamingDecision, GetCodecStreamingFallbackReason, GetDirectMemoryDecision, GetObjectReader,
|
||||
HTTPRangeSpec, HashAlgorithm, HashMap, HashReader, HashSet, HeaderMap, HealChannelPriority, InstanceContext, Instant,
|
||||
LOG_COMPONENT_ECSTORE, LOG_SUBSYSTEM_SET_DISK, OBJECT_OP_IGNORED_ERRS, ObjectApiError, ObjectInfo, ObjectKey,
|
||||
ObjectLockConfigSnapshot, ObjectLockConfigState, ObjectOptions, ObjectReader, ObjectToDelete, OffsetDateTime, Ordering, Pin,
|
||||
PutObjReader, RUSTFS_META_BUCKET, RUSTFS_META_TMP_BUCKET, ReadPathPlan, ReaderImpl, ReplicateDecision,
|
||||
ReplicationObjectBridge, Result, SET_DISK_COMMIT_TAIL_WARN_THRESHOLD_MS, SLASH_SEPARATOR, SUFFIX_ACTUAL_SIZE,
|
||||
SUFFIX_COMPRESSION, SUFFIX_COMPRESSION_SIZE, SUFFIX_RESTORE_OPERATION_ID, SUFFIX_RESTORE_WORKER_LOCK, SetDisks,
|
||||
SmallWritePath, StorageError, TRANSITION_COMPLETE, UpdateMetadataOpts, Uuid, WriteLayout, X_AMZ_OBJECT_LOCK_LEGAL_HOLD,
|
||||
X_AMZ_OBJECT_LOCK_MODE, X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE, X_AMZ_RESTORE, adaptive_duplex_buffer_size,
|
||||
SmallWritePath, StorageError, TRANSITION_COMPLETE, UpdateMetadataOpts, Uuid, WriteLayout, adaptive_duplex_buffer_size,
|
||||
build_get_object_info, build_inline_bitrot_readers, build_inline_bitrot_readers_from_refs, can_try_inline_data_shards_direct,
|
||||
check_object_lock_delete, check_object_lock_for_deletion_with_state, check_object_lock_retention_update,
|
||||
classify_get_codec_streaming_object_class, classify_put_write_path, classify_storage_error,
|
||||
@@ -1831,9 +1831,9 @@ mod duration_metrics_tests {
|
||||
}
|
||||
|
||||
fn is_restore_control_metadata(key: &str) -> bool {
|
||||
key.eq_ignore_ascii_case(X_AMZ_RESTORE.as_str())
|
||||
|| key.eq_ignore_ascii_case(rustfs_utils::http::headers::AMZ_RESTORE_EXPIRY_DAYS)
|
||||
|| key.eq_ignore_ascii_case(rustfs_utils::http::headers::AMZ_RESTORE_REQUEST_DATE)
|
||||
key.eq_ignore_ascii_case(metadata_keys::RESTORE)
|
||||
|| key.eq_ignore_ascii_case(metadata_keys::RESTORE_EXPIRY_DAYS)
|
||||
|| key.eq_ignore_ascii_case(metadata_keys::RESTORE_REQUEST_DATE)
|
||||
|| rustfs_utils::http::internal_key_strip_suffix_prefix(key, SUFFIX_RESTORE_OPERATION_ID)
|
||||
.is_some_and(|remainder| remainder.is_empty())
|
||||
|| rustfs_utils::http::internal_key_strip_suffix_prefix(key, SUFFIX_RESTORE_WORKER_LOCK)
|
||||
@@ -1861,6 +1861,43 @@ fn restore_metadata_update_preserves_protected_metadata(
|
||||
mod restore_metadata_update_tests {
|
||||
use super::*;
|
||||
|
||||
/// The restore keys in pre-`metadata_keys` xl.meta are exactly the ones
|
||||
/// a restore metadata update may change; every other persisted key is
|
||||
/// protected (backlog#1735 A3b).
|
||||
#[test]
|
||||
fn pre_module_xlmeta_restore_keys_are_the_restore_control_keys() {
|
||||
let fi = rustfs_filemeta::FileMeta::load(
|
||||
&rustfs_filemeta::test_data::create_pre_metadata_keys_xlmeta().expect("decode fixture hex"),
|
||||
)
|
||||
.expect("load fixture xl.meta")
|
||||
.into_fileinfo("bucket", "object", "0b1e5a3a-1735-4a3a-8000-00000000a3a0", false, false, false)
|
||||
.expect("fixture version to FileInfo");
|
||||
|
||||
let mut control: Vec<&str> = fi
|
||||
.metadata
|
||||
.keys()
|
||||
.map(String::as_str)
|
||||
.filter(|key| is_restore_control_metadata(key))
|
||||
.collect();
|
||||
control.sort_unstable();
|
||||
assert_eq!(
|
||||
control,
|
||||
["X-Amz-Restore-Expiry-Days", "X-Amz-Restore-Request-Date", "x-amz-restore"],
|
||||
"restore control keys in the pre-module bytes"
|
||||
);
|
||||
for key in [
|
||||
metadata_keys::OBJECT_LOCK_LEGAL_HOLD,
|
||||
metadata_keys::OBJECT_LOCK_MODE,
|
||||
metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE,
|
||||
metadata_keys::SERVER_SIDE_ENCRYPTION,
|
||||
metadata_keys::STORAGE_CLASS,
|
||||
metadata_keys::REPLICATION_STATUS,
|
||||
] {
|
||||
assert!(fi.metadata.contains_key(key), "{key:?}");
|
||||
assert!(!is_restore_control_metadata(key), "{key:?} must stay protected");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restore_metadata_update_cannot_change_retention_or_user_metadata() {
|
||||
let mut existing = HashMap::from([
|
||||
@@ -1869,7 +1906,7 @@ mod restore_metadata_update_tests {
|
||||
("x-amz-object-lock-mode".to_string(), "COMPLIANCE".to_string()),
|
||||
]);
|
||||
let mut replacement = existing.clone();
|
||||
replacement.insert(X_AMZ_RESTORE.as_str().to_string(), "ongoing-request=\"true\"".to_string());
|
||||
replacement.insert(metadata_keys::RESTORE.to_string(), "ongoing-request=\"true\"".to_string());
|
||||
rustfs_utils::http::metadata_compat::insert_str(
|
||||
&mut replacement,
|
||||
SUFFIX_RESTORE_OPERATION_ID,
|
||||
@@ -1889,9 +1926,9 @@ mod restore_metadata_update_tests {
|
||||
replacement.insert("x-amz-meta-owner".to_string(), "mallory".to_string());
|
||||
assert!(!restore_metadata_update_preserves_protected_metadata(&existing, &replacement));
|
||||
|
||||
existing.insert(X_AMZ_RESTORE.as_str().to_string(), "ongoing-request=\"false\"".to_string());
|
||||
existing.insert(metadata_keys::RESTORE.to_string(), "ongoing-request=\"false\"".to_string());
|
||||
replacement.clone_from(&existing);
|
||||
replacement.remove(X_AMZ_RESTORE.as_str());
|
||||
replacement.remove(metadata_keys::RESTORE);
|
||||
assert!(restore_metadata_update_preserves_protected_metadata(&existing, &replacement));
|
||||
}
|
||||
}
|
||||
@@ -3161,9 +3198,7 @@ pub(in crate::set_disk) fn merge_replication_metadata_lww(
|
||||
existing: &HashMap<String, String>,
|
||||
opts: &ObjectOptions,
|
||||
) -> bool {
|
||||
use rustfs_utils::http::headers::{
|
||||
AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER, AMZ_OBJECT_LOCK_MODE_LOWER, AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER, AMZ_OBJECT_TAGGING,
|
||||
};
|
||||
use rustfs_utils::http::headers::AMZ_OBJECT_TAGGING;
|
||||
use rustfs_utils::http::metadata_compat::{
|
||||
SUFFIX_OBJECTLOCK_LEGALHOLD_TIMESTAMP, SUFFIX_OBJECTLOCK_RETENTION_TIMESTAMP, SUFFIX_TAGGING_TIMESTAMP, get_str,
|
||||
remove_str,
|
||||
@@ -3175,12 +3210,12 @@ pub(in crate::set_disk) fn merge_replication_metadata_lww(
|
||||
(
|
||||
opts.replication_retention_timestamp,
|
||||
SUFFIX_OBJECTLOCK_RETENTION_TIMESTAMP,
|
||||
&[AMZ_OBJECT_LOCK_MODE_LOWER, AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER],
|
||||
&[metadata_keys::OBJECT_LOCK_MODE, metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE],
|
||||
),
|
||||
(
|
||||
opts.replication_legalhold_timestamp,
|
||||
SUFFIX_OBJECTLOCK_LEGALHOLD_TIMESTAMP,
|
||||
&[AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER],
|
||||
&[metadata_keys::OBJECT_LOCK_LEGAL_HOLD],
|
||||
),
|
||||
];
|
||||
|
||||
@@ -3486,7 +3521,7 @@ impl SetDisks {
|
||||
self.pool_index,
|
||||
disks.len(),
|
||||
self.default_parity_count,
|
||||
user_defined.get(AMZ_STORAGE_CLASS).map(String::as_str),
|
||||
user_defined.get(metadata_keys::STORAGE_CLASS).map(String::as_str),
|
||||
opts.max_parity,
|
||||
)?;
|
||||
|
||||
@@ -3793,10 +3828,10 @@ impl SetDisks {
|
||||
fi.checksum = Some(content_hash.to_bytes(&[]));
|
||||
}
|
||||
|
||||
if let Some(sc) = user_defined.get(AMZ_STORAGE_CLASS)
|
||||
if let Some(sc) = user_defined.get(metadata_keys::STORAGE_CLASS)
|
||||
&& sc == storageclass::STANDARD
|
||||
{
|
||||
let _ = user_defined.remove(AMZ_STORAGE_CLASS);
|
||||
let _ = user_defined.remove(metadata_keys::STORAGE_CLASS);
|
||||
}
|
||||
|
||||
let mod_time = opts.mod_time;
|
||||
@@ -7546,7 +7581,7 @@ impl crate::storage_api_contracts::object::ObjectOperations for SetDisks {
|
||||
&& src_info
|
||||
.user_defined
|
||||
.keys()
|
||||
.any(|key| key.eq_ignore_ascii_case(X_AMZ_RESTORE.as_str()))
|
||||
.any(|key| key.eq_ignore_ascii_case(metadata_keys::RESTORE))
|
||||
&& restore_metadata_update_preserves_protected_metadata(&fi.metadata, src_info.user_defined.as_ref());
|
||||
if let Some(dst_version_id) = dst_opts.version_id.as_deref()
|
||||
&& !is_meta_bucketname(dst_bucket)
|
||||
@@ -9236,9 +9271,9 @@ impl crate::storage_api_contracts::object::ObjectOperations for SetDisks {
|
||||
CONTENT_DISPOSITION,
|
||||
CACHE_CONTROL,
|
||||
EXPIRES,
|
||||
X_AMZ_OBJECT_LOCK_MODE.as_str(),
|
||||
X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str(),
|
||||
X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str(),
|
||||
metadata_keys::OBJECT_LOCK_MODE,
|
||||
metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE,
|
||||
metadata_keys::OBJECT_LOCK_LEGAL_HOLD,
|
||||
] {
|
||||
if let Some(value) = fi.metadata.lookup(header).filter(|value| !value.is_empty()) {
|
||||
transition_meta.insert(header.to_ascii_lowercase(), value.to_string());
|
||||
@@ -9686,7 +9721,7 @@ impl crate::storage_api_contracts::object::ObjectOperations for SetDisks {
|
||||
}
|
||||
let mut restore_commit_metadata = if let Some(expected_operation_id) = expected_operation_id {
|
||||
let mut metadata = HashMap::new();
|
||||
metadata.insert(X_AMZ_RESTORE.as_str().to_string(), "ongoing-request=\"false\"".to_string());
|
||||
metadata.insert(metadata_keys::RESTORE.to_string(), "ongoing-request=\"false\"".to_string());
|
||||
rustfs_utils::http::metadata_compat::insert_str(
|
||||
&mut metadata,
|
||||
SUFFIX_RESTORE_OPERATION_ID,
|
||||
@@ -10673,9 +10708,7 @@ mod replication_lww_tests {
|
||||
|
||||
use super::hermetic_set_disks_support::hermetic_set_disks_isolated as hermetic_set_disks;
|
||||
use super::*;
|
||||
use rustfs_utils::http::headers::{
|
||||
AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER, AMZ_OBJECT_LOCK_MODE_LOWER, AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER, AMZ_OBJECT_TAGGING,
|
||||
};
|
||||
use rustfs_utils::http::headers::AMZ_OBJECT_TAGGING;
|
||||
use rustfs_utils::http::{
|
||||
SUFFIX_OBJECTLOCK_LEGALHOLD_TIMESTAMP, SUFFIX_OBJECTLOCK_RETENTION_TIMESTAMP, SUFFIX_TAGGING_TIMESTAMP, get_str,
|
||||
insert_str,
|
||||
@@ -10853,8 +10886,11 @@ mod replication_lww_tests {
|
||||
let mut inbound = HashMap::new();
|
||||
inbound.insert(AMZ_OBJECT_TAGGING.to_string(), "site=remote".to_string());
|
||||
insert_str(&mut inbound, SUFFIX_TAGGING_TIMESTAMP, T_OLD.to_string());
|
||||
inbound.insert(AMZ_OBJECT_LOCK_MODE_LOWER.to_string(), "COMPLIANCE".to_string());
|
||||
inbound.insert(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER.to_string(), "2028-01-01T00:00:00Z".to_string());
|
||||
inbound.insert(metadata_keys::OBJECT_LOCK_MODE.to_string(), "COMPLIANCE".to_string());
|
||||
inbound.insert(
|
||||
metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(),
|
||||
"2028-01-01T00:00:00Z".to_string(),
|
||||
);
|
||||
insert_str(&mut inbound, SUFFIX_OBJECTLOCK_RETENTION_TIMESTAMP, T_NEW.to_string());
|
||||
let opts = ObjectOptions {
|
||||
replication_request: true,
|
||||
@@ -10867,7 +10903,7 @@ mod replication_lww_tests {
|
||||
let info = version_info(&set_disks, bucket, object, &version_id).await;
|
||||
assert_eq!(info.user_tags.as_str(), "site=local", "the stale tagging category must keep local values");
|
||||
assert_eq!(
|
||||
info.user_defined.get(AMZ_OBJECT_LOCK_MODE_LOWER).map(String::as_str),
|
||||
info.user_defined.get(metadata_keys::OBJECT_LOCK_MODE).map(String::as_str),
|
||||
Some("COMPLIANCE"),
|
||||
"the newer retention category must be applied in the same write"
|
||||
);
|
||||
@@ -10887,12 +10923,12 @@ mod replication_lww_tests {
|
||||
// LWW-reachable divergence is a stale inbound ON resurrecting a hold
|
||||
// that was released more recently on this site.)
|
||||
let mut local = HashMap::new();
|
||||
local.insert(AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER.to_string(), "OFF".to_string());
|
||||
local.insert(metadata_keys::OBJECT_LOCK_LEGAL_HOLD.to_string(), "OFF".to_string());
|
||||
insert_str(&mut local, SUFFIX_OBJECTLOCK_LEGALHOLD_TIMESTAMP, T_LOCAL.to_string());
|
||||
put_version(&set_disks, bucket, object, &version_id, &versioned_opts(&version_id, local)).await;
|
||||
|
||||
let mut inbound = HashMap::new();
|
||||
inbound.insert(AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER.to_string(), "ON".to_string());
|
||||
inbound.insert(metadata_keys::OBJECT_LOCK_LEGAL_HOLD.to_string(), "ON".to_string());
|
||||
insert_str(&mut inbound, SUFFIX_OBJECTLOCK_LEGALHOLD_TIMESTAMP, T_OLD.to_string());
|
||||
let opts = ObjectOptions {
|
||||
replication_request: true,
|
||||
@@ -10903,7 +10939,9 @@ mod replication_lww_tests {
|
||||
|
||||
let info = version_info(&set_disks, bucket, object, &version_id).await;
|
||||
assert_eq!(
|
||||
info.user_defined.get(AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER).map(String::as_str),
|
||||
info.user_defined
|
||||
.get(metadata_keys::OBJECT_LOCK_LEGAL_HOLD)
|
||||
.map(String::as_str),
|
||||
Some("OFF"),
|
||||
"a stale inbound legal hold must not resurrect a hold released more recently"
|
||||
);
|
||||
@@ -10963,8 +11001,11 @@ mod replication_lww_tests {
|
||||
// path's eval_metadata stomped the metadata key with receiver-now
|
||||
// (simulated by T_NEW here).
|
||||
let mut inbound = HashMap::new();
|
||||
inbound.insert(AMZ_OBJECT_LOCK_MODE_LOWER.to_string(), "GOVERNANCE".to_string());
|
||||
inbound.insert(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER.to_string(), "2028-01-01T00:00:00Z".to_string());
|
||||
inbound.insert(metadata_keys::OBJECT_LOCK_MODE.to_string(), "GOVERNANCE".to_string());
|
||||
inbound.insert(
|
||||
metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(),
|
||||
"2028-01-01T00:00:00Z".to_string(),
|
||||
);
|
||||
insert_str(&mut inbound, SUFFIX_OBJECTLOCK_RETENTION_TIMESTAMP, T_NEW.to_string());
|
||||
let opts = ObjectOptions {
|
||||
replication_request: true,
|
||||
@@ -10979,7 +11020,10 @@ mod replication_lww_tests {
|
||||
Some(T_LOCAL),
|
||||
"the stored category timestamp must be the source-authored time, not the receiver's clock"
|
||||
);
|
||||
assert_eq!(info.user_defined.get(AMZ_OBJECT_LOCK_MODE_LOWER).map(String::as_str), Some("GOVERNANCE"));
|
||||
assert_eq!(
|
||||
info.user_defined.get(metadata_keys::OBJECT_LOCK_MODE).map(String::as_str),
|
||||
Some("GOVERNANCE")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -10991,7 +11035,7 @@ mod replication_lww_tests {
|
||||
make_bucket(&disk_stores, bucket).await;
|
||||
|
||||
let mut inbound = HashMap::new();
|
||||
inbound.insert(AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER.to_string(), "OFF".to_string());
|
||||
inbound.insert(metadata_keys::OBJECT_LOCK_LEGAL_HOLD.to_string(), "OFF".to_string());
|
||||
insert_str(&mut inbound, SUFFIX_OBJECTLOCK_LEGALHOLD_TIMESTAMP, T_OLD.to_string());
|
||||
let mut evaluated = inbound.clone();
|
||||
insert_str(&mut evaluated, SUFFIX_OBJECTLOCK_LEGALHOLD_TIMESTAMP, T_NEW.to_string());
|
||||
@@ -11045,10 +11089,13 @@ mod replication_lww_tests {
|
||||
/// plus an active COMPLIANCE retention (no retention timestamp).
|
||||
async fn seed_locked_version(set_disks: &Arc<SetDisks>, bucket: &str, object: &str, version_id: &str, hold_timestamp: &str) {
|
||||
let mut local = HashMap::new();
|
||||
local.insert(AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER.to_string(), "ON".to_string());
|
||||
local.insert(metadata_keys::OBJECT_LOCK_LEGAL_HOLD.to_string(), "ON".to_string());
|
||||
insert_str(&mut local, SUFFIX_OBJECTLOCK_LEGALHOLD_TIMESTAMP, hold_timestamp.to_string());
|
||||
local.insert(AMZ_OBJECT_LOCK_MODE_LOWER.to_string(), "COMPLIANCE".to_string());
|
||||
local.insert(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER.to_string(), "2099-01-01T00:00:00Z".to_string());
|
||||
local.insert(metadata_keys::OBJECT_LOCK_MODE.to_string(), "COMPLIANCE".to_string());
|
||||
local.insert(
|
||||
metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(),
|
||||
"2099-01-01T00:00:00Z".to_string(),
|
||||
);
|
||||
put_version(set_disks, bucket, object, version_id, &versioned_opts(version_id, local)).await;
|
||||
}
|
||||
|
||||
@@ -11057,10 +11104,13 @@ mod replication_lww_tests {
|
||||
/// category the source version has.
|
||||
fn inbound_legal_hold_release_opts(version_id: &str, timestamp: &str) -> ObjectOptions {
|
||||
let mut inbound = HashMap::new();
|
||||
inbound.insert(AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER.to_string(), "OFF".to_string());
|
||||
inbound.insert(metadata_keys::OBJECT_LOCK_LEGAL_HOLD.to_string(), "OFF".to_string());
|
||||
insert_str(&mut inbound, SUFFIX_OBJECTLOCK_LEGALHOLD_TIMESTAMP, timestamp.to_string());
|
||||
inbound.insert(AMZ_OBJECT_LOCK_MODE_LOWER.to_string(), "COMPLIANCE".to_string());
|
||||
inbound.insert(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER.to_string(), "2099-01-01T00:00:00Z".to_string());
|
||||
inbound.insert(metadata_keys::OBJECT_LOCK_MODE.to_string(), "COMPLIANCE".to_string());
|
||||
inbound.insert(
|
||||
metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(),
|
||||
"2099-01-01T00:00:00Z".to_string(),
|
||||
);
|
||||
insert_str(&mut inbound, SUFFIX_OBJECTLOCK_RETENTION_TIMESTAMP, T_OLD.to_string());
|
||||
ObjectOptions {
|
||||
replication_request: true,
|
||||
@@ -11094,13 +11144,15 @@ mod replication_lww_tests {
|
||||
|
||||
let info = version_info(&set_disks, bucket, object, &version_id).await;
|
||||
assert_eq!(
|
||||
info.user_defined.get(AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER).map(String::as_str),
|
||||
info.user_defined
|
||||
.get(metadata_keys::OBJECT_LOCK_LEGAL_HOLD)
|
||||
.map(String::as_str),
|
||||
Some("OFF"),
|
||||
"a newer source-side legal hold release must be applied to the locked replica"
|
||||
);
|
||||
assert_eq!(get_str(&info.user_defined, SUFFIX_OBJECTLOCK_LEGALHOLD_TIMESTAMP).as_deref(), Some(T_NEW));
|
||||
assert_eq!(
|
||||
info.user_defined.get(AMZ_OBJECT_LOCK_MODE_LOWER).map(String::as_str),
|
||||
info.user_defined.get(metadata_keys::OBJECT_LOCK_MODE).map(String::as_str),
|
||||
Some("COMPLIANCE"),
|
||||
"the untouched retention category must survive the write"
|
||||
);
|
||||
@@ -11128,7 +11180,9 @@ mod replication_lww_tests {
|
||||
|
||||
let info = version_info(&set_disks, bucket, object, &version_id).await;
|
||||
assert_eq!(
|
||||
info.user_defined.get(AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER).map(String::as_str),
|
||||
info.user_defined
|
||||
.get(metadata_keys::OBJECT_LOCK_LEGAL_HOLD)
|
||||
.map(String::as_str),
|
||||
Some("ON"),
|
||||
"a stale inbound release must not lift a hold applied more recently on this site"
|
||||
);
|
||||
@@ -11154,8 +11208,11 @@ mod replication_lww_tests {
|
||||
let mut inbound = HashMap::new();
|
||||
inbound.insert(AMZ_OBJECT_TAGGING.to_string(), "k=v".to_string());
|
||||
insert_str(&mut inbound, SUFFIX_TAGGING_TIMESTAMP, T_NEW.to_string());
|
||||
inbound.insert(AMZ_OBJECT_LOCK_MODE_LOWER.to_string(), "COMPLIANCE".to_string());
|
||||
inbound.insert(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER.to_string(), "2099-01-01T00:00:00Z".to_string());
|
||||
inbound.insert(metadata_keys::OBJECT_LOCK_MODE.to_string(), "COMPLIANCE".to_string());
|
||||
inbound.insert(
|
||||
metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(),
|
||||
"2099-01-01T00:00:00Z".to_string(),
|
||||
);
|
||||
let opts = ObjectOptions {
|
||||
replication_request: true,
|
||||
replication_tagging_timestamp: Some(parse_ts(T_NEW)),
|
||||
@@ -11171,7 +11228,12 @@ mod replication_lww_tests {
|
||||
assert!(matches!(err, StorageError::PrefixAccessDenied(_, _)), "unexpected error: {err}");
|
||||
|
||||
let info = version_info(&set_disks, bucket, object, &version_id).await;
|
||||
assert_eq!(info.user_defined.get(AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER).map(String::as_str), Some("ON"));
|
||||
assert_eq!(
|
||||
info.user_defined
|
||||
.get(metadata_keys::OBJECT_LOCK_LEGAL_HOLD)
|
||||
.map(String::as_str),
|
||||
Some("ON")
|
||||
);
|
||||
}
|
||||
|
||||
fn default_retention_snapshot(mode: &'static str) -> Arc<ObjectLockConfigSnapshot> {
|
||||
@@ -11209,7 +11271,7 @@ mod replication_lww_tests {
|
||||
seed_local_tagged_version(&set_disks, bucket, object, &version_id).await;
|
||||
let seeded = version_info(&set_disks, bucket, object, &version_id).await;
|
||||
assert!(
|
||||
!seeded.user_defined.contains_key(AMZ_OBJECT_LOCK_MODE_LOWER),
|
||||
!seeded.user_defined.contains_key(metadata_keys::OBJECT_LOCK_MODE),
|
||||
"the seeded version must be protected by the bucket default only"
|
||||
);
|
||||
|
||||
@@ -11254,7 +11316,7 @@ mod replication_lww_tests {
|
||||
let version_id = Uuid::new_v4().to_string();
|
||||
make_bucket(&disk_stores, bucket).await;
|
||||
let mut local = HashMap::new();
|
||||
local.insert(AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER.to_string(), "MAYBE".to_string());
|
||||
local.insert(metadata_keys::OBJECT_LOCK_LEGAL_HOLD.to_string(), "MAYBE".to_string());
|
||||
put_version(&set_disks, bucket, object, &version_id, &versioned_opts(&version_id, local)).await;
|
||||
|
||||
let mut reader = PutObjReader::from_vec(b"lww-body".to_vec());
|
||||
@@ -11265,7 +11327,12 @@ mod replication_lww_tests {
|
||||
assert!(!matches!(err, StorageError::PrefixAccessDenied(_, _)), "unexpected error: {err}");
|
||||
|
||||
let info = version_info(&set_disks, bucket, object, &version_id).await;
|
||||
assert_eq!(info.user_defined.get(AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER).map(String::as_str), Some("MAYBE"));
|
||||
assert_eq!(
|
||||
info.user_defined
|
||||
.get(metadata_keys::OBJECT_LOCK_LEGAL_HOLD)
|
||||
.map(String::as_str),
|
||||
Some("MAYBE")
|
||||
);
|
||||
}
|
||||
|
||||
/// The bypass is scoped to authorized replication writes: the same
|
||||
@@ -11291,7 +11358,12 @@ mod replication_lww_tests {
|
||||
assert!(matches!(err, StorageError::PrefixAccessDenied(_, _)), "unexpected error: {err}");
|
||||
|
||||
let info = version_info(&set_disks, bucket, object, &version_id).await;
|
||||
assert_eq!(info.user_defined.get(AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER).map(String::as_str), Some("ON"));
|
||||
assert_eq!(
|
||||
info.user_defined
|
||||
.get(metadata_keys::OBJECT_LOCK_LEGAL_HOLD)
|
||||
.map(String::as_str),
|
||||
Some("ON")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13223,7 +13295,10 @@ mod transition_commit_failure_tests {
|
||||
rustfs_utils::http::metadata_compat::SUFFIX_RESTORE_WORKER_LOCK,
|
||||
rustfs_utils::http::metadata_compat::RESTORE_WORKER_LOCK_PROTOCOL_V1.to_string(),
|
||||
);
|
||||
metadata.insert(s3s::header::X_AMZ_RESTORE.as_str().to_string(), format!("ongoing-request=\"{ongoing}\""));
|
||||
metadata.insert(
|
||||
rustfs_filemeta::metadata_keys::RESTORE.to_string(),
|
||||
format!("ongoing-request=\"{ongoing}\""),
|
||||
);
|
||||
metadata
|
||||
}
|
||||
|
||||
@@ -13416,7 +13491,7 @@ mod transition_commit_failure_tests {
|
||||
.await
|
||||
.expect("failed restore cleanup should leave the transitioned object readable");
|
||||
assert!(
|
||||
!cleaned.user_defined.contains_key(s3s::header::X_AMZ_RESTORE.as_str()),
|
||||
!cleaned.user_defined.contains_key(rustfs_filemeta::metadata_keys::RESTORE),
|
||||
"{point:?}: every post-snapshot failure must clean the public ongoing marker"
|
||||
);
|
||||
assert!(
|
||||
@@ -13492,7 +13567,7 @@ mod transition_commit_failure_tests {
|
||||
);
|
||||
let restore_header = restored
|
||||
.user_defined
|
||||
.get(s3s::header::X_AMZ_RESTORE.as_str())
|
||||
.get(rustfs_filemeta::metadata_keys::RESTORE)
|
||||
.expect("successful multipart restore must persist restore status");
|
||||
let restore_status = parse_restore_obj_status(restore_header).expect("successful restore status must be valid");
|
||||
assert!(!restore_status.on_going(), "successful multipart restore must not remain in progress");
|
||||
@@ -13978,7 +14053,7 @@ mod transition_commit_failure_tests {
|
||||
}
|
||||
return;
|
||||
}
|
||||
assert!(!cleaned.user_defined.contains_key(s3s::header::X_AMZ_RESTORE.as_str()));
|
||||
assert!(!cleaned.user_defined.contains_key(rustfs_filemeta::metadata_keys::RESTORE));
|
||||
assert!(
|
||||
rustfs_utils::http::get_str(cleaned.user_defined.as_ref(), rustfs_utils::http::SUFFIX_RESTORE_OPERATION_ID,)
|
||||
.is_none()
|
||||
@@ -14084,7 +14159,7 @@ mod transition_commit_failure_tests {
|
||||
assert_eq!(restored.version_id, Some(version_id), "restore must preserve the selected {case} version");
|
||||
let restore_header = restored
|
||||
.user_defined
|
||||
.get(s3s::header::X_AMZ_RESTORE.as_str())
|
||||
.get(rustfs_filemeta::metadata_keys::RESTORE)
|
||||
.expect("restored version must carry its completed restore status");
|
||||
let restore_status = parse_restore_obj_status(restore_header).expect("restore status must be valid");
|
||||
assert!(!restore_status.on_going(), "restored {case} version must not remain in progress");
|
||||
@@ -14744,7 +14819,7 @@ mod transition_commit_failure_tests {
|
||||
assert!(
|
||||
current_operation_b
|
||||
.user_defined
|
||||
.contains_key(s3s::header::X_AMZ_RESTORE.as_str()),
|
||||
.contains_key(rustfs_filemeta::metadata_keys::RESTORE),
|
||||
"stale cleanup for operation A must not remove operation B's restore header"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -14779,7 +14854,7 @@ mod transition_commit_failure_tests {
|
||||
.await
|
||||
.expect("cleaned object metadata should remain readable");
|
||||
assert!(
|
||||
!cleaned.user_defined.contains_key(s3s::header::X_AMZ_RESTORE.as_str()),
|
||||
!cleaned.user_defined.contains_key(rustfs_filemeta::metadata_keys::RESTORE),
|
||||
"matching cleanup must remove the restore header"
|
||||
);
|
||||
assert!(
|
||||
@@ -14848,7 +14923,7 @@ mod transition_commit_failure_tests {
|
||||
let restore_status = parse_restore_obj_status(
|
||||
current
|
||||
.user_defined
|
||||
.get(s3s::header::X_AMZ_RESTORE.as_str())
|
||||
.get(rustfs_filemeta::metadata_keys::RESTORE)
|
||||
.expect("restore header must remain pending"),
|
||||
)
|
||||
.expect("restore header should remain parseable");
|
||||
@@ -14912,7 +14987,7 @@ mod transition_commit_failure_tests {
|
||||
.await
|
||||
.expect("restore metadata should remain readable");
|
||||
assert!(
|
||||
current.user_defined.contains_key(s3s::header::X_AMZ_RESTORE.as_str()),
|
||||
current.user_defined.contains_key(rustfs_filemeta::metadata_keys::RESTORE),
|
||||
"lost no_lock cleanup must not remove the restore header"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -15130,7 +15205,7 @@ mod transition_commit_failure_tests {
|
||||
parse_restore_obj_status(
|
||||
current
|
||||
.user_defined
|
||||
.get(s3s::header::X_AMZ_RESTORE.as_str())
|
||||
.get(rustfs_filemeta::metadata_keys::RESTORE)
|
||||
.expect("operation B restore header should remain pending"),
|
||||
)
|
||||
.expect("operation B restore header should parse")
|
||||
@@ -15798,7 +15873,7 @@ mod transition_upload_integrity_tests {
|
||||
rustfs_filemeta::parse_restore_obj_status(
|
||||
current
|
||||
.user_defined
|
||||
.get(s3s::header::X_AMZ_RESTORE.as_str())
|
||||
.get(rustfs_filemeta::metadata_keys::RESTORE)
|
||||
.expect("pending restore header should remain"),
|
||||
)
|
||||
.expect("restore header should parse")
|
||||
@@ -19739,26 +19814,26 @@ mod put_object_tmp_cleanup_tests {
|
||||
"compliance",
|
||||
HashMap::from([
|
||||
(
|
||||
X_AMZ_OBJECT_LOCK_MODE.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_MODE.to_string(),
|
||||
s3s::dto::ObjectLockRetentionMode::COMPLIANCE.to_string(),
|
||||
),
|
||||
(X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str().to_string(), retain_until.clone()),
|
||||
(metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(), retain_until.clone()),
|
||||
]),
|
||||
),
|
||||
(
|
||||
"governance",
|
||||
HashMap::from([
|
||||
(
|
||||
X_AMZ_OBJECT_LOCK_MODE.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_MODE.to_string(),
|
||||
s3s::dto::ObjectLockRetentionMode::GOVERNANCE.to_string(),
|
||||
),
|
||||
(X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str().to_string(), retain_until),
|
||||
(metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(), retain_until),
|
||||
]),
|
||||
),
|
||||
(
|
||||
"legal-hold",
|
||||
HashMap::from([(
|
||||
X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_LEGAL_HOLD.to_string(),
|
||||
s3s::dto::ObjectLockLegalHoldStatus::ON.to_string(),
|
||||
)]),
|
||||
),
|
||||
@@ -20059,9 +20134,9 @@ mod put_object_tmp_cleanup_tests {
|
||||
object,
|
||||
&ObjectOptions {
|
||||
eval_metadata: Some(HashMap::from([
|
||||
(X_AMZ_OBJECT_LOCK_MODE.as_str().to_string(), String::new()),
|
||||
(X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str().to_string(), String::new()),
|
||||
(X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str().to_string(), String::new()),
|
||||
(metadata_keys::OBJECT_LOCK_MODE.to_string(), String::new()),
|
||||
(metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(), String::new()),
|
||||
(metadata_keys::OBJECT_LOCK_LEGAL_HOLD.to_string(), String::new()),
|
||||
])),
|
||||
..version_opts.clone()
|
||||
},
|
||||
@@ -20128,7 +20203,7 @@ mod put_object_tmp_cleanup_tests {
|
||||
version_id: Some(destination_version.clone()),
|
||||
versioned: true,
|
||||
eval_metadata: Some(HashMap::from([(
|
||||
X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_LEGAL_HOLD.to_string(),
|
||||
s3s::dto::ObjectLockLegalHoldStatus::ON.to_string(),
|
||||
)])),
|
||||
..Default::default()
|
||||
@@ -21089,11 +21164,11 @@ mod delete_objects_lock_gating_tests {
|
||||
let retain_until = OffsetDateTime::now_utc() + Duration::from_secs(60 * 60 * 24 * 30);
|
||||
let mut user_defined = HashMap::new();
|
||||
user_defined.insert(
|
||||
X_AMZ_OBJECT_LOCK_MODE.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_MODE.to_string(),
|
||||
s3s::dto::ObjectLockRetentionMode::COMPLIANCE.to_string(),
|
||||
);
|
||||
user_defined.insert(
|
||||
X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(),
|
||||
retain_until
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.expect("retain-until date should format"),
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
|
||||
use super::{
|
||||
Error, FileInfo, NamespaceLockFence, ObjectInfo, ObjectOptions, OffsetDateTime, Result, SetDisks, StorageError,
|
||||
UpdateMetadataOpts, Uuid, X_AMZ_RESTORE, get_raw_etag, restore_operation_id_from_metadata,
|
||||
UpdateMetadataOpts, Uuid, get_raw_etag, restore_operation_id_from_metadata,
|
||||
};
|
||||
use crate::bucket::lifecycle::lifecycle;
|
||||
use crate::core::pools::DecommissionCapacityAdmission;
|
||||
use rustfs_filemeta::RestoreStatusOps;
|
||||
use rustfs_utils::http::headers::{AMZ_RESTORE_EXPIRY_DAYS, AMZ_RESTORE_REQUEST_DATE};
|
||||
use rustfs_filemeta::metadata_keys;
|
||||
use s3s::dto::{RestoreStatus, Timestamp};
|
||||
#[cfg(all(test, feature = "test-util"))]
|
||||
use std::sync::Arc;
|
||||
@@ -213,7 +213,7 @@ impl SetDisks {
|
||||
let restore_expiry =
|
||||
lifecycle::expected_expiry_time(OffsetDateTime::now_utc(), opts.transition.restore_request.days.unwrap_or(1));
|
||||
fi.metadata.insert(
|
||||
X_AMZ_RESTORE.as_str().to_string(),
|
||||
metadata_keys::RESTORE.to_string(),
|
||||
RestoreStatus {
|
||||
is_restore_in_progress: Some(false),
|
||||
restore_expiry_date: Some(Timestamp::from(restore_expiry)),
|
||||
@@ -317,9 +317,9 @@ impl SetDisks {
|
||||
return Ok(());
|
||||
}
|
||||
ensure_restore_metadata_lock_held(bucket, object, opts, "restore_cleanup_metadata")?;
|
||||
fi.metadata.remove(X_AMZ_RESTORE.as_str());
|
||||
fi.metadata.remove(AMZ_RESTORE_EXPIRY_DAYS);
|
||||
fi.metadata.remove(AMZ_RESTORE_REQUEST_DATE);
|
||||
fi.metadata.remove(metadata_keys::RESTORE);
|
||||
fi.metadata.remove(metadata_keys::RESTORE_EXPIRY_DAYS);
|
||||
fi.metadata.remove(metadata_keys::RESTORE_REQUEST_DATE);
|
||||
rustfs_utils::http::metadata_compat::remove_str(
|
||||
&mut fi.metadata,
|
||||
rustfs_utils::http::metadata_compat::SUFFIX_RESTORE_OPERATION_ID,
|
||||
|
||||
@@ -145,7 +145,7 @@ async fn transition_and_restore_reclaim_prior_metadata_generations() {
|
||||
let restore_status = parse_restore_obj_status(
|
||||
restored_info
|
||||
.user_defined
|
||||
.get(s3s::header::X_AMZ_RESTORE.as_str())
|
||||
.get(rustfs_filemeta::metadata_keys::RESTORE)
|
||||
.expect("completed restore header should be present"),
|
||||
)
|
||||
.expect("completed restore header should parse");
|
||||
|
||||
@@ -5665,7 +5665,7 @@ mod tests {
|
||||
metadata_acknowledged_target,
|
||||
&ObjectOptions {
|
||||
eval_metadata: Some(HashMap::from([(
|
||||
s3s::header::X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str().to_string(),
|
||||
rustfs_filemeta::metadata_keys::OBJECT_LOCK_LEGAL_HOLD.to_string(),
|
||||
s3s::dto::ObjectLockLegalHoldStatus::OFF.to_string(),
|
||||
)])),
|
||||
..target_version_opts.clone()
|
||||
@@ -5694,7 +5694,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
metadata_acknowledged
|
||||
.user_defined
|
||||
.get(s3s::header::X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str())
|
||||
.get(rustfs_filemeta::metadata_keys::OBJECT_LOCK_LEGAL_HOLD)
|
||||
.map(String::as_str),
|
||||
Some("OFF")
|
||||
);
|
||||
@@ -5724,9 +5724,9 @@ mod tests {
|
||||
("governance-target.bin", s3s::dto::ObjectLockRetentionMode::GOVERNANCE),
|
||||
] {
|
||||
let retained_metadata = HashMap::from([
|
||||
(s3s::header::X_AMZ_OBJECT_LOCK_MODE.as_str().to_string(), mode.to_string()),
|
||||
(rustfs_filemeta::metadata_keys::OBJECT_LOCK_MODE.to_string(), mode.to_string()),
|
||||
(
|
||||
s3s::header::X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str().to_string(),
|
||||
rustfs_filemeta::metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(),
|
||||
retain_until.clone(),
|
||||
),
|
||||
]);
|
||||
@@ -12441,7 +12441,7 @@ mod tests {
|
||||
.await
|
||||
.expect("restored transitioned source should remain readable");
|
||||
assert!(
|
||||
restored.user_defined.contains_key(s3s::header::X_AMZ_RESTORE.as_str()),
|
||||
restored.user_defined.contains_key(rustfs_filemeta::metadata_keys::RESTORE),
|
||||
"restore completion metadata must be present before the delete regression"
|
||||
);
|
||||
}
|
||||
@@ -16748,11 +16748,11 @@ mod tests {
|
||||
&ObjectOptions {
|
||||
user_defined: HashMap::from([
|
||||
(
|
||||
s3s::header::X_AMZ_OBJECT_LOCK_MODE.as_str().to_string(),
|
||||
rustfs_filemeta::metadata_keys::OBJECT_LOCK_MODE.to_string(),
|
||||
s3s::dto::ObjectLockRetentionMode::COMPLIANCE.to_string(),
|
||||
),
|
||||
(
|
||||
s3s::header::X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str().to_string(),
|
||||
rustfs_filemeta::metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(),
|
||||
"2099-01-01T00:00:00Z".to_string(),
|
||||
),
|
||||
]),
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
use crate::config::storageclass::STANDARD;
|
||||
use crate::disk::{MIGRATING_META_BUCKET, RUSTFS_META_BUCKET};
|
||||
use regex::Regex;
|
||||
use rustfs_utils::http::headers::{AMZ_OBJECT_TAGGING, AMZ_STORAGE_CLASS};
|
||||
use rustfs_filemeta::metadata_keys;
|
||||
use rustfs_utils::http::headers::AMZ_OBJECT_TAGGING;
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Error, Result};
|
||||
use std::sync::LazyLock;
|
||||
@@ -32,8 +33,8 @@ pub fn clean_metadata(metadata: &mut HashMap<String, String>) {
|
||||
}
|
||||
|
||||
pub fn remove_standard_storage_class(metadata: &mut HashMap<String, String>) {
|
||||
if metadata.get(AMZ_STORAGE_CLASS) == Some(&STANDARD.to_string()) {
|
||||
metadata.remove(AMZ_STORAGE_CLASS);
|
||||
if metadata.get(metadata_keys::STORAGE_CLASS) == Some(&STANDARD.to_string()) {
|
||||
metadata.remove(metadata_keys::STORAGE_CLASS);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -206,12 +206,12 @@ mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use rustfs_scanner_metrics::metrics::IlmAction;
|
||||
use rustfs_storage_api::metadata_keys;
|
||||
use s3s::dto::{
|
||||
BucketLifecycleConfiguration, DefaultRetention, ExpirationStatus, LifecycleExpiration, LifecycleRule,
|
||||
NoncurrentVersionExpiration, ObjectLockConfiguration, ObjectLockEnabled, ObjectLockRetentionMode, ObjectLockRule,
|
||||
Transition, TransitionStorageClass,
|
||||
};
|
||||
use s3s::header::{X_AMZ_OBJECT_LOCK_LEGAL_HOLD, X_AMZ_OBJECT_LOCK_MODE, X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE};
|
||||
use time::OffsetDateTime;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -251,7 +251,7 @@ mod tests {
|
||||
for object in &mut objects {
|
||||
object
|
||||
.user_defined
|
||||
.insert(X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str().to_string(), "ON".to_string());
|
||||
.insert(metadata_keys::OBJECT_LOCK_LEGAL_HOLD.to_string(), "ON".to_string());
|
||||
}
|
||||
let locked = evaluator
|
||||
.eval(&objects)
|
||||
@@ -485,7 +485,7 @@ mod tests {
|
||||
|
||||
fn locked_current_object_opts(replication_status: ReplicationStatusType) -> ObjectOpts {
|
||||
let mut user_defined = HashMap::new();
|
||||
user_defined.insert(X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str().to_string(), "ON".to_string());
|
||||
user_defined.insert(metadata_keys::OBJECT_LOCK_LEGAL_HOLD.to_string(), "ON".to_string());
|
||||
|
||||
ObjectOpts {
|
||||
user_defined,
|
||||
@@ -507,10 +507,10 @@ mod tests {
|
||||
.expect("future retain-until date should format");
|
||||
let mut user_defined = HashMap::new();
|
||||
user_defined.insert(
|
||||
X_AMZ_OBJECT_LOCK_MODE.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_MODE.to_string(),
|
||||
s3s::dto::ObjectLockRetentionMode::COMPLIANCE.to_string(),
|
||||
);
|
||||
user_defined.insert(X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str().to_string(), retain_until);
|
||||
user_defined.insert(metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(), retain_until);
|
||||
|
||||
ObjectOpts {
|
||||
user_defined,
|
||||
@@ -717,7 +717,7 @@ mod tests {
|
||||
.eval(&version_group(
|
||||
Some(Uuid::nil()),
|
||||
ReplicationStatusType::Completed,
|
||||
HashMap::from([(X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str().to_string(), "ON".to_string())]),
|
||||
HashMap::from([(metadata_keys::OBJECT_LOCK_LEGAL_HOLD.to_string(), "ON".to_string())]),
|
||||
))
|
||||
.await
|
||||
.expect("locked null-version lifecycle evaluation should fail closed without aborting evaluation");
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rustfs_storage_api::metadata_keys;
|
||||
use s3s::dto::{ObjectLockConfiguration, ObjectLockRetentionMode};
|
||||
use s3s::header::{X_AMZ_OBJECT_LOCK_LEGAL_HOLD, X_AMZ_OBJECT_LOCK_MODE, X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE};
|
||||
use time::{OffsetDateTime, format_description};
|
||||
|
||||
pub fn is_object_locked_by_metadata(user_defined: &HashMap<String, String>, is_delete_marker: bool) -> bool {
|
||||
@@ -24,13 +24,13 @@ pub fn is_object_locked_by_metadata(user_defined: &HashMap<String, String>, is_d
|
||||
}
|
||||
|
||||
if user_defined
|
||||
.get(X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str())
|
||||
.get(metadata_keys::OBJECT_LOCK_LEGAL_HOLD)
|
||||
.is_some_and(|value| value.eq_ignore_ascii_case("ON"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
let Some(mode) = user_defined.get(X_AMZ_OBJECT_LOCK_MODE.as_str()) else {
|
||||
let Some(mode) = user_defined.get(metadata_keys::OBJECT_LOCK_MODE) else {
|
||||
return false;
|
||||
};
|
||||
if !is_retention_mode(mode) {
|
||||
@@ -38,7 +38,7 @@ pub fn is_object_locked_by_metadata(user_defined: &HashMap<String, String>, is_d
|
||||
}
|
||||
|
||||
user_defined
|
||||
.get(X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str())
|
||||
.get(metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE)
|
||||
.and_then(|value| OffsetDateTime::parse(value, &format_description::well_known::Iso8601::DEFAULT).ok())
|
||||
.is_some_and(|retain_until| retain_until.unix_timestamp() > OffsetDateTime::now_utc().unix_timestamp())
|
||||
}
|
||||
@@ -85,22 +85,22 @@ pub fn is_object_locked(
|
||||
}
|
||||
|
||||
fn has_explicit_lock_metadata(user_defined: &HashMap<String, String>) -> bool {
|
||||
user_defined.contains_key(X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str())
|
||||
|| user_defined.contains_key(X_AMZ_OBJECT_LOCK_MODE.as_str())
|
||||
|| user_defined.contains_key(X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str())
|
||||
user_defined.contains_key(metadata_keys::OBJECT_LOCK_LEGAL_HOLD)
|
||||
|| user_defined.contains_key(metadata_keys::OBJECT_LOCK_MODE)
|
||||
|| user_defined.contains_key(metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE)
|
||||
}
|
||||
|
||||
fn explicit_lock_metadata_is_well_formed(user_defined: &HashMap<String, String>) -> bool {
|
||||
if user_defined
|
||||
.get(X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str())
|
||||
.get(metadata_keys::OBJECT_LOCK_LEGAL_HOLD)
|
||||
.is_some_and(|value| !value.eq_ignore_ascii_case("ON") && !value.eq_ignore_ascii_case("OFF"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
match (
|
||||
user_defined.get(X_AMZ_OBJECT_LOCK_MODE.as_str()),
|
||||
user_defined.get(X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str()),
|
||||
user_defined.get(metadata_keys::OBJECT_LOCK_MODE),
|
||||
user_defined.get(metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE),
|
||||
) {
|
||||
(None, None) => true,
|
||||
(Some(mode), Some(retain_until)) => {
|
||||
@@ -142,7 +142,7 @@ mod tests {
|
||||
#[test]
|
||||
fn is_object_locked_by_metadata_preserves_object_lock_parser_behavior() {
|
||||
let mut user_defined = HashMap::new();
|
||||
user_defined.insert(X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str().to_string(), "ON".to_string());
|
||||
user_defined.insert(metadata_keys::OBJECT_LOCK_LEGAL_HOLD.to_string(), "ON".to_string());
|
||||
|
||||
assert!(is_object_locked_by_metadata(&user_defined, false));
|
||||
assert!(!is_object_locked_by_metadata(&user_defined, true));
|
||||
@@ -231,11 +231,11 @@ mod tests {
|
||||
let config = default_retention_config(30);
|
||||
let mut user_defined = HashMap::new();
|
||||
user_defined.insert(
|
||||
X_AMZ_OBJECT_LOCK_MODE.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_MODE.to_string(),
|
||||
ObjectLockRetentionMode::GOVERNANCE.to_string(),
|
||||
);
|
||||
user_defined.insert(
|
||||
X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string(),
|
||||
(OffsetDateTime::now_utc() - Duration::days(1))
|
||||
.format(&format_description::well_known::Iso8601::DEFAULT)
|
||||
.expect("expired retention date should format"),
|
||||
@@ -249,7 +249,7 @@ mod tests {
|
||||
let config = default_retention_config(1);
|
||||
let mut user_defined = HashMap::new();
|
||||
user_defined.insert(
|
||||
X_AMZ_OBJECT_LOCK_MODE.as_str().to_string(),
|
||||
metadata_keys::OBJECT_LOCK_MODE.to_string(),
|
||||
ObjectLockRetentionMode::GOVERNANCE.to_string(),
|
||||
);
|
||||
let created = OffsetDateTime::now_utc() - Duration::days(2);
|
||||
|
||||
@@ -101,6 +101,10 @@ pub use object::{VersionMarker, WalkOptions, WalkVersionsSortOrder};
|
||||
pub use observability::{
|
||||
MemorySamplingState, ObservabilitySnapshot, ObservabilitySnapshotProvider, PlatformSupport, UserspaceProfilingCapability,
|
||||
};
|
||||
/// Object-metadata keys persisted in xl.meta `meta_user`. filemeta owns the
|
||||
/// on-disk spelling; contract consumers that read or write persisted object
|
||||
/// metadata (lifecycle object-lock checks) take the keys from here.
|
||||
pub use rustfs_filemeta::metadata_keys;
|
||||
pub use topology::{
|
||||
DiskCapabilities, TopologyCapabilities, TopologyDisk, TopologyLabels, TopologyPool, TopologySet, TopologySnapshot,
|
||||
TopologySnapshotProvider,
|
||||
|
||||
@@ -61,14 +61,14 @@ cd "$(dirname "$0")/.."
|
||||
# 213 -> 212 on 2026-09-14: rustfs/backlog#1735 A4 moved rio's trailer
|
||||
# handle behind rustfs_rio::TrailerSource; the only adapter imports s3s through
|
||||
# the app storage_api shim, so crates/rio no longer references s3s.
|
||||
S3S_IMPORT_FILES_BASELINE=212
|
||||
S3S_IMPORT_FILES_BASELINE=210
|
||||
S3_ERROR_LINES_BASELINE=1588
|
||||
# ecstore-scoped ratchet (rustfs/backlog#1842): the storage engine must not
|
||||
# know S3 wire/DTO types (ARCHITECTURE.md invariant 4). The S3-*consuming*
|
||||
# client was extracted to crates/s3-client, where s3s usage is legitimate;
|
||||
# this counter ratchets the remaining serving-side s3s references out of
|
||||
# crates/ecstore. Baseline verified on 2026-08-26.
|
||||
S3S_ECSTORE_FILES_BASELINE=39
|
||||
S3S_ECSTORE_FILES_BASELINE=37
|
||||
S3S_PATH_PATTERN='(^|[^"[:alnum:]_])s3s::'
|
||||
E2E_TEST_GLOB='--glob=!crates/e2e_test/**'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user