fix(disk): Fix Usage Report Capacity Calculation (#2274)

Co-authored-by: cxymds <Cxymds@qq.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
houseme
2026-03-24 23:47:30 +08:00
committed by GitHub
parent 8c8d157418
commit 19b8389dc4
19 changed files with 2990 additions and 14 deletions
+25 -9
View File
@@ -1013,7 +1013,7 @@ async fn handle_authenticated_request(
type SymlinkResolutionFuture<'a> =
Pin<Box<dyn std::future::Future<Output = Result<(String, String, String, Option<String>), SwiftError>> + Send + 'a>>;
/// Resolve symlink chain recursively
/// Resolve symlink chain recursively with circular reference detection
///
/// Returns (final_account, final_container, final_object, symlink_target_header)
/// where symlink_target_header is Some(target) if the original object was a symlink
@@ -1023,12 +1023,17 @@ fn resolve_symlink_chain<'a>(
object: &'a str,
credentials: &'a Option<Credentials>,
depth: u8,
visited: std::collections::HashSet<crate::swift::symlink::SymlinkPath>,
) -> SymlinkResolutionFuture<'a> {
Box::pin(async move {
use super::symlink;
// Validate depth to prevent infinite loops
symlink::validate_symlink_depth(depth)?;
// Validate both depth and circular references
symlink::validate_symlink_access(&visited, depth, account, container, object)?;
// Add current path to visited set
let mut new_visited = visited;
new_visited.insert(symlink::SymlinkPath::new(account, container, object));
// Get object metadata
let info = if let Some(creds) = credentials {
@@ -1041,14 +1046,14 @@ fn resolve_symlink_chain<'a>(
// Check if this object is a symlink
if let Some(target) = symlink::get_symlink_target(&info.user_defined)? {
let target_container = target.resolve_container(container);
let target_object = &target.object;
let target_object = target.object.clone();
// Store the original target for the response header
let target_header = target.to_header_value(container);
// Recursively resolve the target (it might also be a symlink)
let (final_account, final_container, final_object, _) =
resolve_symlink_chain(account, target_container, target_object, credentials, depth + 1).await?;
resolve_symlink_chain(account, target_container, &target_object, credentials, depth + 1, new_visited).await?;
// Return the final target, but keep the first-level symlink target for the header
Ok((final_account, final_container, final_object, Some(target_header)))
@@ -1059,6 +1064,18 @@ fn resolve_symlink_chain<'a>(
})
}
/// Helper function to start symlink resolution with an empty visited set
fn resolve_symlink_chain_wrapper<'a>(
account: &'a str,
container: &'a str,
object: &'a str,
credentials: &'a Option<Credentials>,
) -> SymlinkResolutionFuture<'a> {
Box::pin(
async move { resolve_symlink_chain(account, container, object, credentials, 0, std::collections::HashSet::new()).await },
)
}
/// Helper function for object GET operations (used by both authenticated and TempURL requests)
async fn handle_object_get(
account: &str,
@@ -1072,7 +1089,7 @@ async fn handle_object_get(
// Resolve symlinks first (with loop detection)
let (final_account, final_container, final_object, symlink_target) =
resolve_symlink_chain(account, container, object, credentials, 0).await?;
resolve_symlink_chain_wrapper(account, container, object, credentials).await?;
// Check if object is SLO (via metadata)
if slo::is_slo_object(&final_account, &final_container, &final_object, credentials).await? {
@@ -1213,7 +1230,7 @@ async fn handle_object_head(
) -> Result<Response<Body>, SwiftError> {
// Resolve symlinks first (with loop detection)
let (final_account, final_container, final_object, symlink_target) =
resolve_symlink_chain(account, container, object, credentials, 0).await?;
resolve_symlink_chain_wrapper(account, container, object, credentials).await?;
let info = if let Some(creds) = credentials {
object::head_object(&final_account, &final_container, &final_object, creds).await?
@@ -1437,8 +1454,7 @@ fn swift_error_to_response(error: SwiftError) -> Response<Body> {
#[cfg(test)]
mod tests {
use super::*;
use super::parse_range_header;
#[test]
fn test_parse_range_header_start_end() {
// bytes=100-199
+151 -1
View File
@@ -59,11 +59,34 @@
//! ```
use super::{SwiftError, SwiftResult};
use tracing::debug;
use std::collections::HashSet;
use tracing::{debug, warn};
/// Maximum symlink follow depth to prevent infinite loops
const MAX_SYMLINK_DEPTH: u8 = 5;
/// Symlink path used for loop detection
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SymlinkPath {
pub account: String,
pub container: String,
pub object: String,
}
impl SymlinkPath {
pub fn new(account: &str, container: &str, object: &str) -> Self {
Self {
account: account.to_string(),
container: container.to_string(),
object: object.to_string(),
}
}
pub fn from_strs(account: &str, container: &str, object: &str) -> Self {
Self::new(account, container, object)
}
}
/// Parsed symlink target
#[derive(Debug, Clone, PartialEq)]
pub struct SymlinkTarget {
@@ -167,6 +190,43 @@ pub fn validate_symlink_depth(depth: u8) -> SwiftResult<()> {
Ok(())
}
/// Check if a symlink path has been visited before (circular reference detection)
pub fn check_circular_reference(visited: &HashSet<SymlinkPath>, account: &str, container: &str, object: &str) -> SwiftResult<()> {
let path = SymlinkPath::new(account, container, object);
if visited.contains(&path) {
warn!(
account = %account,
container = %container,
object = %object,
"Circular symlink reference detected"
);
return Err(SwiftError::Conflict(format!(
"Circular symlink reference detected: {}/{}/{}",
account, container, object
)));
}
Ok(())
}
/// Validate symlink depth and check for circular references
pub fn validate_symlink_access(
visited: &HashSet<SymlinkPath>,
depth: u8,
account: &str,
container: &str,
object: &str,
) -> SwiftResult<()> {
// Check depth limit first
validate_symlink_depth(depth)?;
// Check for circular references
check_circular_reference(visited, account, container, object)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
@@ -310,4 +370,94 @@ mod tests {
assert!(validate_symlink_depth(5).is_err());
assert!(validate_symlink_depth(10).is_err());
}
#[test]
fn test_symlink_path_creation() {
let path = SymlinkPath::new("account1", "container1", "object1");
assert_eq!(path.account, "account1");
assert_eq!(path.container, "container1");
assert_eq!(path.object, "object1");
}
#[test]
fn test_symlink_path_equality() {
let path1 = SymlinkPath::new("account1", "container1", "object1");
let path2 = SymlinkPath::new("account1", "container1", "object1");
let path3 = SymlinkPath::new("account2", "container1", "object1");
assert_eq!(path1, path2);
assert_ne!(path1, path3);
}
#[test]
fn test_check_circular_reference_not_visited() {
let visited = HashSet::new();
assert!(check_circular_reference(&visited, "acc", "cont", "obj").is_ok());
}
#[test]
fn test_check_circular_reference_visited() {
let mut visited = HashSet::new();
visited.insert(SymlinkPath::new("acc", "cont", "obj"));
let result = check_circular_reference(&visited, "acc", "cont", "obj");
assert!(result.is_err());
if let Err(SwiftError::Conflict(msg)) = result {
assert!(msg.contains("Circular symlink reference detected"));
assert!(msg.contains("acc/cont/obj"));
} else {
panic!("Expected Conflict error");
}
}
#[test]
fn test_check_circular_reference_different_path() {
let mut visited = HashSet::new();
visited.insert(SymlinkPath::new("acc1", "cont1", "obj1"));
// Different path should not trigger circular reference error
assert!(check_circular_reference(&visited, "acc2", "cont2", "obj2").is_ok());
}
#[test]
fn test_validate_symlink_access_success() {
let visited = HashSet::new();
assert!(validate_symlink_access(&visited, 0, "acc", "cont", "obj").is_ok());
assert!(validate_symlink_access(&visited, 4, "acc", "cont", "obj").is_ok());
}
#[test]
fn test_validate_symlink_access_depth_exceeded() {
let visited = HashSet::new();
assert!(validate_symlink_access(&visited, 5, "acc", "cont", "obj").is_err());
assert!(validate_symlink_access(&visited, 10, "acc", "cont", "obj").is_err());
}
#[test]
fn test_validate_symlink_access_circular_reference() {
let mut visited = HashSet::new();
visited.insert(SymlinkPath::new("acc", "cont", "obj"));
let result = validate_symlink_access(&visited, 0, "acc", "cont", "obj");
assert!(result.is_err());
if let Err(SwiftError::Conflict(msg)) = result {
assert!(msg.contains("Circular symlink reference detected"));
} else {
panic!("Expected Conflict error");
}
}
#[test]
fn test_validate_symlink_access_both_checks() {
let mut visited = HashSet::new();
visited.insert(SymlinkPath::new("acc", "cont", "obj"));
// Should fail due to circular reference even though depth is OK
assert!(validate_symlink_access(&visited, 0, "acc", "cont", "obj").is_err());
// Should fail due to depth even though no circular reference
assert!(validate_symlink_access(&visited, 6, "acc2", "cont2", "obj2").is_err());
}
}