refactor(storage): extract list_objects v1 response builder (#1810)

This commit is contained in:
安正超
2026-02-14 11:50:15 +08:00
committed by GitHub
parent fb0267981d
commit 22ae004205
3 changed files with 129 additions and 53 deletions

View File

@@ -23,6 +23,7 @@ use crate::storage::head_prefix::{head_prefix_not_found_message, probe_prefix_ha
use crate::storage::helper::OperationHelper;
use crate::storage::options::{filter_object_metadata, get_content_sha256};
use crate::storage::readers::InMemoryAsyncReader;
use crate::storage::s3_api::bucket::build_list_objects_output;
use crate::storage::sse::{
DecryptionRequest, EncryptionRequest, PrepareEncryptionRequest, check_encryption_metadata, sse_decryption, sse_encryption,
sse_prepare_encryption, strip_managed_encryption_metadata,
@@ -3724,58 +3725,7 @@ impl S3 for FS {
}))
.await?;
Ok(v2_resp.map_output(|v2| {
// For ListObjects (v1) API, NextMarker should be the last item returned when truncated
// When both Contents and CommonPrefixes are present, NextMarker should be the
// lexicographically last item (either last key or last prefix)
let next_marker = if v2.is_truncated.unwrap_or(false) {
let last_key = v2
.contents
.as_ref()
.and_then(|contents| contents.last())
.and_then(|obj| obj.key.as_ref())
.cloned();
let last_prefix = v2
.common_prefixes
.as_ref()
.and_then(|prefixes| prefixes.last())
.and_then(|prefix| prefix.prefix.as_ref())
.cloned();
// NextMarker should be the lexicographically last item
// This matches S3 standard behavior
match (last_key, last_prefix) {
(Some(k), Some(p)) => {
// Return the lexicographically greater one
if k > p { Some(k) } else { Some(p) }
}
(Some(k), None) => Some(k),
(None, Some(p)) => Some(p),
(None, None) => None,
}
} else {
None
};
// S3 API requires marker field in response, echoing back the request marker
// If no marker was provided in request, return empty string per S3 standard
let marker = Some(request_marker.unwrap_or_default());
ListObjectsOutput {
contents: v2.contents,
delimiter: v2.delimiter,
encoding_type: v2.encoding_type,
name: v2.name,
prefix: v2.prefix,
max_keys: v2.max_keys,
common_prefixes: v2.common_prefixes,
is_truncated: v2.is_truncated,
marker,
next_marker,
..Default::default()
}
}))
Ok(v2_resp.map_output(move |v2| build_list_objects_output(v2, request_marker.clone())))
}
#[instrument(level = "debug", skip(self, req))]

View File

@@ -0,0 +1,126 @@
// 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 s3s::dto::{ListObjectsOutput, ListObjectsV2Output};
pub(crate) fn build_list_objects_output(v2: ListObjectsV2Output, request_marker: Option<String>) -> ListObjectsOutput {
let next_marker = calculate_next_marker(&v2);
// S3 API requires marker field in response, echoing back the request marker.
// If no marker was provided in request, return empty string per S3 standard.
let marker = Some(request_marker.unwrap_or_default());
ListObjectsOutput {
contents: v2.contents,
delimiter: v2.delimiter,
encoding_type: v2.encoding_type,
name: v2.name,
prefix: v2.prefix,
max_keys: v2.max_keys,
common_prefixes: v2.common_prefixes,
is_truncated: v2.is_truncated,
marker,
next_marker,
..Default::default()
}
}
fn calculate_next_marker(v2: &ListObjectsV2Output) -> Option<String> {
// For ListObjects (v1) API, NextMarker should be the last item returned when truncated.
// When both Contents and CommonPrefixes are present, NextMarker should be the
// lexicographically last item (either last key or last prefix).
if !v2.is_truncated.unwrap_or(false) {
return None;
}
let last_key = v2
.contents
.as_ref()
.and_then(|contents| contents.last())
.and_then(|obj| obj.key.as_ref())
.cloned();
let last_prefix = v2
.common_prefixes
.as_ref()
.and_then(|prefixes| prefixes.last())
.and_then(|prefix| prefix.prefix.as_ref())
.cloned();
// NextMarker should be the lexicographically last item.
// This matches S3 standard behavior.
match (last_key, last_prefix) {
(Some(k), Some(p)) => {
if k > p {
Some(k)
} else {
Some(p)
}
}
(Some(k), None) => Some(k),
(None, Some(p)) => Some(p),
(None, None) => None,
}
}
#[cfg(test)]
mod tests {
use super::build_list_objects_output;
use s3s::dto::{CommonPrefix, ListObjectsV2Output, Object};
#[test]
fn test_list_objects_marker_echoes_request_value() {
let output = build_list_objects_output(ListObjectsV2Output::default(), Some("m-1".to_string()));
assert_eq!(output.marker, Some("m-1".to_string()));
}
#[test]
fn test_list_objects_marker_defaults_to_empty_string() {
let output = build_list_objects_output(ListObjectsV2Output::default(), None);
assert_eq!(output.marker, Some(String::new()));
}
#[test]
fn test_list_objects_next_marker_uses_lexicographically_last_item() {
let v2 = ListObjectsV2Output {
is_truncated: Some(true),
contents: Some(vec![Object {
key: Some("apple".to_string()),
..Default::default()
}]),
common_prefixes: Some(vec![CommonPrefix {
prefix: Some("zebra/".to_string()),
}]),
..Default::default()
};
let output = build_list_objects_output(v2, None);
assert_eq!(output.next_marker, Some("zebra/".to_string()));
}
#[test]
fn test_list_objects_next_marker_is_none_when_not_truncated() {
let v2 = ListObjectsV2Output {
is_truncated: Some(false),
contents: Some(vec![Object {
key: Some("only-item".to_string()),
..Default::default()
}]),
..Default::default()
};
let output = build_list_objects_output(v2, None);
assert_eq!(output.next_marker, None);
}
}

View File

@@ -18,7 +18,7 @@
//! until each helper is moved with dedicated small refactor steps.
pub(crate) mod acl {}
pub(crate) mod bucket {}
pub(crate) mod bucket;
pub(crate) mod encryption {}
pub(crate) mod multipart {}
/// Object helper facade placeholder.