mirror of
https://github.com/rustfs/rustfs.git
synced 2026-03-17 14:24:08 +00:00
refactor: stabilize heal format recovery integration tests (#1984)
Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
@@ -2,12 +2,13 @@
|
||||
|
||||
.PHONY: test
|
||||
test: core-deps test-deps ## Run all tests
|
||||
TEST_THREADS ?= 1
|
||||
@echo "🧪 Running tests..."
|
||||
@if command -v cargo-nextest >/dev/null 2>&1; then \
|
||||
cargo nextest run --all --exclude e2e_test; \
|
||||
else \
|
||||
echo "ℹ️ cargo-nextest not found; falling back to 'cargo test'"; \
|
||||
cargo test --workspace --exclude e2e_test -- --nocapture; \
|
||||
cargo test --workspace --exclude e2e_test -- --nocapture --test-threads="$(TEST_THREADS)"; \
|
||||
fi
|
||||
cargo test --all --doc
|
||||
|
||||
@@ -17,4 +18,4 @@ e2e-server: ## Run e2e-server tests
|
||||
|
||||
.PHONY: probe-e2e
|
||||
probe-e2e: ## Probe e2e tests
|
||||
sh $(shell pwd)/scripts/probe.sh
|
||||
sh $(shell pwd)/scripts/probe.sh
|
||||
|
||||
@@ -18,7 +18,7 @@ use std::{
|
||||
fmt::{self, Display},
|
||||
sync::OnceLock,
|
||||
};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub const HEAL_DELETE_DANGLING: bool = true;
|
||||
@@ -290,6 +290,11 @@ pub type HealChannelReceiver = mpsc::UnboundedReceiver<HealChannelCommand>;
|
||||
/// Global heal channel sender
|
||||
static GLOBAL_HEAL_CHANNEL_SENDER: OnceLock<HealChannelSender> = OnceLock::new();
|
||||
|
||||
type HealResponseSender = broadcast::Sender<HealChannelResponse>;
|
||||
|
||||
/// Global heal response broadcaster
|
||||
static GLOBAL_HEAL_RESPONSE_SENDER: OnceLock<HealResponseSender> = OnceLock::new();
|
||||
|
||||
/// Initialize global heal channel
|
||||
pub fn init_heal_channel() -> HealChannelReceiver {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
@@ -316,6 +321,23 @@ pub async fn send_heal_command(command: HealChannelCommand) -> Result<(), String
|
||||
}
|
||||
}
|
||||
|
||||
fn heal_response_sender() -> &'static HealResponseSender {
|
||||
GLOBAL_HEAL_RESPONSE_SENDER.get_or_init(|| {
|
||||
let (tx, _rx) = broadcast::channel(1024);
|
||||
tx
|
||||
})
|
||||
}
|
||||
|
||||
/// Publish a heal response to subscribers.
|
||||
pub fn publish_heal_response(response: HealChannelResponse) -> Result<(), broadcast::error::SendError<HealChannelResponse>> {
|
||||
heal_response_sender().send(response).map(|_| ())
|
||||
}
|
||||
|
||||
/// Subscribe to heal responses.
|
||||
pub fn subscribe_heal_responses() -> broadcast::Receiver<HealChannelResponse> {
|
||||
heal_response_sender().subscribe()
|
||||
}
|
||||
|
||||
/// Send heal start request
|
||||
pub async fn send_heal_request(request: HealChannelRequest) -> Result<(), String> {
|
||||
send_heal_command(HealChannelCommand::Start(request)).await
|
||||
@@ -514,3 +536,20 @@ pub async fn send_heal_disk(set_disk_id: String, priority: Option<HealChannelPri
|
||||
};
|
||||
send_heal_request(req).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn heal_response_broadcast_reaches_subscriber() {
|
||||
let mut receiver = subscribe_heal_responses();
|
||||
let response = create_heal_response("req-1".to_string(), true, None, None);
|
||||
|
||||
publish_heal_response(response.clone()).expect("publish should succeed");
|
||||
|
||||
let received = receiver.recv().await.expect("should receive heal response");
|
||||
assert_eq!(received.request_id, response.request_id);
|
||||
assert!(received.success);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ use time::OffsetDateTime;
|
||||
use tokio::select;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tokio::sync::{RwLock, mpsc};
|
||||
use tracing::{debug, error, info};
|
||||
use tracing::{debug, error, info, warn};
|
||||
use uuid::Uuid;
|
||||
use xxhash_rust::xxh64;
|
||||
|
||||
@@ -102,7 +102,7 @@ impl LifecycleSys {
|
||||
}
|
||||
|
||||
pub fn trace(_oi: &ObjectInfo) -> TraceFn {
|
||||
todo!();
|
||||
Arc::new(|_oi, _ctx| Box::pin(async move {}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,7 +395,7 @@ impl ExpiryState {
|
||||
}
|
||||
else {
|
||||
//info!("Invalid work type - {:?}", v);
|
||||
todo!();
|
||||
warn!("lifecycle worker received unsupported operation type");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -788,7 +788,7 @@ pub async fn transition_object(api: Arc<ECStore>, oi: &ObjectInfo, lae: LcAuditE
|
||||
}
|
||||
|
||||
pub fn audit_tier_actions(_api: ECStore, _tier: &str, _bytes: i64) -> TimeFn {
|
||||
todo!();
|
||||
Arc::new(|| Box::pin(async move {}))
|
||||
}
|
||||
|
||||
pub async fn get_transitioned_object_reader(
|
||||
|
||||
@@ -41,7 +41,11 @@ use tokio_util::io::ReaderStream;
|
||||
|
||||
impl TransitionClient {
|
||||
pub fn get_object(&self, bucket_name: &str, object_name: &str, opts: &GetObjectOptions) -> Result<Object, std::io::Error> {
|
||||
todo!();
|
||||
let _ = opts;
|
||||
Err(std::io::Error::new(
|
||||
IoErrorKind::Unsupported,
|
||||
format!("get_object is not implemented for {bucket_name}/{object_name}"),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn get_object_inner(
|
||||
@@ -131,7 +135,18 @@ impl Object {
|
||||
}
|
||||
|
||||
fn do_get_request(&self, request: &GetRequest) -> Result<GetResponse, std::io::Error> {
|
||||
todo!()
|
||||
let _ = request.did_offset_change;
|
||||
let _ = request.offset;
|
||||
let _ = request.is_first_req;
|
||||
let _ = request.is_read_at;
|
||||
let _ = request.setting_object_info;
|
||||
let _ = request.is_read_op;
|
||||
let _ = request.been_read;
|
||||
let _ = request.buffer.len();
|
||||
Err(std::io::Error::new(
|
||||
IoErrorKind::Unsupported,
|
||||
"read-path for Object in api_get_object is not implemented",
|
||||
))
|
||||
}
|
||||
|
||||
fn set_offset(&mut self, bytes_read: i64) -> Result<(), std::io::Error> {
|
||||
|
||||
@@ -34,10 +34,21 @@ use hyper::body::Bytes;
|
||||
use rustfs_config::MAX_S3_CLIENT_RESPONSE_SIZE;
|
||||
use rustfs_utils::hash::EMPTY_STRING_SHA256_HASH;
|
||||
use std::collections::HashMap;
|
||||
use std::io::ErrorKind;
|
||||
|
||||
impl TransitionClient {
|
||||
pub fn list_buckets(&self) -> Result<Vec<BucketInfo>, std::io::Error> {
|
||||
todo!();
|
||||
Err(std::io::Error::new(
|
||||
ErrorKind::Unsupported,
|
||||
credentials::ErrorResponse {
|
||||
sts_error: credentials::STSError {
|
||||
r#type: "".to_string(),
|
||||
code: "NotImplemented".to_string(),
|
||||
message: "The list_buckets API is not implemented in this build.".to_string(),
|
||||
},
|
||||
request_id: "".to_string(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn list_objects_v2_query(
|
||||
@@ -237,7 +248,17 @@ impl TransitionClient {
|
||||
}
|
||||
|
||||
Ok(listObjectVersionsOutput)*/
|
||||
todo!();
|
||||
Err(std::io::Error::new(
|
||||
ErrorKind::Unsupported,
|
||||
credentials::ErrorResponse {
|
||||
sts_error: credentials::STSError {
|
||||
r#type: "".to_string(),
|
||||
code: "NotImplemented".to_string(),
|
||||
message: format!("list_object_versions_query is not implemented for bucket {bucket_name}"),
|
||||
},
|
||||
request_id: "".to_string(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn list_objects_query(
|
||||
@@ -249,7 +270,17 @@ impl TransitionClient {
|
||||
max_keys: i64,
|
||||
headers: HeaderMap,
|
||||
) -> Result<ListBucketResult, std::io::Error> {
|
||||
todo!();
|
||||
Err(std::io::Error::new(
|
||||
ErrorKind::Unsupported,
|
||||
credentials::ErrorResponse {
|
||||
sts_error: credentials::STSError {
|
||||
r#type: "".to_string(),
|
||||
code: "NotImplemented".to_string(),
|
||||
message: format!("list_objects_query is not implemented for bucket {bucket_name}"),
|
||||
},
|
||||
request_id: "".to_string(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn list_multipart_uploads_query(
|
||||
@@ -261,7 +292,17 @@ impl TransitionClient {
|
||||
delimiter: &str,
|
||||
max_uploads: i64,
|
||||
) -> Result<ListMultipartUploadsResult, std::io::Error> {
|
||||
todo!();
|
||||
Err(std::io::Error::new(
|
||||
ErrorKind::Unsupported,
|
||||
credentials::ErrorResponse {
|
||||
sts_error: credentials::STSError {
|
||||
r#type: "".to_string(),
|
||||
code: "NotImplemented".to_string(),
|
||||
message: format!("list_multipart_uploads_query is not implemented for bucket {bucket_name}"),
|
||||
},
|
||||
request_id: "".to_string(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn list_object_parts(
|
||||
@@ -270,11 +311,33 @@ impl TransitionClient {
|
||||
object_name: &str,
|
||||
upload_id: &str,
|
||||
) -> Result<HashMap<i64, ObjectPart>, std::io::Error> {
|
||||
todo!();
|
||||
Err(std::io::Error::new(
|
||||
ErrorKind::Unsupported,
|
||||
credentials::ErrorResponse {
|
||||
sts_error: credentials::STSError {
|
||||
r#type: "".to_string(),
|
||||
code: "NotImplemented".to_string(),
|
||||
message: format!(
|
||||
"list_object_parts is not implemented for bucket {bucket_name}, object {object_name}, upload_id {upload_id}"
|
||||
),
|
||||
},
|
||||
request_id: "".to_string(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn find_upload_ids(&self, bucket_name: &str, object_name: &str) -> Result<Vec<String>, std::io::Error> {
|
||||
todo!();
|
||||
Err(std::io::Error::new(
|
||||
ErrorKind::Unsupported,
|
||||
credentials::ErrorResponse {
|
||||
sts_error: credentials::STSError {
|
||||
r#type: "".to_string(),
|
||||
code: "NotImplemented".to_string(),
|
||||
message: format!("find_upload_ids is not implemented for bucket {bucket_name}, object {object_name}"),
|
||||
},
|
||||
request_id: "".to_string(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn list_object_parts_query(
|
||||
@@ -285,7 +348,19 @@ impl TransitionClient {
|
||||
part_number_marker: i64,
|
||||
max_parts: i64,
|
||||
) -> Result<ListObjectPartsResult, std::io::Error> {
|
||||
todo!();
|
||||
Err(std::io::Error::new(
|
||||
ErrorKind::Unsupported,
|
||||
credentials::ErrorResponse {
|
||||
sts_error: credentials::STSError {
|
||||
r#type: "".to_string(),
|
||||
code: "NotImplemented".to_string(),
|
||||
message: format!(
|
||||
"list_object_parts_query is not implemented for bucket {bucket_name}, object {object_name}, upload_id {upload_id}"
|
||||
),
|
||||
},
|
||||
request_id: "".to_string(),
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,7 +379,44 @@ pub struct ListObjectsOptions {
|
||||
|
||||
impl ListObjectsOptions {
|
||||
pub fn set(&mut self, key: &str, value: &str) {
|
||||
todo!();
|
||||
match key {
|
||||
"prefix" => {
|
||||
self.prefix = value.to_string();
|
||||
}
|
||||
"start-after" => {
|
||||
self.start_after = value.to_string();
|
||||
}
|
||||
"max-keys" => {
|
||||
if let Ok(v) = value.parse::<i64>() {
|
||||
self.max_keys = v;
|
||||
}
|
||||
}
|
||||
"delimiter" => {
|
||||
// delimiter is currently kept in request only; this option structure does not persist it yet.
|
||||
}
|
||||
"reverse" | "versions" | "metadata" | "recursive" | "use-v1" => {
|
||||
if let Some(v) = value.strip_prefix("v").or_else(|| value.strip_prefix("V")) {
|
||||
let v = v.eq_ignore_ascii_case("true");
|
||||
match key {
|
||||
"reverse" => self.reverse_versions = v,
|
||||
"versions" => self.with_versions = v,
|
||||
"metadata" => self.with_metadata = v,
|
||||
"recursive" => self.recursive = v,
|
||||
_ => self.use_v1 = v,
|
||||
}
|
||||
} else {
|
||||
let v = value.eq_ignore_ascii_case("true");
|
||||
match key {
|
||||
"reverse" => self.reverse_versions = v,
|
||||
"versions" => self.with_versions = v,
|
||||
"metadata" => self.with_metadata = v,
|
||||
"recursive" => self.recursive = v,
|
||||
_ => self.use_v1 = v,
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,11 +27,11 @@ use crate::client::{
|
||||
};
|
||||
|
||||
pub fn is_object(reader: &ReaderImpl) -> bool {
|
||||
todo!();
|
||||
matches!(reader, ReaderImpl::ObjectBody(_))
|
||||
}
|
||||
|
||||
pub fn is_read_at(reader: ReaderImpl) -> bool {
|
||||
todo!();
|
||||
matches!(reader, ReaderImpl::ObjectBody(_))
|
||||
}
|
||||
|
||||
pub fn optimal_part_info(object_size: i64, configured_part_size: u64) -> Result<(i64, i64, i64), std::io::Error> {
|
||||
|
||||
@@ -97,11 +97,11 @@ impl TransitionClient {
|
||||
if opts.checksum.is_set() {
|
||||
opts.auto_checksum = opts.checksum.clone();
|
||||
}
|
||||
let with_checksum = self.trailing_header_support;
|
||||
let upload_id = self.new_upload_id(bucket_name, object_name, &opts).await?;
|
||||
|
||||
opts.user_metadata.remove("X-Amz-Checksum-Algorithm");
|
||||
|
||||
todo!();
|
||||
self.put_object_multipart_stream_optional_checksum(bucket_name, object_name, reader, size, &opts)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn put_object_multipart_stream_optional_checksum(
|
||||
|
||||
@@ -26,14 +26,19 @@ use rustfs_utils::HashAlgorithm;
|
||||
use s3s::S3ErrorCode;
|
||||
use s3s::dto::ReplicationStatus;
|
||||
use s3s::header::X_AMZ_BYPASS_GOVERNANCE_RETENTION;
|
||||
use serde::Deserialize;
|
||||
use std::fmt::Display;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
};
|
||||
use time::OffsetDateTime;
|
||||
use tokio::sync::mpsc::{self, Receiver, Sender};
|
||||
|
||||
use crate::client::utils::base64_encode;
|
||||
use crate::client::{
|
||||
api_error_response::{ErrorResponse, http_resp_to_error_response, to_error_response},
|
||||
api_s3_datatypes::{DeleteMultiObjects, DeleteObject},
|
||||
transition_api::{ReaderImpl, RequestMetadata, TransitionClient},
|
||||
};
|
||||
use crate::{
|
||||
@@ -46,6 +51,8 @@ pub struct RemoveBucketOptions {
|
||||
_forced_delete: bool,
|
||||
}
|
||||
|
||||
const DELETE_RESPONSE_PREVIEW_LEN: usize = 1024;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct AdvancedRemoveOptions {
|
||||
@@ -354,7 +361,7 @@ impl TransitionClient {
|
||||
body_vec.extend_from_slice(data);
|
||||
}
|
||||
}
|
||||
process_remove_multi_objects_response(ReaderImpl::Body(Bytes::from(body_vec)), result_tx.clone());
|
||||
process_remove_multi_objects_response(ReaderImpl::Body(Bytes::from(body_vec)), &batch, result_tx.clone()).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -498,11 +505,239 @@ pub struct RemoveObjectsOptions {
|
||||
}
|
||||
|
||||
pub fn generate_remove_multi_objects_request(objects: &[ObjectInfo]) -> Vec<u8> {
|
||||
todo!();
|
||||
let escape_xml = |value: &str| -> String {
|
||||
value
|
||||
.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('\"', """)
|
||||
.replace('\'', "'")
|
||||
};
|
||||
|
||||
let request: DeleteMultiObjects = DeleteMultiObjects {
|
||||
quiet: false,
|
||||
objects: objects
|
||||
.iter()
|
||||
.map(|object| DeleteObject {
|
||||
key: object.name.clone(),
|
||||
version_id: object.version_id.map(|v| v.to_string()).unwrap_or_default(),
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
match request.marshal_msg() {
|
||||
Ok(body) => body.into_bytes(),
|
||||
Err(_) => {
|
||||
let mut body = String::new();
|
||||
body.push_str("<Delete><Quiet>false</Quiet>");
|
||||
for object in objects {
|
||||
body.push_str("<Object>");
|
||||
body.push_str("<Key>");
|
||||
body.push_str(&escape_xml(&object.name));
|
||||
body.push_str("</Key>");
|
||||
if object.version_id.is_some() {
|
||||
body.push_str("<VersionId>");
|
||||
body.push_str(&escape_xml(&object.version_id.as_ref().map(|v| v.to_string()).unwrap_or_default()));
|
||||
body.push_str("</VersionId>");
|
||||
}
|
||||
body.push_str("</Object>");
|
||||
}
|
||||
body.push_str("</Delete>");
|
||||
body.into_bytes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_remove_multi_objects_response(body: ReaderImpl, result_tx: Sender<RemoveObjectResult>) {
|
||||
todo!();
|
||||
pub async fn process_remove_multi_objects_response(
|
||||
body: ReaderImpl,
|
||||
objects: &[ObjectInfo],
|
||||
result_tx: Sender<RemoveObjectResult>,
|
||||
) {
|
||||
let mut body_vec = Vec::new();
|
||||
match body {
|
||||
ReaderImpl::Body(content_body) => {
|
||||
body_vec = content_body.to_vec();
|
||||
}
|
||||
ReaderImpl::ObjectBody(mut object_body) => match object_body.read_all().await {
|
||||
Ok(content) => {
|
||||
body_vec = content;
|
||||
}
|
||||
Err(err) => {
|
||||
for object in objects {
|
||||
let version_id = object.version_id.as_ref().map(|v| v.to_string()).unwrap_or_default();
|
||||
let _ = result_tx
|
||||
.send(RemoveObjectResult {
|
||||
object_name: object.name.clone(),
|
||||
object_version_id: version_id,
|
||||
err: Some(std::io::Error::other(ErrorResponse {
|
||||
code: S3ErrorCode::Custom("ReadDeleteResponseFailed".into()),
|
||||
message: format!("read multi remove response failed: {err}"),
|
||||
bucket_name: object.bucket.clone(),
|
||||
key: object.name.clone(),
|
||||
resource: "".to_string(),
|
||||
request_id: "".to_string(),
|
||||
host_id: "".to_string(),
|
||||
region: "".to_string(),
|
||||
server: "".to_string(),
|
||||
status_code: StatusCode::OK,
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
}
|
||||
return;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename = "DeleteResult")]
|
||||
struct Deleted {
|
||||
#[serde(rename = "Deleted", default)]
|
||||
deleted: Vec<DeleteResultDeleted>,
|
||||
#[serde(rename = "Error", default)]
|
||||
error: Vec<DeleteResultError>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct DeleteResultDeleted {
|
||||
#[serde(rename = "Key")]
|
||||
key: String,
|
||||
#[serde(rename = "VersionId", default)]
|
||||
version_id: String,
|
||||
#[serde(rename = "DeleteMarker")]
|
||||
deletemarker: bool,
|
||||
#[serde(rename = "DeleteMarkerVersionId", default)]
|
||||
deletemarker_version_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct DeleteResultError {
|
||||
#[serde(rename = "Key")]
|
||||
key: String,
|
||||
#[serde(rename = "VersionId", default)]
|
||||
version_id: String,
|
||||
#[serde(rename = "Code")]
|
||||
code: String,
|
||||
#[serde(rename = "Message")]
|
||||
message: String,
|
||||
}
|
||||
|
||||
let mut pending = HashSet::with_capacity(objects.len());
|
||||
for object in objects {
|
||||
pending.insert((object.name.clone(), object.version_id.as_ref().map(|v| v.to_string()).unwrap_or_default()));
|
||||
}
|
||||
|
||||
let body = String::from_utf8_lossy(&body_vec).into_owned();
|
||||
let parsed: Deleted = match quick_xml::de::from_str(&body) {
|
||||
Ok(parsed) => parsed,
|
||||
Err(err) => {
|
||||
for object in objects {
|
||||
let version_id = object.version_id.as_ref().map(|v| v.to_string()).unwrap_or_default();
|
||||
let _ = result_tx
|
||||
.send(RemoveObjectResult {
|
||||
object_name: object.name.clone(),
|
||||
object_version_id: version_id,
|
||||
err: Some(std::io::Error::other(ErrorResponse {
|
||||
code: S3ErrorCode::Custom("UnmarshalDeleteResponseFailed".into()),
|
||||
message: format!(
|
||||
"unmarshal multi remove response failed: {err}; response_body={}",
|
||||
body.chars().take(DELETE_RESPONSE_PREVIEW_LEN).collect::<String>()
|
||||
),
|
||||
bucket_name: object.bucket.clone(),
|
||||
key: object.name.clone(),
|
||||
resource: "".to_string(),
|
||||
request_id: "".to_string(),
|
||||
host_id: "".to_string(),
|
||||
region: "".to_string(),
|
||||
server: "".to_string(),
|
||||
status_code: StatusCode::OK,
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
for deleted in parsed.deleted {
|
||||
if !pending.remove(&(deleted.key.clone(), deleted.version_id.clone())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let _ = result_tx
|
||||
.send(RemoveObjectResult {
|
||||
object_name: deleted.key,
|
||||
object_version_id: deleted.version_id,
|
||||
delete_marker: deleted.deletemarker,
|
||||
delete_marker_version_id: deleted.deletemarker_version_id,
|
||||
err: None,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
for removed in parsed.error {
|
||||
if !pending.remove(&(removed.key.clone(), removed.version_id.clone())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let _ = result_tx
|
||||
.send(RemoveObjectResult {
|
||||
object_name: removed.key.clone(),
|
||||
object_version_id: removed.version_id,
|
||||
err: Some(std::io::Error::other(ErrorResponse {
|
||||
code: S3ErrorCode::Custom(removed.code.into()),
|
||||
message: removed.message,
|
||||
bucket_name: "".to_string(),
|
||||
key: removed.key,
|
||||
resource: "".to_string(),
|
||||
request_id: "".to_string(),
|
||||
host_id: "".to_string(),
|
||||
region: "".to_string(),
|
||||
server: "".to_string(),
|
||||
status_code: StatusCode::OK,
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
for (object_name, object_version_id) in pending {
|
||||
let bucket_name = objects
|
||||
.iter()
|
||||
.find(|object| {
|
||||
object.name == object_name && object.version_id.as_ref().map(|v| v.to_string()) == Some(object_version_id.clone())
|
||||
})
|
||||
.map(|o| o.bucket.clone())
|
||||
.unwrap_or_default();
|
||||
let object_name = object_name;
|
||||
let object_version_id = object_version_id;
|
||||
let error_message = format!(
|
||||
"remove response did not contain an entry for object {} with version {}",
|
||||
object_name, object_version_id
|
||||
);
|
||||
|
||||
let _ = result_tx
|
||||
.send(RemoveObjectResult {
|
||||
object_name: object_name.clone(),
|
||||
object_version_id: object_version_id.clone(),
|
||||
err: Some(std::io::Error::other(ErrorResponse {
|
||||
code: S3ErrorCode::Custom("UnmatchedDeleteResponseEntry".into()),
|
||||
message: error_message,
|
||||
bucket_name,
|
||||
key: object_name,
|
||||
resource: "".to_string(),
|
||||
request_id: "".to_string(),
|
||||
host_id: "".to_string(),
|
||||
region: "".to_string(),
|
||||
server: "".to_string(),
|
||||
status_code: StatusCode::OK,
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
fn has_invalid_xml_char(str: &str) -> bool {
|
||||
|
||||
@@ -290,7 +290,50 @@ impl CompleteMultipartUpload {
|
||||
}
|
||||
|
||||
pub fn unmarshal(buf: &[u8]) -> Result<Self, std::io::Error> {
|
||||
todo!();
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct WirePart {
|
||||
#[serde(rename = "ETag")]
|
||||
etag: String,
|
||||
#[serde(rename = "PartNumber")]
|
||||
part_num: i64,
|
||||
#[serde(rename = "ChecksumCRC32")]
|
||||
checksum_crc32: String,
|
||||
#[serde(rename = "ChecksumCRC32C")]
|
||||
checksum_crc32c: String,
|
||||
#[serde(rename = "ChecksumSHA1")]
|
||||
checksum_sha1: String,
|
||||
#[serde(rename = "ChecksumSHA256")]
|
||||
checksum_sha256: String,
|
||||
#[serde(rename = "ChecksumCRC64NVME")]
|
||||
checksum_crc64nvme: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename = "CompleteMultipartUpload")]
|
||||
struct WireCompleteMultipartUpload {
|
||||
#[serde(rename = "Part", default)]
|
||||
parts: Vec<WirePart>,
|
||||
}
|
||||
|
||||
let body = String::from_utf8_lossy(buf);
|
||||
let wire: WireCompleteMultipartUpload = quick_xml::de::from_str(&body)
|
||||
.map_err(|err| std::io::Error::other(format!("failed to parse CompleteMultipartUpload XML: {err}; body: {body}")))?;
|
||||
|
||||
Ok(Self {
|
||||
parts: wire
|
||||
.parts
|
||||
.into_iter()
|
||||
.map(|p| CompletePart {
|
||||
etag: p.etag,
|
||||
part_num: p.part_num,
|
||||
checksum_crc32: p.checksum_crc32,
|
||||
checksum_crc32c: p.checksum_crc32c,
|
||||
checksum_sha1: p.checksum_sha1,
|
||||
checksum_sha256: p.checksum_sha256,
|
||||
checksum_crc64nvme: p.checksum_crc64nvme,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,7 +383,37 @@ impl DeleteMultiObjects {
|
||||
}
|
||||
|
||||
pub fn unmarshal(buf: &[u8]) -> Result<Self, std::io::Error> {
|
||||
todo!();
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct WireDeleteObject {
|
||||
#[serde(rename = "Key")]
|
||||
key: String,
|
||||
#[serde(rename = "VersionId")]
|
||||
version_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename = "Delete")]
|
||||
struct WireDeleteMultiObjects {
|
||||
#[serde(rename = "Quiet", default)]
|
||||
quiet: bool,
|
||||
#[serde(rename = "Object", default)]
|
||||
objects: Vec<WireDeleteObject>,
|
||||
}
|
||||
|
||||
let body = String::from_utf8_lossy(buf);
|
||||
let wire: WireDeleteMultiObjects = quick_xml::de::from_str(&body).map_err(|err| std::io::Error::other(err))?;
|
||||
|
||||
Ok(Self {
|
||||
quiet: wire.quiet,
|
||||
objects: wire
|
||||
.objects
|
||||
.into_iter()
|
||||
.map(|o| DeleteObject {
|
||||
key: o.key,
|
||||
version_id: o.version_id,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -248,8 +248,12 @@ impl ChecksumMode {
|
||||
}
|
||||
});
|
||||
let c = self.base();
|
||||
let crc_bytes = Vec::<u8>::with_capacity(p.len() * self.raw_byte_len() as usize);
|
||||
let mut crc_bytes = Vec::<u8>::with_capacity(p.len() * self.raw_byte_len() as usize);
|
||||
let mut h = self.hasher()?;
|
||||
for part in p.iter() {
|
||||
let part_checksum = part.checksum_raw(&c)?;
|
||||
crc_bytes.extend(part_checksum);
|
||||
}
|
||||
h.update(crc_bytes.as_ref());
|
||||
let hash = h.finalize();
|
||||
Ok(Checksum {
|
||||
@@ -260,7 +264,11 @@ impl ChecksumMode {
|
||||
}
|
||||
|
||||
pub fn full_object_checksum(&self, p: &mut [ObjectPart]) -> Result<Checksum, std::io::Error> {
|
||||
todo!();
|
||||
if !self.can_merge_crc() {
|
||||
return Err(std::io::Error::other("cannot do full-object checksum"));
|
||||
}
|
||||
|
||||
self.composite_checksum(p)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,10 @@
|
||||
#![allow(unused_must_use)]
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use quick_xml;
|
||||
use serde::de::Deserialize;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io::{Error, ErrorKind};
|
||||
|
||||
use time::OffsetDateTime;
|
||||
|
||||
@@ -154,10 +157,21 @@ impl ErrorResponse {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn xml_decoder<T>(body: &[u8]) -> Result<T, std::io::Error> {
|
||||
todo!();
|
||||
pub fn xml_decoder<T>(body: &[u8]) -> Result<T, Error>
|
||||
where
|
||||
for<'de> T: Deserialize<'de>,
|
||||
{
|
||||
match std::str::from_utf8(body) {
|
||||
Ok(xml_body) => quick_xml::de::from_str::<T>(xml_body).map_err(|err| Error::new(ErrorKind::InvalidData, err.to_string())),
|
||||
Err(err) => Err(Error::new(ErrorKind::InvalidData, err.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn xml_decode_and_body<T>(body_reader: &[u8]) -> Result<(Vec<u8>, T), std::io::Error> {
|
||||
todo!();
|
||||
pub fn xml_decode_and_body<T>(body_reader: &[u8]) -> Result<(Vec<u8>, T), std::io::Error>
|
||||
where
|
||||
for<'de> T: Deserialize<'de>,
|
||||
{
|
||||
let body = body_reader.to_vec();
|
||||
let parsed = xml_decoder(&body)?;
|
||||
Ok((body, parsed))
|
||||
}
|
||||
|
||||
@@ -33,18 +33,17 @@ use s3s::S3ErrorCode;
|
||||
//#[derive(Clone)]
|
||||
pub struct PutObjReader {
|
||||
pub reader: HashReader,
|
||||
pub raw_reader: HashReader,
|
||||
//pub sealMD5Fn: SealMD5CurrFn,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl PutObjReader {
|
||||
pub fn new(raw_reader: HashReader) -> Self {
|
||||
todo!();
|
||||
pub fn new(reader: HashReader) -> Self {
|
||||
Self { reader }
|
||||
}
|
||||
|
||||
fn md5_current_hex_string(&self) -> String {
|
||||
todo!();
|
||||
self.reader.checksum().map(|v| v.encoded).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn with_encryption(&mut self, enc_reader: HashReader) -> Result<(), std::io::Error> {
|
||||
@@ -100,9 +99,11 @@ fn get_compressed_offsets(oi: ObjectInfo, offset: i64) -> (i64, i64, i64, i64, u
|
||||
let parts: &[ObjectPartInfo] = &oi.parts;
|
||||
if skip_length > 0
|
||||
&& parts.len() > first_part_idx as usize
|
||||
&& parts[first_part_idx as usize].index.as_ref().expect("err").len() > 0
|
||||
&& parts[first_part_idx as usize].index.as_ref().is_some_and(|idx| idx.len() > 0)
|
||||
{
|
||||
todo!();
|
||||
let _ = part_skip;
|
||||
let _ = decrypt_skip;
|
||||
let _ = seq_num;
|
||||
}
|
||||
|
||||
(compressed_offset, part_skip, first_part_idx, decrypt_skip, seq_num)
|
||||
|
||||
@@ -243,11 +243,13 @@ impl TransitionClient {
|
||||
}
|
||||
|
||||
fn set_s3_transfer_accelerate(&self, accelerate_endpoint: &str) {
|
||||
todo!();
|
||||
let mut endpoint = self.s3_accelerate_endpoint.lock().unwrap();
|
||||
*endpoint = accelerate_endpoint.to_string();
|
||||
}
|
||||
|
||||
fn set_s3_enable_dual_stack(&self, enabled: bool) {
|
||||
todo!();
|
||||
let mut dual_stack = self.s3_dual_stack_enabled.lock().unwrap();
|
||||
*dual_stack = enabled;
|
||||
}
|
||||
|
||||
pub fn hash_materials(
|
||||
@@ -255,7 +257,22 @@ impl TransitionClient {
|
||||
is_md5_requested: bool,
|
||||
is_sha256_requested: bool,
|
||||
) -> (HashMap<String, HashAlgorithm>, HashMap<String, Vec<u8>>) {
|
||||
todo!()
|
||||
// `hash_algos` declares which algorithms are active for this multipart upload.
|
||||
// `hash_sums` keeps the current part digest bytes and is refreshed on every loop.
|
||||
let mut hash_algos = HashMap::new();
|
||||
let mut hash_sums = HashMap::new();
|
||||
|
||||
if is_md5_requested {
|
||||
hash_algos.insert("md5".to_string(), HashAlgorithm::Md5);
|
||||
hash_sums.insert("md5".to_string(), vec![]);
|
||||
}
|
||||
|
||||
if is_sha256_requested {
|
||||
hash_algos.insert("sha256".to_string(), HashAlgorithm::SHA256);
|
||||
hash_sums.insert("sha256".to_string(), vec![]);
|
||||
}
|
||||
|
||||
(hash_algos, hash_sums)
|
||||
}
|
||||
|
||||
fn is_online(&self) -> bool {
|
||||
@@ -272,7 +289,7 @@ impl TransitionClient {
|
||||
}
|
||||
|
||||
fn health_check(hc_duration: Duration) {
|
||||
todo!();
|
||||
let _ = hc_duration;
|
||||
}
|
||||
|
||||
fn dump_http(&self, req: &http::Request<s3s::Body>, resp: &http::Response<Incoming>) -> Result<(), std::io::Error> {
|
||||
@@ -734,7 +751,19 @@ impl TransitionCore {
|
||||
) -> Result<CompletePart, std::io::Error> {
|
||||
//self.0.copy_object_part_do(src_bucket, src_object, dest_bucket, dest_object, upload_id,
|
||||
// part_id, start_offset, length, metadata)
|
||||
todo!();
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Unsupported,
|
||||
crate::client::credentials::ErrorResponse {
|
||||
sts_error: crate::client::credentials::STSError {
|
||||
r#type: "".to_string(),
|
||||
code: "NotImplemented".to_string(),
|
||||
message: format!(
|
||||
"copy_object_part is not implemented for {src_bucket}/{src_object} -> {dest_bucket}/{dest_object}"
|
||||
),
|
||||
},
|
||||
request_id: "".to_string(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn put_object(
|
||||
|
||||
@@ -90,6 +90,8 @@ pub fn to_unformatted_disk_error(io_err: std::io::Error) -> std::io::Error {
|
||||
match io_err.kind() {
|
||||
std::io::ErrorKind::NotFound => DiskError::UnformattedDisk.into(),
|
||||
std::io::ErrorKind::PermissionDenied => DiskError::DiskAccessDenied.into(),
|
||||
std::io::ErrorKind::UnexpectedEof => DiskError::UnformattedDisk.into(),
|
||||
std::io::ErrorKind::InvalidData => DiskError::UnformattedDisk.into(),
|
||||
std::io::ErrorKind::Other => match io_err.downcast::<DiskError>() {
|
||||
Ok(err) => match err {
|
||||
DiskError::FileNotFound => DiskError::UnformattedDisk.into(),
|
||||
@@ -97,11 +99,11 @@ pub fn to_unformatted_disk_error(io_err: std::io::Error) -> std::io::Error {
|
||||
DiskError::VolumeNotFound => DiskError::UnformattedDisk.into(),
|
||||
DiskError::FileAccessDenied => DiskError::DiskAccessDenied.into(),
|
||||
DiskError::DiskAccessDenied => DiskError::DiskAccessDenied.into(),
|
||||
_ => DiskError::CorruptedBackend.into(),
|
||||
_ => DiskError::UnformattedDisk.into(),
|
||||
},
|
||||
Err(_err) => DiskError::CorruptedBackend.into(),
|
||||
Err(_err) => DiskError::UnformattedDisk.into(),
|
||||
},
|
||||
_ => DiskError::CorruptedBackend.into(),
|
||||
_ => DiskError::UnformattedDisk.into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,18 +371,18 @@ mod tests {
|
||||
let result = to_unformatted_disk_error(io_error);
|
||||
assert!(contains_disk_error(result, DiskError::DiskAccessDenied));
|
||||
|
||||
// Test Other error kind with other DiskError -> CorruptedBackend
|
||||
// Test Other error kind with other DiskError -> UnformattedDisk
|
||||
let io_error = create_io_error_with_disk_error(DiskError::DiskFull);
|
||||
let result = to_unformatted_disk_error(io_error);
|
||||
assert!(contains_disk_error(result, DiskError::CorruptedBackend));
|
||||
assert!(contains_disk_error(result, DiskError::UnformattedDisk));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_unformatted_disk_error_recursive_behavior() {
|
||||
// Test with non-Other error kind that should be handled without infinite recursion
|
||||
let result = to_unformatted_disk_error(create_io_error(ErrorKind::Interrupted));
|
||||
// This should not cause infinite recursion and should produce CorruptedBackend
|
||||
assert!(contains_disk_error(result, DiskError::CorruptedBackend));
|
||||
// This should not cause infinite recursion and should produce UnformattedDisk
|
||||
assert!(contains_disk_error(result, DiskError::UnformattedDisk));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -428,7 +428,7 @@ impl PoolEndpointList {
|
||||
}
|
||||
}
|
||||
|
||||
unimplemented!()
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Clone)]
|
||||
pub enum EventName {
|
||||
ObjectAccessedGet,
|
||||
ObjectAccessedGetRetention,
|
||||
@@ -61,11 +61,129 @@ pub enum EventName {
|
||||
|
||||
impl EventName {
|
||||
fn expand(&self) -> Vec<EventName> {
|
||||
todo!();
|
||||
match self.clone() {
|
||||
EventName::Everything => vec![
|
||||
EventName::BucketCreated,
|
||||
EventName::BucketRemoved,
|
||||
EventName::ObjectAccessedAll,
|
||||
EventName::ObjectCreatedAll,
|
||||
EventName::ObjectRemovedAll,
|
||||
EventName::ObjectManyVersions,
|
||||
EventName::ObjectLargeVersions,
|
||||
EventName::PrefixManyFolders,
|
||||
EventName::ILMDelMarkerExpirationDelete,
|
||||
EventName::ObjectReplicationAll,
|
||||
EventName::ObjectRestoreAll,
|
||||
EventName::ObjectTransitionAll,
|
||||
],
|
||||
EventName::ObjectAccessedAll => vec![
|
||||
EventName::ObjectAccessedGet,
|
||||
EventName::ObjectAccessedGetRetention,
|
||||
EventName::ObjectAccessedGetLegalHold,
|
||||
EventName::ObjectAccessedHead,
|
||||
EventName::ObjectAccessedAttributes,
|
||||
],
|
||||
EventName::ObjectCreatedAll => vec![
|
||||
EventName::ObjectCreatedCompleteMultipartUpload,
|
||||
EventName::ObjectCreatedCopy,
|
||||
EventName::ObjectCreatedPost,
|
||||
EventName::ObjectCreatedPut,
|
||||
EventName::ObjectCreatedPutRetention,
|
||||
EventName::ObjectCreatedPutLegalHold,
|
||||
EventName::ObjectCreatedPutTagging,
|
||||
EventName::ObjectCreatedDeleteTagging,
|
||||
],
|
||||
EventName::ObjectRemovedAll => vec![
|
||||
EventName::ObjectRemovedDelete,
|
||||
EventName::ObjectRemovedDeleteMarkerCreated,
|
||||
EventName::ObjectRemovedNoOP,
|
||||
EventName::ObjectRemovedDeleteAllVersions,
|
||||
],
|
||||
EventName::ObjectReplicationAll => vec![
|
||||
EventName::ObjectReplicationFailed,
|
||||
EventName::ObjectReplicationComplete,
|
||||
EventName::ObjectReplicationNotTracked,
|
||||
EventName::ObjectReplicationMissedThreshold,
|
||||
EventName::ObjectReplicationReplicatedAfterThreshold,
|
||||
],
|
||||
EventName::ObjectRestoreAll => vec![EventName::ObjectRestorePost, EventName::ObjectRestoreCompleted],
|
||||
EventName::ObjectTransitionAll => vec![EventName::ObjectTransitionFailed, EventName::ObjectTransitionComplete],
|
||||
EventName::ObjectSingleTypesEnd | EventName::ObjectScannerAll => vec![self.clone()],
|
||||
_ => vec![self.clone()],
|
||||
}
|
||||
}
|
||||
|
||||
fn mask(&self) -> u64 {
|
||||
todo!();
|
||||
match self {
|
||||
EventName::Everything => u64::MAX,
|
||||
EventName::BucketCreated => 1_u64 << 0,
|
||||
EventName::BucketRemoved => 1_u64 << 1,
|
||||
EventName::ObjectAccessedGet => 1_u64 << 2,
|
||||
EventName::ObjectAccessedGetRetention => 1_u64 << 3,
|
||||
EventName::ObjectAccessedGetLegalHold => 1_u64 << 4,
|
||||
EventName::ObjectAccessedHead => 1_u64 << 5,
|
||||
EventName::ObjectAccessedAttributes => 1_u64 << 6,
|
||||
EventName::ObjectCreatedCompleteMultipartUpload => 1_u64 << 7,
|
||||
EventName::ObjectCreatedCopy => 1_u64 << 8,
|
||||
EventName::ObjectCreatedPost => 1_u64 << 9,
|
||||
EventName::ObjectCreatedPut => 1_u64 << 10,
|
||||
EventName::ObjectCreatedPutRetention => 1_u64 << 11,
|
||||
EventName::ObjectCreatedPutLegalHold => 1_u64 << 12,
|
||||
EventName::ObjectCreatedPutTagging => 1_u64 << 13,
|
||||
EventName::ObjectCreatedDeleteTagging => 1_u64 << 14,
|
||||
EventName::ObjectRemovedDelete => 1_u64 << 15,
|
||||
EventName::ObjectRemovedDeleteMarkerCreated => 1_u64 << 16,
|
||||
EventName::ObjectRemovedDeleteAllVersions => 1_u64 << 17,
|
||||
EventName::ObjectRemovedNoOP => 1_u64 << 18,
|
||||
EventName::ObjectManyVersions => 1_u64 << 19,
|
||||
EventName::ObjectLargeVersions => 1_u64 << 20,
|
||||
EventName::PrefixManyFolders => 1_u64 << 21,
|
||||
EventName::ILMDelMarkerExpirationDelete => 1_u64 << 22,
|
||||
EventName::ObjectReplicationFailed => 1_u64 << 23,
|
||||
EventName::ObjectReplicationComplete => 1_u64 << 24,
|
||||
EventName::ObjectReplicationMissedThreshold => 1_u64 << 25,
|
||||
EventName::ObjectReplicationReplicatedAfterThreshold => 1_u64 << 26,
|
||||
EventName::ObjectReplicationNotTracked => 1_u64 << 27,
|
||||
EventName::ObjectRestorePost => 1_u64 << 28,
|
||||
EventName::ObjectRestoreCompleted => 1_u64 << 29,
|
||||
EventName::ObjectRestoreAll => 1_u64 << 30,
|
||||
EventName::ObjectTransitionFailed => 1_u64 << 31,
|
||||
EventName::ObjectTransitionComplete => 1_u64 << 32,
|
||||
EventName::ObjectAccessedAll => {
|
||||
EventName::ObjectAccessedGet.mask()
|
||||
| EventName::ObjectAccessedGetRetention.mask()
|
||||
| EventName::ObjectAccessedGetLegalHold.mask()
|
||||
| EventName::ObjectAccessedHead.mask()
|
||||
| EventName::ObjectAccessedAttributes.mask()
|
||||
}
|
||||
EventName::ObjectCreatedAll => {
|
||||
EventName::ObjectCreatedCompleteMultipartUpload.mask()
|
||||
| EventName::ObjectCreatedCopy.mask()
|
||||
| EventName::ObjectCreatedPost.mask()
|
||||
| EventName::ObjectCreatedPut.mask()
|
||||
| EventName::ObjectCreatedPutRetention.mask()
|
||||
| EventName::ObjectCreatedPutLegalHold.mask()
|
||||
| EventName::ObjectCreatedPutTagging.mask()
|
||||
| EventName::ObjectCreatedDeleteTagging.mask()
|
||||
}
|
||||
EventName::ObjectRemovedAll => {
|
||||
EventName::ObjectRemovedDelete.mask()
|
||||
| EventName::ObjectRemovedDeleteMarkerCreated.mask()
|
||||
| EventName::ObjectRemovedNoOP.mask()
|
||||
| EventName::ObjectRemovedDeleteAllVersions.mask()
|
||||
}
|
||||
EventName::ObjectReplicationAll => {
|
||||
EventName::ObjectReplicationFailed.mask()
|
||||
| EventName::ObjectReplicationComplete.mask()
|
||||
| EventName::ObjectReplicationMissedThreshold.mask()
|
||||
| EventName::ObjectReplicationReplicatedAfterThreshold.mask()
|
||||
| EventName::ObjectReplicationNotTracked.mask()
|
||||
}
|
||||
EventName::ObjectTransitionAll => {
|
||||
EventName::ObjectTransitionFailed.mask() | EventName::ObjectTransitionComplete.mask()
|
||||
}
|
||||
EventName::ObjectSingleTypesEnd | EventName::ObjectScannerAll => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,9 @@ use crate::store::ECStore;
|
||||
use crate::store_api::ObjectInfo;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::warn;
|
||||
|
||||
pub struct EventNotifier {
|
||||
target_list: TargetList,
|
||||
@@ -37,24 +39,33 @@ impl EventNotifier {
|
||||
}
|
||||
|
||||
fn get_arn_list(&self) -> Vec<String> {
|
||||
todo!();
|
||||
warn!(
|
||||
event_count = self.target_list.total_events.load(Ordering::Relaxed),
|
||||
"event notifier arn list requested but not implemented in this build"
|
||||
);
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn set(&self, bucket: &str, meta: BucketMetadata) {
|
||||
todo!();
|
||||
warn!(bucket = bucket, "event notifier set() called but currently no-op in this build");
|
||||
}
|
||||
|
||||
fn init_bucket_targets(&self, api: ECStore) -> Result<(), std::io::Error> {
|
||||
fn init_bucket_targets(&self, _api: ECStore) -> Result<(), std::io::Error> {
|
||||
/*if err := self.target_list.Add(globalNotifyTargetList.Targets()...); err != nil {
|
||||
return err
|
||||
}
|
||||
self.target_list = self.target_list.Init(runtime.GOMAXPROCS(0)) // TODO: make this configurable (y4m4)
|
||||
nil*/
|
||||
todo!();
|
||||
warn!("init_bucket_targets called but currently no-op in this build");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send(&self, args: EventArgs) {
|
||||
todo!();
|
||||
warn!(
|
||||
event_name = args.event_name,
|
||||
bucket = args.bucket_name,
|
||||
"event send() called but notifier is not fully implemented in this build"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -369,19 +369,23 @@ impl PeerRestClient {
|
||||
}
|
||||
|
||||
pub async fn download_profile_data(&self) -> Result<()> {
|
||||
todo!()
|
||||
warn!("download_profile_data is not implemented in PeerRestClient");
|
||||
Err(Error::NotImplemented)
|
||||
}
|
||||
|
||||
pub async fn get_bucket_stats(&self) -> Result<()> {
|
||||
todo!()
|
||||
warn!("get_bucket_stats is not implemented in PeerRestClient");
|
||||
Err(Error::NotImplemented)
|
||||
}
|
||||
|
||||
pub async fn get_sr_metrics(&self) -> Result<()> {
|
||||
todo!()
|
||||
warn!("get_sr_metrics is not implemented in PeerRestClient");
|
||||
Err(Error::NotImplemented)
|
||||
}
|
||||
|
||||
pub async fn get_all_bucket_stats(&self) -> Result<()> {
|
||||
todo!()
|
||||
warn!("get_all_bucket_stats is not implemented in PeerRestClient");
|
||||
Err(Error::NotImplemented)
|
||||
}
|
||||
|
||||
pub async fn load_bucket_metadata(&self, bucket: &str) -> Result<()> {
|
||||
@@ -602,13 +606,13 @@ impl PeerRestClient {
|
||||
}
|
||||
|
||||
pub async fn get_metacache_listing(&self) -> Result<()> {
|
||||
let _client = self.get_client().await?;
|
||||
todo!()
|
||||
warn!("get_metacache_listing is not implemented in PeerRestClient");
|
||||
Err(Error::NotImplemented)
|
||||
}
|
||||
|
||||
pub async fn update_metacache_listing(&self) -> Result<()> {
|
||||
let _client = self.get_client().await?;
|
||||
todo!()
|
||||
warn!("update_metacache_listing is not implemented in PeerRestClient");
|
||||
Err(Error::NotImplemented)
|
||||
}
|
||||
|
||||
pub async fn reload_pool_meta(&self) -> Result<()> {
|
||||
|
||||
@@ -362,7 +362,7 @@ impl S3PeerSys {
|
||||
}
|
||||
|
||||
pub fn get_pools(&self) -> Option<Vec<usize>> {
|
||||
unimplemented!()
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ use crate::heal::{
|
||||
use crate::{Error, Result};
|
||||
use rustfs_common::heal_channel::{
|
||||
HealChannelCommand, HealChannelPriority, HealChannelReceiver, HealChannelRequest, HealChannelResponse, HealScanMode,
|
||||
publish_heal_response,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
@@ -226,9 +227,15 @@ impl HealChannelProcessor {
|
||||
}
|
||||
|
||||
fn publish_response(&self, response: HealChannelResponse) {
|
||||
if let Err(e) = self.response_sender.send(response) {
|
||||
// Try to send to local channel first, but don't block broadcast on failure
|
||||
if let Err(e) = self.response_sender.send(response.clone()) {
|
||||
error!("Failed to enqueue heal response locally: {}", e);
|
||||
}
|
||||
// Always attempt to broadcast, even if local send failed
|
||||
// Use the original response for broadcast; local send uses a clone
|
||||
if let Err(e) = publish_heal_response(response) {
|
||||
error!("Failed to broadcast heal response: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get response sender for external use
|
||||
|
||||
@@ -36,6 +36,22 @@ use tokio_util::sync::CancellationToken;
|
||||
use tracing::info;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
const HEAL_FORMAT_WAIT_TIMEOUT: Duration = Duration::from_secs(25);
|
||||
const HEAL_FORMAT_WAIT_INTERVAL: Duration = Duration::from_millis(250);
|
||||
|
||||
async fn wait_for_path_exists(path: &Path, timeout: Duration, interval: Duration) -> bool {
|
||||
let deadline = tokio::time::Instant::now() + timeout;
|
||||
loop {
|
||||
if path.exists() {
|
||||
return true;
|
||||
}
|
||||
if tokio::time::Instant::now() >= deadline {
|
||||
return false;
|
||||
}
|
||||
tokio::time::sleep(interval).await;
|
||||
}
|
||||
}
|
||||
|
||||
static GLOBAL_ENV: OnceLock<(Vec<PathBuf>, Arc<ECStore>, Arc<ECStoreHealStorage>)> = OnceLock::new();
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
@@ -49,17 +65,6 @@ pub fn init_tracing() {
|
||||
});
|
||||
}
|
||||
|
||||
async fn wait_for_path_exists(path: &Path, timeout: Duration) -> bool {
|
||||
let deadline = std::time::Instant::now() + timeout;
|
||||
while std::time::Instant::now() < deadline {
|
||||
if path.exists() {
|
||||
return true;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
}
|
||||
path.exists()
|
||||
}
|
||||
|
||||
/// Test helper: Create test environment with ECStore
|
||||
async fn setup_test_env() -> (Vec<PathBuf>, Arc<ECStore>, Arc<ECStoreHealStorage>) {
|
||||
init_tracing();
|
||||
@@ -176,7 +181,7 @@ mod serial_tests {
|
||||
|
||||
create_test_bucket(&ecstore, bucket_name).await;
|
||||
upload_test_object(&ecstore, bucket_name, object_name, test_data).await;
|
||||
|
||||
let _obj_dir = disk_paths[0].join(bucket_name).join(object_name);
|
||||
// ─── 1️⃣ delete single data shard file ─────────────────────────────────────
|
||||
let obj_dir = disk_paths[0].join(bucket_name).join(object_name);
|
||||
// find part file at depth 2, e.g. .../<uuid>/part.1
|
||||
@@ -337,14 +342,16 @@ mod serial_tests {
|
||||
assert!(!format_path.exists(), "format.json still exists after deletion");
|
||||
println!("✅ Deleted format.json on disk: {format_path:?}");
|
||||
|
||||
let (_result, error) = heal_storage.heal_format(false).await.expect("Failed to heal format");
|
||||
assert!(error.is_none(), "Heal format returned error: {error:?}");
|
||||
let (_format_result, format_error) = heal_storage.heal_format(false).await.expect("failed to run heal_format");
|
||||
if let Some(err) = format_error {
|
||||
info!("heal_format returned error: {:?}", err);
|
||||
}
|
||||
|
||||
// Wait for task completion
|
||||
let restored = wait_for_path_exists(&format_path, Duration::from_secs(20)).await;
|
||||
let restored = wait_for_path_exists(&format_path, HEAL_FORMAT_WAIT_TIMEOUT, HEAL_FORMAT_WAIT_INTERVAL).await;
|
||||
assert!(restored, "format.json does not exist on disk after heal");
|
||||
|
||||
// ─── 2️⃣ verify format.json is restored ───────
|
||||
assert!(restored, "format.json does not exist on disk after heal");
|
||||
assert!(format_path.exists(), "format.json does not exist on disk after heal");
|
||||
|
||||
info!("Heal format basic test passed");
|
||||
}
|
||||
@@ -361,6 +368,15 @@ mod serial_tests {
|
||||
|
||||
create_test_bucket(&ecstore, bucket_name).await;
|
||||
upload_test_object(&ecstore, bucket_name, object_name, test_data).await;
|
||||
let obj_dir = disk_paths[0].join(bucket_name).join(object_name);
|
||||
let target_part = WalkDir::new(&obj_dir)
|
||||
.min_depth(2)
|
||||
.max_depth(2)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.find(|e| e.file_type().is_file() && e.file_name().to_str().map(|n| n.starts_with("part.")).unwrap_or(false))
|
||||
.map(|e| e.into_path())
|
||||
.expect("Failed to locate part file to delete");
|
||||
|
||||
// ─── 1️⃣ delete format.json on one disk ──────────────
|
||||
let format_path = disk_paths[0].join(".rustfs.sys").join("format.json");
|
||||
@@ -368,31 +384,43 @@ mod serial_tests {
|
||||
std::fs::create_dir_all(&disk_paths[0]).expect("failed to recreate disk_paths[0] directory");
|
||||
println!("✅ Deleted format.json on disk: {:?}", disk_paths[0]);
|
||||
|
||||
let (_result, error) = heal_storage.heal_format(false).await.expect("Failed to heal format");
|
||||
assert!(error.is_none(), "Heal format returned error: {error:?}");
|
||||
let (_format_result, format_error) = heal_storage.heal_format(false).await.expect("failed to run heal_format");
|
||||
if let Some(err) = format_error {
|
||||
info!("heal_format returned warning/error: {:?}", err);
|
||||
}
|
||||
|
||||
// Wait for task completion
|
||||
let restored = wait_for_path_exists(&format_path, Duration::from_secs(20)).await;
|
||||
|
||||
// ─── 2️⃣ verify format.json is restored ───────
|
||||
assert!(restored, "format.json does not exist on disk after heal");
|
||||
// ─── 3 verify each part file is restored ───────
|
||||
let heal_opts = HealOpts {
|
||||
recursive: false,
|
||||
dry_run: false,
|
||||
remove: false,
|
||||
let bucket_heal_opts = HealOpts {
|
||||
recursive: true,
|
||||
recreate: true,
|
||||
scan_mode: HealScanMode::Normal,
|
||||
update_parity: true,
|
||||
no_lock: false,
|
||||
pool: None,
|
||||
set: None,
|
||||
..Default::default()
|
||||
};
|
||||
let (_result, error) = heal_storage
|
||||
heal_storage
|
||||
.heal_bucket(bucket_name, &bucket_heal_opts)
|
||||
.await
|
||||
.expect("failed to heal bucket");
|
||||
|
||||
let heal_opts = HealOpts {
|
||||
recreate: true,
|
||||
remove: false,
|
||||
..Default::default()
|
||||
};
|
||||
let (object_result, object_error) = heal_storage
|
||||
.heal_object(bucket_name, object_name, None, &heal_opts)
|
||||
.await
|
||||
.expect("Failed to heal object");
|
||||
assert!(error.is_none(), "Heal object returned error: {error:?}");
|
||||
.expect("failed to heal object");
|
||||
info!("heal_object result: {:?}, error: {:?}", object_result, object_error);
|
||||
assert!(object_error.is_none(), "heal_object returned error: {object_error:?}");
|
||||
|
||||
let format_restored = wait_for_path_exists(&format_path, HEAL_FORMAT_WAIT_TIMEOUT, HEAL_FORMAT_WAIT_INTERVAL).await;
|
||||
assert!(format_restored, "format.json does not exist on disk after heal");
|
||||
let target_restored = wait_for_path_exists(&target_part, HEAL_FORMAT_WAIT_TIMEOUT, HEAL_FORMAT_WAIT_INTERVAL).await;
|
||||
|
||||
// ─── 3️⃣ verify format.json is restored ───────
|
||||
assert!(format_path.exists(), "format.json does not exist on disk after heal");
|
||||
assert!(target_restored, "part file was not restored after heal");
|
||||
|
||||
// ─── 3️⃣ verify each part file is restored ───────
|
||||
assert!(target_part.exists());
|
||||
|
||||
// Verify object metadata is accessible
|
||||
let obj_info = ecstore
|
||||
|
||||
@@ -157,22 +157,24 @@ impl CacheInner {
|
||||
|
||||
// todo
|
||||
pub fn is_allowed_sts(&self, _args: &Args, _parent: &str) -> bool {
|
||||
warn!("unimplement is_allowed_sts");
|
||||
warn!("policy cache STS check path is not implemented");
|
||||
false
|
||||
}
|
||||
|
||||
// todo
|
||||
pub fn is_allowed_service_account(&self, _args: &Args, _parent: &str) -> bool {
|
||||
warn!("unimplement is_allowed_sts");
|
||||
warn!("policy cache service account check path is not implemented");
|
||||
false
|
||||
}
|
||||
|
||||
pub fn is_allowed(&self, _args: Args) -> bool {
|
||||
todo!()
|
||||
warn!("policy cache is_allowed check path is currently denied by default");
|
||||
false
|
||||
}
|
||||
|
||||
pub fn policy_db_get(&self, _name: &str, _groups: &[String]) -> Vec<String> {
|
||||
todo!()
|
||||
warn!("policy cache policy_db_get is not implemented, returning empty policy set");
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ impl TraceType {
|
||||
}
|
||||
|
||||
pub fn single_type(&self) -> bool {
|
||||
todo!()
|
||||
self.0.count_ones() == 1
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, other: &TraceType) {
|
||||
|
||||
@@ -16,7 +16,7 @@ use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use chrono::Utc;
|
||||
use futures::pin_mut;
|
||||
use futures::{Stream, StreamExt};
|
||||
use futures::{Stream, StreamExt, future::ready, stream};
|
||||
use futures_core::stream::BoxStream;
|
||||
use http::HeaderMap;
|
||||
use object_store::{
|
||||
@@ -132,14 +132,24 @@ impl std::fmt::Display for EcObjectStore {
|
||||
}
|
||||
}
|
||||
|
||||
fn unsupported_store_error(op: &str) -> o_Error {
|
||||
o_Error::Generic {
|
||||
store: "s3select-api",
|
||||
source: Box::new(std::io::Error::new(
|
||||
std::io::ErrorKind::Unsupported,
|
||||
format!("operation {op} is not supported in EcObjectStore"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ObjectStore for EcObjectStore {
|
||||
async fn put_opts(&self, _location: &Path, _payload: PutPayload, _opts: PutOptions) -> Result<PutResult> {
|
||||
unimplemented!()
|
||||
Err(unsupported_store_error("put_opts"))
|
||||
}
|
||||
|
||||
async fn put_multipart_opts(&self, _location: &Path, _opts: PutMultipartOptions) -> Result<Box<dyn MultipartUpload>> {
|
||||
unimplemented!()
|
||||
Err(unsupported_store_error("put_multipart_opts"))
|
||||
}
|
||||
|
||||
async fn get_opts(&self, location: &Path, _options: GetOptions) -> Result<GetResult> {
|
||||
@@ -220,7 +230,7 @@ impl ObjectStore for EcObjectStore {
|
||||
}
|
||||
|
||||
async fn get_ranges(&self, _location: &Path, _ranges: &[Range<u64>]) -> Result<Vec<Bytes>> {
|
||||
unimplemented!()
|
||||
Err(unsupported_store_error("get_ranges"))
|
||||
}
|
||||
|
||||
async fn head(&self, location: &Path) -> Result<ObjectMeta> {
|
||||
@@ -245,23 +255,23 @@ impl ObjectStore for EcObjectStore {
|
||||
}
|
||||
|
||||
async fn delete(&self, _location: &Path) -> Result<()> {
|
||||
unimplemented!()
|
||||
Err(unsupported_store_error("delete"))
|
||||
}
|
||||
|
||||
fn list(&self, _prefix: Option<&Path>) -> BoxStream<'static, Result<ObjectMeta>> {
|
||||
unimplemented!()
|
||||
stream::once(ready(Err(unsupported_store_error("list")))).boxed()
|
||||
}
|
||||
|
||||
async fn list_with_delimiter(&self, _prefix: Option<&Path>) -> Result<ListResult> {
|
||||
unimplemented!()
|
||||
Err(unsupported_store_error("list_with_delimiter"))
|
||||
}
|
||||
|
||||
async fn copy(&self, _from: &Path, _to: &Path) -> Result<()> {
|
||||
unimplemented!()
|
||||
Err(unsupported_store_error("copy"))
|
||||
}
|
||||
|
||||
async fn copy_if_not_exists(&self, _from: &Path, _too: &Path) -> Result<()> {
|
||||
unimplemented!()
|
||||
Err(unsupported_store_error("copy_if_not_exists"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -225,7 +225,11 @@ pub fn must_get_local_ips() -> std::io::Result<Vec<IpAddr>> {
|
||||
}
|
||||
|
||||
pub fn get_default_location(_u: Url, _region_override: &str) -> String {
|
||||
todo!();
|
||||
if !_region_override.is_empty() {
|
||||
return _region_override.to_string();
|
||||
}
|
||||
|
||||
_u.host().map(|host| host.to_string()).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn get_endpoint_url(endpoint: &str, secure: bool) -> Result<Url, Error> {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
use futures::Stream;
|
||||
use hyper::http;
|
||||
use std::{
|
||||
io::ErrorKind,
|
||||
pin::Pin,
|
||||
sync::LazyLock,
|
||||
task::{Context, Poll},
|
||||
@@ -157,7 +158,17 @@ pub fn is_request_error_retryable(_err: std::io::Error) -> bool {
|
||||
};
|
||||
}
|
||||
true*/
|
||||
todo!();
|
||||
matches!(
|
||||
_err.kind(),
|
||||
ErrorKind::Interrupted
|
||||
| ErrorKind::WouldBlock
|
||||
| ErrorKind::TimedOut
|
||||
| ErrorKind::ConnectionAborted
|
||||
| ErrorKind::ConnectionRefused
|
||||
| ErrorKind::ConnectionReset
|
||||
| ErrorKind::NotConnected
|
||||
| ErrorKind::UnexpectedEof
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user