mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-12 08:06:54 +00:00
fix(scanner): preserve background heal compatibility (#3041)
This commit is contained in:
@@ -20,7 +20,7 @@ use bytes::Bytes;
|
||||
use http::{HeaderMap, HeaderValue, Uri};
|
||||
use hyper::{Method, StatusCode};
|
||||
use matchit::Params;
|
||||
use rustfs_common::heal_channel::HealOpts;
|
||||
use rustfs_common::heal_channel::{HealChannelPriority, HealChannelRequest, HealOpts};
|
||||
use rustfs_config::MAX_HEAL_REQUEST_SIZE;
|
||||
use rustfs_ecstore::bucket::utils::is_valid_object_prefix;
|
||||
use rustfs_ecstore::new_object_layer_fn;
|
||||
@@ -29,10 +29,11 @@ use rustfs_ecstore::store_utils::is_reserved_or_invalid_bucket;
|
||||
use rustfs_policy::policy::action::{Action, AdminAction};
|
||||
use rustfs_scanner::scanner::{BackgroundHealInfo, read_background_heal_info};
|
||||
use rustfs_utils::path::path_join;
|
||||
use s3s::header::CONTENT_TYPE;
|
||||
use s3s::header::{CONTENT_LENGTH, CONTENT_TYPE};
|
||||
use s3s::{Body, S3Request, S3Response, S3Result, s3_error};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{info, warn};
|
||||
@@ -75,7 +76,7 @@ fn extract_heal_init_params(body: &Bytes, uri: &Uri, params: Params<'_, '_>) ->
|
||||
}
|
||||
}
|
||||
|
||||
if (hip.force_start && hip.force_stop) || (!hip.client_token.is_empty() && (hip.force_start || hip.force_stop)) {
|
||||
if hip.force_start && (hip.force_stop || !hip.client_token.is_empty()) {
|
||||
return Err(s3_error!(
|
||||
InvalidRequest,
|
||||
"invalid combination of clientToken, forceStart, and forceStop parameters"
|
||||
@@ -142,6 +143,34 @@ struct HealResp {
|
||||
api_err: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct HealStartSuccess {
|
||||
client_token: String,
|
||||
client_address: String,
|
||||
start_time: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct HealTaskStatus {
|
||||
summary: String,
|
||||
#[serde(rename = "detail")]
|
||||
failure_detail: String,
|
||||
start_time: String,
|
||||
#[serde(rename = "settings")]
|
||||
heal_settings: HealOpts,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
items: Vec<rustfs_madmin::heal_commands::HealResultItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct HealTaskStatusPayload {
|
||||
summary: String,
|
||||
#[serde(default)]
|
||||
items: Vec<rustfs_madmin::heal_commands::HealResultItem>,
|
||||
}
|
||||
|
||||
fn map_heal_response(result: Option<HealResp>) -> S3Result<(StatusCode, Vec<u8>)> {
|
||||
match result {
|
||||
Some(result) => {
|
||||
@@ -149,12 +178,105 @@ fn map_heal_response(result: Option<HealResp>) -> S3Result<(StatusCode, Vec<u8>)
|
||||
return Err(s3_error!(InternalError, "{err}"));
|
||||
}
|
||||
|
||||
if result.resp_bytes.is_empty() {
|
||||
return Err(s3_error!(InternalError, "heal response body is empty"));
|
||||
}
|
||||
|
||||
Ok((StatusCode::OK, result.resp_bytes))
|
||||
}
|
||||
None => Err(s3_error!(InternalError, "heal channel closed unexpectedly")),
|
||||
}
|
||||
}
|
||||
|
||||
fn current_rfc3339_time() -> S3Result<String> {
|
||||
OffsetDateTime::now_utc()
|
||||
.format(&Rfc3339)
|
||||
.map_err(|e| s3_error!(InternalError, "failed to format heal timestamp: {e}"))
|
||||
}
|
||||
|
||||
fn encode_json<T: Serialize>(value: &T) -> S3Result<Vec<u8>> {
|
||||
serde_json::to_vec(value).map_err(|e| s3_error!(InternalError, "failed to serialize heal response: {e}"))
|
||||
}
|
||||
|
||||
fn encode_heal_start_success(client_token: String, client_address: String) -> S3Result<Vec<u8>> {
|
||||
encode_json(&HealStartSuccess {
|
||||
client_token,
|
||||
client_address,
|
||||
start_time: current_rfc3339_time()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn encode_heal_task_status(
|
||||
summary: String,
|
||||
failure_detail: String,
|
||||
heal_settings: HealOpts,
|
||||
items: Vec<rustfs_madmin::heal_commands::HealResultItem>,
|
||||
) -> S3Result<Vec<u8>> {
|
||||
encode_json(&HealTaskStatus {
|
||||
summary,
|
||||
failure_detail,
|
||||
start_time: current_rfc3339_time()?,
|
||||
heal_settings,
|
||||
items,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_heal_channel_request(hip: &HealInitParams) -> HealChannelRequest {
|
||||
let mut heal_request = rustfs_common::heal_channel::create_heal_request(
|
||||
hip.bucket.clone(),
|
||||
if hip.obj_prefix.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(hip.obj_prefix.clone())
|
||||
},
|
||||
hip.force_start,
|
||||
Some(HealChannelPriority::Normal),
|
||||
);
|
||||
|
||||
heal_request.pool_index = hip.hs.pool;
|
||||
heal_request.set_index = hip.hs.set;
|
||||
heal_request.scan_mode = Some(hip.hs.scan_mode);
|
||||
heal_request.remove_corrupted = Some(hip.hs.remove);
|
||||
heal_request.recreate_missing = Some(hip.hs.recreate);
|
||||
heal_request.update_parity = Some(hip.hs.update_parity);
|
||||
heal_request.recursive = Some(hip.hs.recursive);
|
||||
heal_request.dry_run = Some(hip.hs.dry_run);
|
||||
heal_request
|
||||
}
|
||||
|
||||
fn heal_channel_response_status(
|
||||
response: &rustfs_common::heal_channel::HealChannelResponse,
|
||||
) -> (String, Vec<rustfs_madmin::heal_commands::HealResultItem>) {
|
||||
let Some(data) = response.data.as_deref() else {
|
||||
return ("running".to_string(), Vec::new());
|
||||
};
|
||||
|
||||
if let Ok(payload) = serde_json::from_slice::<HealTaskStatusPayload>(data)
|
||||
&& !payload.summary.is_empty()
|
||||
{
|
||||
return (payload.summary, payload.items);
|
||||
}
|
||||
|
||||
let summary = std::str::from_utf8(data)
|
||||
.ok()
|
||||
.filter(|summary| !summary.is_empty())
|
||||
.unwrap_or("running")
|
||||
.to_string();
|
||||
(summary, Vec::new())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn heal_channel_response_summary(response: &rustfs_common::heal_channel::HealChannelResponse) -> String {
|
||||
heal_channel_response_status(response).0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn heal_channel_response_items(
|
||||
response: &rustfs_common::heal_channel::HealChannelResponse,
|
||||
) -> Vec<rustfs_madmin::heal_commands::HealResultItem> {
|
||||
heal_channel_response_status(response).1
|
||||
}
|
||||
|
||||
fn encode_background_heal_status(info: &BackgroundHealInfo) -> S3Result<Vec<u8>> {
|
||||
serde_json::to_vec(info).map_err(|e| s3_error!(InternalError, "failed to serialize background heal status: {e}"))
|
||||
}
|
||||
@@ -185,6 +307,9 @@ fn map_root_heal_status(heal_err: Option<rustfs_ecstore::error::Error>) -> S3Res
|
||||
fn json_response(status: StatusCode, body: Vec<u8>) -> S3Response<(StatusCode, Body)> {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
|
||||
if let Ok(value) = HeaderValue::from_str(&body.len().to_string()) {
|
||||
headers.insert(CONTENT_LENGTH, value);
|
||||
}
|
||||
S3Response::with_headers((status, Body::from(body)), headers)
|
||||
}
|
||||
|
||||
@@ -213,6 +338,11 @@ impl Operation for HealHandler {
|
||||
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
warn!("handle HealHandler, req: {:?}, params: {:?}", req, params);
|
||||
validate_heal_admin_request(&req).await?;
|
||||
let client_address = req
|
||||
.extensions
|
||||
.get::<Option<RemoteAddr>>()
|
||||
.and_then(|opt| opt.map(|addr| addr.0.to_string()))
|
||||
.unwrap_or_default();
|
||||
let mut input = req.input;
|
||||
let bytes = match input.store_all_limited(MAX_HEAL_REQUEST_SIZE).await {
|
||||
Ok(b) => b,
|
||||
@@ -236,8 +366,9 @@ impl Operation for HealHandler {
|
||||
.map_err(|e| s3_error!(InternalError, "root heal failed: {e}"))?;
|
||||
|
||||
map_root_heal_status(heal_err)?;
|
||||
let body = encode_heal_start_success("root-heal".to_string(), client_address)?;
|
||||
|
||||
return Ok(S3Response::new((StatusCode::OK, Body::empty())));
|
||||
return Ok(json_response(StatusCode::OK, body));
|
||||
}
|
||||
validate_heal_request_mode(&hip)?;
|
||||
info!("body: {:?}", hip);
|
||||
@@ -252,11 +383,33 @@ impl Operation for HealHandler {
|
||||
let client_token = hip.client_token.clone();
|
||||
spawn(async move {
|
||||
match rustfs_common::heal_channel::query_heal_status(heal_path_str, client_token).await {
|
||||
Ok(_) => {
|
||||
// TODO: Get actual response from channel
|
||||
Ok(response) if response.success => {
|
||||
let (summary, items) = heal_channel_response_status(&response);
|
||||
let resp_bytes =
|
||||
encode_heal_task_status(summary, response.error.unwrap_or_default(), HealOpts::default(), items);
|
||||
match resp_bytes {
|
||||
Ok(resp_bytes) => {
|
||||
let _ = tx_clone
|
||||
.send(HealResp {
|
||||
resp_bytes,
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = tx_clone
|
||||
.send(HealResp {
|
||||
api_err: Some(e.to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(response) => {
|
||||
let _ = tx_clone
|
||||
.send(HealResp {
|
||||
resp_bytes: vec![],
|
||||
api_err: Some(response.error.unwrap_or_else(|| "query heal status failed".to_string())),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
@@ -275,13 +428,41 @@ impl Operation for HealHandler {
|
||||
// Cancel heal task
|
||||
let tx_clone = tx.clone();
|
||||
let heal_path_str = heal_path.to_str().unwrap_or_default().to_string();
|
||||
let client_token = hip.client_token.clone();
|
||||
let client_address = client_address.clone();
|
||||
let heal_settings = hip.hs;
|
||||
spawn(async move {
|
||||
match rustfs_common::heal_channel::cancel_heal_task(heal_path_str).await {
|
||||
Ok(_) => {
|
||||
// TODO: Get actual response from channel
|
||||
match rustfs_common::heal_channel::cancel_heal_task(heal_path_str, client_token.clone()).await {
|
||||
Ok(response) if response.success => {
|
||||
let resp_bytes = if client_token.is_empty() {
|
||||
encode_heal_start_success(response.request_id, client_address)
|
||||
} else {
|
||||
let (summary, items) = heal_channel_response_status(&response);
|
||||
encode_heal_task_status(summary, response.error.unwrap_or_default(), heal_settings, items)
|
||||
};
|
||||
match resp_bytes {
|
||||
Ok(resp_bytes) => {
|
||||
let _ = tx_clone
|
||||
.send(HealResp {
|
||||
resp_bytes,
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = tx_clone
|
||||
.send(HealResp {
|
||||
api_err: Some(e.to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(response) => {
|
||||
let _ = tx_clone
|
||||
.send(HealResp {
|
||||
resp_bytes: vec![],
|
||||
api_err: Some(response.error.unwrap_or_else(|| "cancel heal task failed".to_string())),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
@@ -299,25 +480,42 @@ impl Operation for HealHandler {
|
||||
} else if hip.client_token.is_empty() {
|
||||
// Use new heal channel mechanism
|
||||
let tx_clone = tx.clone();
|
||||
let client_address = client_address.clone();
|
||||
spawn(async move {
|
||||
// Create heal request through channel
|
||||
let heal_request = rustfs_common::heal_channel::create_heal_request(
|
||||
hip.bucket.clone(),
|
||||
if hip.obj_prefix.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(hip.obj_prefix.clone())
|
||||
},
|
||||
hip.force_start,
|
||||
Some(rustfs_common::heal_channel::HealChannelPriority::Normal),
|
||||
);
|
||||
let heal_request = build_heal_channel_request(&hip);
|
||||
let client_token = heal_request.id.clone();
|
||||
|
||||
match rustfs_common::heal_channel::send_heal_request(heal_request).await {
|
||||
Ok(_) => {
|
||||
// Success - send empty response for now
|
||||
match rustfs_common::heal_channel::send_heal_request_with_admission(heal_request).await {
|
||||
Ok(admission) if admission.is_admitted() => {
|
||||
let resp_bytes = encode_heal_start_success(client_token, client_address);
|
||||
match resp_bytes {
|
||||
Ok(resp_bytes) => {
|
||||
let _ = tx_clone
|
||||
.send(HealResp {
|
||||
resp_bytes,
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = tx_clone
|
||||
.send(HealResp {
|
||||
api_err: Some(e.to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(admission) => {
|
||||
let _ = tx_clone
|
||||
.send(HealResp {
|
||||
resp_bytes: vec![],
|
||||
api_err: Some(format!(
|
||||
"heal request not admitted: admission={}, reason={}",
|
||||
admission.result_label(),
|
||||
admission.reason_label()
|
||||
)),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
@@ -336,7 +534,7 @@ impl Operation for HealHandler {
|
||||
}
|
||||
|
||||
let (status, body) = map_heal_response(rx.recv().await)?;
|
||||
Ok(S3Response::new((status, Body::from(body))))
|
||||
Ok(json_response(status, body))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,8 +561,9 @@ impl Operation for BackgroundHealStatusHandler {
|
||||
mod tests {
|
||||
use super::extract_heal_init_params;
|
||||
use super::{
|
||||
HealInitParams, HealResp, encode_background_heal_status, json_response, map_heal_response, map_root_heal_status,
|
||||
should_handle_root_heal_directly, validate_heal_request_mode, validate_heal_target,
|
||||
HealInitParams, HealResp, build_heal_channel_request, encode_background_heal_status, encode_heal_start_success,
|
||||
encode_heal_task_status, heal_channel_response_items, heal_channel_response_summary, json_response, map_heal_response,
|
||||
map_root_heal_status, should_handle_root_heal_directly, validate_heal_request_mode, validate_heal_target,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use http::StatusCode;
|
||||
@@ -373,8 +572,12 @@ mod tests {
|
||||
use rustfs_common::heal_channel::{HealOpts, HealScanMode};
|
||||
use rustfs_ecstore::error::StorageError;
|
||||
use rustfs_scanner::scanner::BackgroundHealInfo;
|
||||
use s3s::{S3ErrorCode, header::CONTENT_TYPE};
|
||||
use s3s::{
|
||||
S3ErrorCode,
|
||||
header::{CONTENT_LENGTH, CONTENT_TYPE},
|
||||
};
|
||||
use serde_json::json;
|
||||
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::debug;
|
||||
|
||||
@@ -442,6 +645,23 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_heal_init_params_allows_client_token_force_stop() {
|
||||
let uri: Uri = "/rustfs/admin/v3/heal/test-bucket?clientToken=token&forceStop=true"
|
||||
.parse()
|
||||
.expect("uri should parse");
|
||||
|
||||
let mut router = Router::new();
|
||||
router
|
||||
.insert("/rustfs/admin/v3/heal/{bucket}", ())
|
||||
.expect("route should insert");
|
||||
let matched = router.at("/rustfs/admin/v3/heal/test-bucket").expect("route should match");
|
||||
|
||||
let parsed = extract_heal_init_params(&Bytes::new(), &uri, matched.params).expect("client-token stop should be accepted");
|
||||
assert_eq!(parsed.client_token, "token");
|
||||
assert!(parsed.force_stop);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_heal_init_params_allows_root_heal_target() {
|
||||
let uri: Uri = "/rustfs/admin/v3/heal/".parse().expect("uri should parse");
|
||||
@@ -566,6 +786,17 @@ mod tests {
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().code(), &S3ErrorCode::InternalError);
|
||||
|
||||
let (tx, mut rx) = mpsc::channel(1);
|
||||
tx.send(HealResp {
|
||||
resp_bytes: vec![],
|
||||
api_err: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let result = map_heal_response(rx.recv().await);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().code(), &S3ErrorCode::InternalError);
|
||||
|
||||
let (tx, mut rx) = mpsc::channel(1);
|
||||
let _ = tx
|
||||
.send(HealResp {
|
||||
@@ -594,10 +825,119 @@ mod tests {
|
||||
assert!(json["bitrotStartTime"].is_null());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_heal_start_success_uses_client_wire_shape() {
|
||||
let encoded = encode_heal_start_success("token-1".to_string(), "127.0.0.1:9000".to_string())
|
||||
.expect("start response should serialize");
|
||||
let json: serde_json::Value = serde_json::from_slice(&encoded).expect("json should deserialize");
|
||||
|
||||
assert_eq!(json["clientToken"], "token-1");
|
||||
assert_eq!(json["clientAddress"], "127.0.0.1:9000");
|
||||
let start_time = json["startTime"].as_str().expect("startTime should be a string");
|
||||
OffsetDateTime::parse(start_time, &Rfc3339).expect("startTime should be RFC3339");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_heal_task_status_uses_client_wire_shape() {
|
||||
let encoded =
|
||||
encode_heal_task_status("Heal status query accepted".to_string(), String::new(), HealOpts::default(), Vec::new())
|
||||
.expect("status response should serialize");
|
||||
let json: serde_json::Value = serde_json::from_slice(&encoded).expect("json should deserialize");
|
||||
|
||||
assert_eq!(json["summary"], "Heal status query accepted");
|
||||
assert_eq!(json["detail"], "");
|
||||
assert!(json["items"].is_null());
|
||||
assert!(json["settings"].is_object());
|
||||
let start_time = json["startTime"].as_str().expect("startTime should be a string");
|
||||
OffsetDateTime::parse(start_time, &Rfc3339).expect("startTime should be RFC3339");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_heal_channel_request_preserves_client_options() {
|
||||
let hip = HealInitParams {
|
||||
bucket: "bucket-a".to_string(),
|
||||
obj_prefix: "prefix-a".to_string(),
|
||||
force_start: true,
|
||||
hs: HealOpts {
|
||||
recursive: true,
|
||||
dry_run: true,
|
||||
remove: true,
|
||||
recreate: false,
|
||||
scan_mode: HealScanMode::Deep,
|
||||
update_parity: false,
|
||||
no_lock: true,
|
||||
pool: Some(1),
|
||||
set: Some(2),
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let request = build_heal_channel_request(&hip);
|
||||
|
||||
assert_eq!(request.bucket, "bucket-a");
|
||||
assert_eq!(request.object_prefix.as_deref(), Some("prefix-a"));
|
||||
assert!(request.force_start);
|
||||
assert_eq!(request.pool_index, Some(1));
|
||||
assert_eq!(request.set_index, Some(2));
|
||||
assert_eq!(request.scan_mode, Some(HealScanMode::Deep));
|
||||
assert_eq!(request.remove_corrupted, Some(true));
|
||||
assert_eq!(request.recreate_missing, Some(false));
|
||||
assert_eq!(request.update_parity, Some(false));
|
||||
assert_eq!(request.recursive, Some(true));
|
||||
assert_eq!(request.dry_run, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_heal_channel_response_summary_defaults_to_running() {
|
||||
let response = rustfs_common::heal_channel::create_heal_response("token".to_string(), true, None, None);
|
||||
assert_eq!(heal_channel_response_summary(&response), "running");
|
||||
|
||||
let response =
|
||||
rustfs_common::heal_channel::create_heal_response("token".to_string(), true, Some(b"finished".to_vec()), None);
|
||||
assert_eq!(heal_channel_response_summary(&response), "finished");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_heal_channel_response_status_preserves_items() {
|
||||
let payload = serde_json::json!({
|
||||
"summary": "finished",
|
||||
"items": [{
|
||||
"resultId": 0,
|
||||
"type": "object",
|
||||
"bucket": "bucket-a",
|
||||
"object": "object-a",
|
||||
"versionId": "",
|
||||
"detail": "",
|
||||
"parityBlocks": 2,
|
||||
"dataBlocks": 2,
|
||||
"diskCount": 4,
|
||||
"setCount": 1,
|
||||
"before": { "drives": [] },
|
||||
"after": { "drives": [] },
|
||||
"objectSize": 1024
|
||||
}]
|
||||
});
|
||||
let response = rustfs_common::heal_channel::create_heal_response(
|
||||
"token".to_string(),
|
||||
true,
|
||||
Some(serde_json::to_vec(&payload).expect("payload should serialize")),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(heal_channel_response_summary(&response), "finished");
|
||||
let items = heal_channel_response_items(&response);
|
||||
assert_eq!(items.len(), 1);
|
||||
assert_eq!(items[0].bucket, "bucket-a");
|
||||
assert_eq!(items[0].object, "object-a");
|
||||
assert_eq!(items[0].object_size, 1024);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_response_sets_application_json_content_type() {
|
||||
let response = json_response(StatusCode::OK, b"{}".to_vec());
|
||||
let content_type = response.headers.get(CONTENT_TYPE).and_then(|value| value.to_str().ok());
|
||||
assert_eq!(content_type, Some("application/json"),);
|
||||
let content_length = response.headers.get(CONTENT_LENGTH).and_then(|value| value.to_str().ok());
|
||||
assert_eq!(content_length, Some("2"),);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -572,10 +572,6 @@ async fn run(config: rustfs::config::Config) -> Result<()> {
|
||||
init_heal_manager(heal_storage, None).await?;
|
||||
}
|
||||
|
||||
if enable_scanner {
|
||||
init_data_scanner(ctx.clone(), store.clone()).await;
|
||||
}
|
||||
|
||||
if !enable_heal && !enable_scanner {
|
||||
info!(target: "rustfs::main::run","Both scanner and heal are disabled, skipping AHM service initialization");
|
||||
}
|
||||
@@ -610,6 +606,10 @@ async fn run(config: rustfs::config::Config) -> Result<()> {
|
||||
// Publish ready only after all critical bootstrap metadata is in place
|
||||
state_manager.update(ServiceState::Ready);
|
||||
|
||||
if enable_scanner {
|
||||
init_data_scanner(ctx.clone(), store.clone()).await;
|
||||
}
|
||||
|
||||
// Perform hibernation for 1 second
|
||||
tokio::time::sleep(SHUTDOWN_TIMEOUT).await;
|
||||
// listen to the shutdown signal
|
||||
|
||||
+67
-13
@@ -275,7 +275,7 @@ fn should_force_zero_content_length_for_empty_body_route<B>(req: &HttpRequest<B>
|
||||
return false;
|
||||
}
|
||||
|
||||
if is_empty_body_admin_path(req.method(), req.uri().path()) {
|
||||
if is_empty_body_admin_path(req.method(), req.uri()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -286,7 +286,8 @@ fn should_force_zero_content_length_for_empty_body_route<B>(req: &HttpRequest<B>
|
||||
is_empty_body_s3_path(req.method(), req.uri())
|
||||
}
|
||||
|
||||
fn is_empty_body_admin_path(method: &Method, path: &str) -> bool {
|
||||
fn is_empty_body_admin_path(method: &Method, uri: &http::Uri) -> bool {
|
||||
let path = uri.path();
|
||||
match *method {
|
||||
Method::PUT => matches!(
|
||||
path,
|
||||
@@ -295,21 +296,40 @@ fn is_empty_body_admin_path(method: &Method, path: &str) -> bool {
|
||||
| "/rustfs/admin/v3/set-user-status"
|
||||
| "/rustfs/admin/v3/set-group-status"
|
||||
),
|
||||
Method::POST => matches!(
|
||||
path,
|
||||
"/minio/admin/v3/rebalance/start"
|
||||
| "/minio/admin/v3/rebalance/stop"
|
||||
| "/minio/admin/v3/pools/decommission"
|
||||
| "/minio/admin/v3/pools/cancel"
|
||||
| "/rustfs/admin/v3/rebalance/start"
|
||||
| "/rustfs/admin/v3/rebalance/stop"
|
||||
| "/rustfs/admin/v3/pools/decommission"
|
||||
| "/rustfs/admin/v3/pools/cancel"
|
||||
),
|
||||
Method::POST => {
|
||||
matches!(
|
||||
path,
|
||||
"/minio/admin/v3/rebalance/start"
|
||||
| "/minio/admin/v3/rebalance/stop"
|
||||
| "/minio/admin/v3/pools/decommission"
|
||||
| "/minio/admin/v3/pools/cancel"
|
||||
| "/rustfs/admin/v3/rebalance/start"
|
||||
| "/rustfs/admin/v3/rebalance/stop"
|
||||
| "/rustfs/admin/v3/pools/decommission"
|
||||
| "/rustfs/admin/v3/pools/cancel"
|
||||
) || is_heal_status_query(path, uri.query())
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_heal_status_query(path: &str, query: Option<&str>) -> bool {
|
||||
let Some(query) = query else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if !path.find("/v3/heal/").is_some_and(|index| path[..index].ends_with("/admin")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
query.split('&').any(|param| {
|
||||
param
|
||||
.split_once('=')
|
||||
.map(|(key, value)| key == "clientToken" && !value.is_empty())
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
fn is_empty_body_s3_path(method: &Method, uri: &http::Uri) -> bool {
|
||||
*method == Method::DELETE && ConditionalCorsLayer::is_s3_path(uri.path())
|
||||
}
|
||||
@@ -1320,6 +1340,40 @@ mod tests {
|
||||
assert!(headers.get(http::header::TRANSFER_ENCODING).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_heal_status_query_without_content_length_is_normalized() {
|
||||
let paths = [
|
||||
format!("{ADMIN_PREFIX}/v3/heal/?clientToken=root-heal"),
|
||||
format!("{ADMIN_PREFIX}/v3/heal/bucket?clientToken=bucket-heal"),
|
||||
];
|
||||
|
||||
for path in paths {
|
||||
let request = Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri(path.clone())
|
||||
.header(http::header::TRANSFER_ENCODING, "chunked")
|
||||
.body(())
|
||||
.expect("request");
|
||||
|
||||
assert!(
|
||||
should_force_zero_content_length_for_empty_body_route(&request),
|
||||
"{path} should force Content-Length: 0"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_heal_start_without_status_token_is_not_normalized() {
|
||||
let request = Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri(format!("{ADMIN_PREFIX}/v3/heal/"))
|
||||
.header(http::header::TRANSFER_ENCODING, "chunked")
|
||||
.body(())
|
||||
.expect("request");
|
||||
|
||||
assert!(!should_force_zero_content_length_for_empty_body_route(&request));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn s3_delete_object_version_without_content_length_is_normalized() {
|
||||
let request = Request::builder()
|
||||
|
||||
Reference in New Issue
Block a user