mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-27 00:38:16 +00:00
271 lines
9.7 KiB
Rust
271 lines
9.7 KiB
Rust
// 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.
|
|
|
|
use crate::filemeta::{ReplicationAction, ReplicationType};
|
|
use crate::http::{
|
|
AMZ_OBJECT_LOCK_LEGAL_HOLD, AMZ_OBJECT_LOCK_MODE, AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE, AMZ_OBJECT_TAGGING,
|
|
AMZ_WEBSITE_REDIRECT_LOCATION, CACHE_CONTROL, CONTENT_DISPOSITION, CONTENT_ENCODING, CONTENT_LANGUAGE, EXPIRES,
|
|
has_prefix_fold, trim_etag,
|
|
};
|
|
use crate::tagging::ReplicationTagFilter;
|
|
use std::collections::HashMap;
|
|
use time::OffsetDateTime;
|
|
|
|
const AMZ_META_PREFIX: &str = "X-Amz-Meta-";
|
|
const CONTENT_ENCODING_LOWER: &str = "content-encoding";
|
|
const REPLICATION_METADATA_COMPARE_KEYS: [&str; 9] = [
|
|
EXPIRES,
|
|
CACHE_CONTROL,
|
|
CONTENT_LANGUAGE,
|
|
CONTENT_DISPOSITION,
|
|
AMZ_OBJECT_LOCK_MODE,
|
|
AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE,
|
|
AMZ_OBJECT_LOCK_LEGAL_HOLD,
|
|
AMZ_WEBSITE_REDIRECT_LOCATION,
|
|
AMZ_META_PREFIX,
|
|
];
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ReplicationSourceObject<'a> {
|
|
pub mod_time: Option<OffsetDateTime>,
|
|
pub version_id: Option<String>,
|
|
pub etag: Option<&'a str>,
|
|
pub actual_size: i64,
|
|
pub delete_marker: bool,
|
|
pub content_type: Option<&'a str>,
|
|
pub content_encoding: Option<&'a str>,
|
|
pub user_tags: &'a str,
|
|
pub user_defined: &'a HashMap<String, String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ReplicationTargetObject<'a> {
|
|
pub last_modified: Option<OffsetDateTime>,
|
|
pub version_id: Option<&'a str>,
|
|
pub etag: Option<&'a str>,
|
|
pub content_length: i64,
|
|
pub delete_marker: bool,
|
|
pub content_type: Option<&'a str>,
|
|
pub metadata: Option<&'a HashMap<String, String>>,
|
|
pub tag_count: i32,
|
|
}
|
|
|
|
pub fn content_matches_by_etag(source: &ReplicationSourceObject<'_>, target: &ReplicationTargetObject<'_>) -> bool {
|
|
replication_etags_match(source.etag, target.etag)
|
|
}
|
|
|
|
pub fn replication_etags_match(source: Option<&str>, target: Option<&str>) -> bool {
|
|
let source_etag = source.map(trim_etag);
|
|
let target_etag = target.map(trim_etag);
|
|
source_etag.is_some() && source_etag == target_etag
|
|
}
|
|
|
|
pub fn target_is_newer_than_source_null_version(
|
|
source: &ReplicationSourceObject<'_>,
|
|
target: &ReplicationTargetObject<'_>,
|
|
) -> bool {
|
|
target
|
|
.last_modified
|
|
.is_some_and(|target_mod_time| target_mod_time > source.mod_time.unwrap_or(OffsetDateTime::UNIX_EPOCH))
|
|
&& source.version_id.is_none()
|
|
}
|
|
|
|
pub fn replication_action_for_target(
|
|
source: &ReplicationSourceObject<'_>,
|
|
target: &ReplicationTargetObject<'_>,
|
|
op_type: ReplicationType,
|
|
) -> ReplicationAction {
|
|
if op_type == ReplicationType::ExistingObject && target_is_newer_than_source_null_version(source, target) {
|
|
return ReplicationAction::None;
|
|
}
|
|
|
|
if source.etag.map(trim_etag) != target.etag.map(trim_etag)
|
|
|| source.version_id.as_deref() != target.version_id
|
|
|| source.actual_size != target.content_length
|
|
|| source.delete_marker != target.delete_marker
|
|
|| source.mod_time != target.last_modified
|
|
{
|
|
return ReplicationAction::All;
|
|
}
|
|
|
|
if source.content_type != target.content_type {
|
|
return ReplicationAction::Metadata;
|
|
}
|
|
|
|
if content_encoding_differs(source, target) {
|
|
return ReplicationAction::Metadata;
|
|
}
|
|
|
|
if tag_metadata_differs(source, target) {
|
|
return ReplicationAction::Metadata;
|
|
}
|
|
|
|
if comparable_metadata(Some(source.user_defined)) != comparable_metadata(target.metadata) {
|
|
return ReplicationAction::Metadata;
|
|
}
|
|
|
|
ReplicationAction::None
|
|
}
|
|
|
|
fn content_encoding_differs(source: &ReplicationSourceObject<'_>, target: &ReplicationTargetObject<'_>) -> bool {
|
|
if let Some(content_encoding) = source.content_encoding {
|
|
return target
|
|
.metadata
|
|
.and_then(|metadata| {
|
|
metadata
|
|
.get(CONTENT_ENCODING)
|
|
.or_else(|| metadata.get(CONTENT_ENCODING_LOWER))
|
|
})
|
|
.is_none_or(|enc| enc != content_encoding);
|
|
}
|
|
false
|
|
}
|
|
|
|
fn tag_metadata_differs(source: &ReplicationSourceObject<'_>, target: &ReplicationTargetObject<'_>) -> bool {
|
|
let source_tags = ReplicationTagFilter::decode_tags_to_map(source.user_tags);
|
|
let target_tagging = target
|
|
.metadata
|
|
.and_then(|metadata| metadata.get(AMZ_OBJECT_TAGGING).map(String::as_str))
|
|
.unwrap_or_default();
|
|
let target_tags = ReplicationTagFilter::decode_tags_to_map(target_tagging);
|
|
let source_tag_count = i32::try_from(source_tags.len()).unwrap_or(i32::MAX);
|
|
|
|
(target.tag_count > 0 && source_tags != target_tags) || target.tag_count != source_tag_count
|
|
}
|
|
|
|
fn comparable_metadata(metadata: Option<&HashMap<String, String>>) -> HashMap<String, String> {
|
|
let mut comparable = HashMap::new();
|
|
for (key, value) in metadata.into_iter().flatten() {
|
|
if REPLICATION_METADATA_COMPARE_KEYS
|
|
.iter()
|
|
.any(|prefix| has_prefix_fold(key, prefix))
|
|
{
|
|
comparable.insert(key.to_lowercase(), value.clone());
|
|
}
|
|
}
|
|
comparable
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{
|
|
ReplicationSourceObject, ReplicationTargetObject, content_matches_by_etag, replication_action_for_target,
|
|
replication_etags_match, target_is_newer_than_source_null_version,
|
|
};
|
|
use crate::filemeta::{ReplicationAction, ReplicationType};
|
|
use std::collections::HashMap;
|
|
use time::{Duration, OffsetDateTime};
|
|
|
|
fn source_object(user_defined: &HashMap<String, String>) -> ReplicationSourceObject<'_> {
|
|
ReplicationSourceObject {
|
|
mod_time: Some(OffsetDateTime::UNIX_EPOCH + Duration::seconds(10)),
|
|
version_id: Some("source-version".to_string()),
|
|
etag: Some("\"abc\""),
|
|
actual_size: 10,
|
|
delete_marker: false,
|
|
content_type: Some("text/plain"),
|
|
content_encoding: None,
|
|
user_tags: "a=1",
|
|
user_defined,
|
|
}
|
|
}
|
|
|
|
fn target_object(metadata: &HashMap<String, String>) -> ReplicationTargetObject<'_> {
|
|
ReplicationTargetObject {
|
|
last_modified: Some(OffsetDateTime::UNIX_EPOCH + Duration::seconds(10)),
|
|
version_id: Some("source-version"),
|
|
etag: Some("abc"),
|
|
content_length: 10,
|
|
delete_marker: false,
|
|
content_type: Some("text/plain"),
|
|
metadata: Some(metadata),
|
|
tag_count: 1,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn content_matches_by_etag_ignores_version_ids() {
|
|
let source_metadata = HashMap::new();
|
|
let target_metadata = HashMap::new();
|
|
let source = ReplicationSourceObject {
|
|
version_id: Some("source-version".to_string()),
|
|
etag: Some("\"abc\""),
|
|
user_defined: &source_metadata,
|
|
..source_object(&source_metadata)
|
|
};
|
|
let target = ReplicationTargetObject {
|
|
version_id: Some("different-version"),
|
|
etag: Some("abc"),
|
|
metadata: Some(&target_metadata),
|
|
..target_object(&target_metadata)
|
|
};
|
|
|
|
assert!(content_matches_by_etag(&source, &target));
|
|
assert!(replication_etags_match(source.etag, target.etag));
|
|
}
|
|
|
|
#[test]
|
|
fn target_newer_null_version_skips_existing_object_replication() {
|
|
let source_metadata = HashMap::new();
|
|
let target_metadata = HashMap::new();
|
|
let source = ReplicationSourceObject {
|
|
version_id: None,
|
|
..source_object(&source_metadata)
|
|
};
|
|
let target = ReplicationTargetObject {
|
|
last_modified: Some(OffsetDateTime::UNIX_EPOCH + Duration::seconds(20)),
|
|
..target_object(&target_metadata)
|
|
};
|
|
|
|
assert!(target_is_newer_than_source_null_version(&source, &target));
|
|
assert_eq!(
|
|
replication_action_for_target(&source, &target, ReplicationType::ExistingObject),
|
|
ReplicationAction::None
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn replication_action_detects_content_and_metadata_differences() {
|
|
let mut source_metadata = HashMap::new();
|
|
source_metadata.insert("Cache-Control".to_string(), "max-age=1".to_string());
|
|
let mut target_metadata = HashMap::new();
|
|
target_metadata.insert("X-Amz-Tagging".to_string(), "a=1".to_string());
|
|
target_metadata.insert("Cache-Control".to_string(), "max-age=1".to_string());
|
|
|
|
let source = source_object(&source_metadata);
|
|
let target = target_object(&target_metadata);
|
|
assert_eq!(
|
|
replication_action_for_target(&source, &target, ReplicationType::ExistingObject),
|
|
ReplicationAction::None
|
|
);
|
|
|
|
let changed_content = ReplicationTargetObject {
|
|
content_length: 11,
|
|
..target_object(&target_metadata)
|
|
};
|
|
assert_eq!(
|
|
replication_action_for_target(&source, &changed_content, ReplicationType::ExistingObject),
|
|
ReplicationAction::All
|
|
);
|
|
|
|
let mut changed_target_metadata = target_metadata.clone();
|
|
changed_target_metadata.insert("Cache-Control".to_string(), "max-age=2".to_string());
|
|
let changed_metadata = target_object(&changed_target_metadata);
|
|
assert_eq!(
|
|
replication_action_for_target(&source, &changed_metadata, ReplicationType::ExistingObject),
|
|
ReplicationAction::Metadata
|
|
);
|
|
}
|
|
}
|