Merge branch 'main' of github.com:rustfs/s3-rustfs into feature/observability-metrics

# Conflicts:
#	.github/workflows/build.yml
#	.github/workflows/ci.yml
#	Cargo.lock
#	Cargo.toml
#	appauth/src/token.rs
#	crates/config/src/config.rs
#	crates/event-notifier/examples/simple.rs
#	crates/event-notifier/src/global.rs
#	crates/event-notifier/src/lib.rs
#	crates/event-notifier/src/notifier.rs
#	crates/event-notifier/src/store.rs
#	crates/filemeta/src/filemeta.rs
#	crates/notify/examples/webhook.rs
#	crates/utils/Cargo.toml
#	ecstore/Cargo.toml
#	ecstore/src/cmd/bucket_replication.rs
#	ecstore/src/config/com.rs
#	ecstore/src/disk/error.rs
#	ecstore/src/disk/mod.rs
#	ecstore/src/set_disk.rs
#	ecstore/src/store_api.rs
#	ecstore/src/store_list_objects.rs
#	iam/Cargo.toml
#	iam/src/manager.rs
#	policy/Cargo.toml
#	rustfs/src/admin/rpc.rs
#	rustfs/src/main.rs
#	rustfs/src/storage/mod.rs
This commit is contained in:
houseme
2025-06-19 13:16:48 +08:00
249 changed files with 25137 additions and 11731 deletions
+5
View File
@@ -93,6 +93,11 @@ tower-http = { workspace = true, features = [
] }
urlencoding = { workspace = true }
uuid = { workspace = true }
rustfs-filemeta.workspace = true
rustfs-rio.workspace = true
base64 = { workspace = true }
hmac = { workspace = true }
sha2 = { workspace = true }
[target.'cfg(target_os = "linux")'.dependencies]
libsystemd.workspace = true
+10 -11
View File
@@ -2,45 +2,44 @@ use super::router::Operation;
use crate::auth::check_key_valid;
use crate::auth::get_condition_values;
use crate::auth::get_session_token;
//use ecstore::error::Error as ec_Error;
use crate::storage::error::to_s3_error;
use crate::error::ApiError;
use bytes::Bytes;
use common::error::Error as ec_Error;
use ecstore::admin_server_info::get_server_info;
use ecstore::bucket::metadata_sys::{self, get_replication_config};
use ecstore::bucket::target::BucketTarget;
use ecstore::bucket::versioning_sys::BucketVersioningSys;
use ecstore::cmd::bucket_targets::{self, GLOBAL_Bucket_Target_Sys};
use ecstore::error::StorageError;
use ecstore::global::GLOBAL_ALlHealState;
use ecstore::heal::data_usage::load_data_usage_from_backend;
use ecstore::heal::heal_commands::HealOpts;
use ecstore::heal::heal_ops::new_heal_sequence;
use ecstore::metrics_realtime::{collect_local_metrics, CollectMetricsOpts, MetricType};
use ecstore::metrics_realtime::{CollectMetricsOpts, MetricType, collect_local_metrics};
use ecstore::new_object_layer_fn;
use ecstore::peer::is_reserved_or_invalid_bucket;
use ecstore::pools::{get_total_usable_capacity, get_total_usable_capacity_free};
use ecstore::store::is_valid_object_prefix;
use ecstore::store_api::BucketOptions;
use ecstore::store_api::StorageAPI;
use ecstore::utils::path::path_join;
use futures::{Stream, StreamExt};
use http::{HeaderMap, Uri};
use hyper::StatusCode;
use iam::get_global_action_cred;
use iam::store::MappedPolicy;
use rustfs_utils::path::path_join;
// use lazy_static::lazy_static;
use madmin::metrics::RealtimeMetrics;
use madmin::utils::parse_duration;
use matchit::Params;
use percent_encoding::{percent_encode, AsciiSet, CONTROLS};
use percent_encoding::{AsciiSet, CONTROLS, percent_encode};
use policy::policy::Args;
use policy::policy::BucketPolicy;
use policy::policy::action::Action;
use policy::policy::action::S3Action;
use policy::policy::default::DEFAULT_POLICIES;
use policy::policy::Args;
use policy::policy::BucketPolicy;
use s3s::header::CONTENT_TYPE;
use s3s::stream::{ByteStream, DynByteStream};
use s3s::{s3_error, Body, S3Error, S3Request, S3Response, S3Result};
use s3s::{Body, S3Error, S3Request, S3Response, S3Result, s3_error};
use s3s::{S3ErrorCode, StdError};
use serde::{Deserialize, Serialize};
// use serde_json::to_vec;
@@ -666,7 +665,7 @@ impl Operation for HealHandler {
#[derive(Default)]
struct HealResp {
resp_bytes: Vec<u8>,
_api_err: Option<ec_Error>,
_api_err: Option<StorageError>,
_err_body: String,
}
@@ -836,7 +835,7 @@ impl Operation for SetRemoteTargetHandler {
}
}
let mut remote_target: BucketTarget = serde_json::from_slice(&body).map_err(|arg0| to_s3_error(arg0.into()))?; // 错误会被传播
let mut remote_target: BucketTarget = serde_json::from_slice(&body).map_err(ApiError::other)?; // 错误会被传播
remote_target.source_bucket = bucket.clone();
info!("remote target {} And arn is:", remote_target.source_bucket.clone());
+1 -1
View File
@@ -5,7 +5,7 @@ use iam::{
};
use madmin::GroupAddRemove;
use matchit::Params;
use s3s::{header::CONTENT_TYPE, s3_error, Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result};
use s3s::{Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result, header::CONTENT_TYPE, s3_error};
use serde::Deserialize;
use serde_urlencoded::from_bytes;
use tracing::warn;
+1 -1
View File
@@ -3,7 +3,7 @@ use http::{HeaderMap, StatusCode};
use iam::{error::is_err_no_such_user, get_global_action_cred, store::MappedPolicy};
use matchit::Params;
use policy::policy::Policy;
use s3s::{header::CONTENT_TYPE, s3_error, Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result};
use s3s::{Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result, header::CONTENT_TYPE, s3_error};
use serde::Deserialize;
use serde_urlencoded::from_bytes;
use std::collections::HashMap;
+9 -17
View File
@@ -1,13 +1,13 @@
use ecstore::{new_object_layer_fn, GLOBAL_Endpoints};
use ecstore::{GLOBAL_Endpoints, new_object_layer_fn};
use http::{HeaderMap, StatusCode};
use matchit::Params;
use s3s::{header::CONTENT_TYPE, s3_error, Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result};
use s3s::{Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result, header::CONTENT_TYPE, s3_error};
use serde::Deserialize;
use serde_urlencoded::from_bytes;
use tokio::sync::broadcast;
use tracing::warn;
use crate::{admin::router::Operation, storage::error::to_s3_error};
use crate::{admin::router::Operation, error::ApiError};
pub struct ListPools {}
@@ -33,7 +33,7 @@ impl Operation for ListPools {
let mut pools_status = Vec::new();
for (idx, _) in endpoints.as_ref().iter().enumerate() {
let state = store.status(idx).await.map_err(to_s3_error)?;
let state = store.status(idx).await.map_err(ApiError::from)?;
pools_status.push(state);
}
@@ -88,11 +88,7 @@ impl Operation for StatusPool {
let has_idx = {
if is_byid {
let a = query.pool.parse::<usize>().unwrap_or_default();
if a < endpoints.as_ref().len() {
Some(a)
} else {
None
}
if a < endpoints.as_ref().len() { Some(a) } else { None }
} else {
endpoints.get_pool_idx(&query.pool)
}
@@ -107,7 +103,7 @@ impl Operation for StatusPool {
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
};
let pools_status = store.status(idx).await.map_err(to_s3_error)?;
let pools_status = store.status(idx).await.map_err(ApiError::from)?;
let data = serde_json::to_vec(&pools_status)
.map_err(|_e| S3Error::with_message(S3ErrorCode::InternalError, "parse accountInfo failed"))?;
@@ -195,7 +191,7 @@ impl Operation for StartDecommission {
}
if !pools_indices.is_empty() {
store.decommission(ctx_rx, pools_indices).await.map_err(to_s3_error)?;
store.decommission(ctx_rx, pools_indices).await.map_err(ApiError::from)?;
}
Ok(S3Response::new((StatusCode::OK, Body::default())))
@@ -234,11 +230,7 @@ impl Operation for CancelDecommission {
let has_idx = {
if is_byid {
let a = query.pool.parse::<usize>().unwrap_or_default();
if a < endpoints.as_ref().len() {
Some(a)
} else {
None
}
if a < endpoints.as_ref().len() { Some(a) } else { None }
} else {
endpoints.get_pool_idx(&query.pool)
}
@@ -253,7 +245,7 @@ impl Operation for CancelDecommission {
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
};
store.decommission_cancel(idx).await.map_err(to_s3_error)?;
store.decommission_cancel(idx).await.map_err(ApiError::from)?;
Ok(S3Response::new((StatusCode::OK, Body::default())))
}
+55 -20
View File
@@ -1,16 +1,17 @@
use ecstore::{
config::error::is_err_config_not_found,
StorageAPI,
error::StorageError,
new_object_layer_fn,
notification_sys::get_global_notification_sys,
rebalance::{DiskStat, RebalSaveOpt},
store_api::BucketOptions,
StorageAPI,
};
use http::{HeaderMap, StatusCode};
use matchit::Params;
use s3s::{header::CONTENT_TYPE, s3_error, Body, S3Request, S3Response, S3Result};
use s3s::{Body, S3Request, S3Response, S3Result, header::CONTENT_TYPE, s3_error};
use serde::{Deserialize, Serialize};
use std::time::{Duration, SystemTime};
use std::time::Duration;
use time::OffsetDateTime;
use tracing::warn;
use crate::admin::router::Operation;
@@ -56,8 +57,8 @@ pub struct RebalanceAdminStatus {
pub id: String, // Identifies the ongoing rebalance operation by a UUID
#[serde(rename = "pools")]
pub pools: Vec<RebalancePoolStatus>, // Contains all pools, including inactive
#[serde(rename = "stoppedAt")]
pub stopped_at: Option<SystemTime>, // Optional timestamp when rebalance was stopped
#[serde(rename = "stoppedAt", with = "offsetdatetime_rfc3339")]
pub stopped_at: Option<OffsetDateTime>, // Optional timestamp when rebalance was stopped
}
pub struct RebalanceStart {}
@@ -101,11 +102,13 @@ impl Operation for RebalanceStart {
}
};
store.start_rebalance().await;
warn!("Rebalance started with id: {}", id);
if let Some(notification_sys) = get_global_notification_sys() {
warn!("Loading rebalance meta");
warn!("RebalanceStart Loading rebalance meta start");
notification_sys.load_rebalance_meta(true).await;
warn!("Rebalance meta loaded");
warn!("RebalanceStart Loading rebalance meta done");
}
let resp = RebalanceResp { id };
@@ -133,7 +136,7 @@ impl Operation for RebalanceStatus {
let mut meta = RebalanceMeta::new();
if let Err(err) = meta.load(store.pools[0].clone()).await {
if is_err_config_not_found(&err) {
if err == StorageError::ConfigNotFound {
return Err(s3_error!(NoSuchResource, "Pool rebalance is not started"));
}
@@ -175,15 +178,14 @@ impl Operation for RebalanceStatus {
let total_bytes_to_rebal = ps.init_capacity as f64 * meta.percent_free_goal - ps.init_free_space as f64;
let mut elapsed = if let Some(start_time) = ps.info.start_time {
SystemTime::now()
.duration_since(start_time)
.map_err(|e| s3_error!(InternalError, "Failed to calculate elapsed time: {}", e))?
let now = OffsetDateTime::now_utc();
now - start_time
} else {
return Err(s3_error!(InternalError, "Start time is not available"));
};
let mut eta = if ps.bytes > 0 {
Duration::from_secs_f64(total_bytes_to_rebal * elapsed.as_secs_f64() / ps.bytes as f64)
Duration::from_secs_f64(total_bytes_to_rebal * elapsed.as_seconds_f64() / ps.bytes as f64)
} else {
Duration::ZERO
};
@@ -193,10 +195,8 @@ impl Operation for RebalanceStatus {
}
if let Some(stopped_at) = stop_time {
if let Ok(du) = stopped_at.duration_since(ps.info.start_time.unwrap_or(stopped_at)) {
elapsed = du;
} else {
return Err(s3_error!(InternalError, "Failed to calculate elapsed time"));
if let Some(start_time) = ps.info.start_time {
elapsed = stopped_at - start_time;
}
eta = Duration::ZERO;
@@ -208,7 +208,7 @@ impl Operation for RebalanceStatus {
bytes: ps.bytes,
bucket: ps.bucket.clone(),
object: ps.object.clone(),
elapsed: elapsed.as_secs(),
elapsed: elapsed.whole_seconds() as u64,
eta: eta.as_secs(),
});
}
@@ -244,10 +244,45 @@ impl Operation for RebalanceStop {
.await
.map_err(|e| s3_error!(InternalError, "Failed to stop rebalance: {}", e))?;
warn!("handle RebalanceStop save_rebalance_stats done ");
if let Some(notification_sys) = get_global_notification_sys() {
notification_sys.load_rebalance_meta(true).await;
warn!("handle RebalanceStop notification_sys load_rebalance_meta");
notification_sys.load_rebalance_meta(false).await;
warn!("handle RebalanceStop notification_sys load_rebalance_meta done");
}
return Err(s3_error!(NotImplemented));
Ok(S3Response::new((StatusCode::OK, Body::empty())))
}
}
mod offsetdatetime_rfc3339 {
use serde::{self, Deserialize, Deserializer, Serializer};
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
pub fn serialize<S>(dt: &Option<OffsetDateTime>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match dt {
Some(dt) => {
let s = dt.format(&Rfc3339).map_err(serde::ser::Error::custom)?;
serializer.serialize_some(&s)
}
None => serializer.serialize_none(),
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<OffsetDateTime>, D::Error>
where
D: Deserializer<'de>,
{
let opt = Option::<String>::deserialize(deserializer)?;
match opt {
Some(s) => {
let dt = OffsetDateTime::parse(&s, &Rfc3339).map_err(serde::de::Error::custom)?;
Ok(Some(dt))
}
None => Ok(None),
}
}
}
+1 -1
View File
@@ -16,7 +16,7 @@ use matchit::Params;
use policy::policy::action::{Action, AdminAction};
use policy::policy::{Args, Policy};
use s3s::S3ErrorCode::InvalidRequest;
use s3s::{header::CONTENT_TYPE, s3_error, Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result};
use s3s::{Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result, header::CONTENT_TYPE, s3_error};
use serde::Deserialize;
use serde_urlencoded::from_bytes;
use std::collections::HashMap;
+5 -3
View File
@@ -2,14 +2,16 @@ use crate::{
admin::router::Operation,
auth::{check_key_valid, get_session_token},
};
use ecstore::utils::{crypto::base64_encode, xml};
use ecstore::bucket::utils::serialize;
use http::StatusCode;
use iam::{manager::get_token_signing_key, sys::SESSION_POLICY_NAME};
use matchit::Params;
use policy::{auth::get_new_credentials_with_metadata, policy::Policy};
use rustfs_utils::crypto::base64_encode;
use s3s::{
Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result,
dto::{AssumeRoleOutput, Credentials, Timestamp},
s3_error, Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result,
s3_error,
};
use serde::Deserialize;
use serde_json::Value;
@@ -136,7 +138,7 @@ impl Operation for AssumeRoleHandle {
};
// getAssumeRoleCredentials
let output = xml::serialize::<AssumeRoleOutput>(&resp).unwrap();
let output = serialize::<AssumeRoleOutput>(&resp).unwrap();
Ok(S3Response::new((StatusCode::OK, Body::from(output))))
}
+2 -2
View File
@@ -1,9 +1,9 @@
use ecstore::{peer_rest_client::PeerRestClient, GLOBAL_Endpoints};
use ecstore::{GLOBAL_Endpoints, peer_rest_client::PeerRestClient};
use http::StatusCode;
use hyper::Uri;
use madmin::service_commands::ServiceTraceOpts;
use matchit::Params;
use s3s::{s3_error, Body, S3Request, S3Response, S3Result};
use s3s::{Body, S3Request, S3Response, S3Result, s3_error};
use tracing::warn;
use crate::admin::router::Operation;
+2 -2
View File
@@ -5,10 +5,10 @@ use iam::get_global_action_cred;
use madmin::{AccountStatus, AddOrUpdateUserReq};
use matchit::Params;
use policy::policy::{
action::{Action, AdminAction},
Args,
action::{Action, AdminAction},
};
use s3s::{header::CONTENT_TYPE, s3_error, Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result};
use s3s::{Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result, header::CONTENT_TYPE, s3_error};
use serde::Deserialize;
use serde_urlencoded::from_bytes;
use tracing::warn;
+2 -3
View File
@@ -3,7 +3,6 @@ pub mod router;
mod rpc;
pub mod utils;
use common::error::Result;
// use ecstore::global::{is_dist_erasure, is_erasure};
use handlers::{
group, policys, pools, rebalance,
@@ -20,7 +19,7 @@ use s3s::route::S3Route;
const ADMIN_PREFIX: &str = "/rustfs/admin";
const RUSTFS_ADMIN_PREFIX: &str = "/rustfs/admin";
pub fn make_admin_route() -> Result<impl S3Route> {
pub fn make_admin_route() -> std::io::Result<impl S3Route> {
let mut r: S3Router<AdminOperation> = S3Router::new();
// 1
@@ -126,7 +125,7 @@ pub fn make_admin_route() -> Result<impl S3Route> {
Ok(r)
}
fn register_user_route(r: &mut S3Router<AdminOperation>) -> Result<()> {
fn register_user_route(r: &mut S3Router<AdminOperation>) -> std::io::Result<()> {
// 1
r.insert(
Method::GET,
+86 -9
View File
@@ -1,22 +1,93 @@
use common::error::Result;
use hyper::http::Extensions;
use base64::{Engine as _, engine::general_purpose};
use hmac::{Hmac, Mac};
use hyper::HeaderMap;
use hyper::Method;
use hyper::StatusCode;
use hyper::Uri;
use hyper::http::Extensions;
use matchit::Params;
use matchit::Router;
use s3s::header;
use s3s::route::S3Route;
use s3s::s3_error;
use s3s::Body;
use s3s::S3Request;
use s3s::S3Response;
use s3s::S3Result;
use s3s::header;
use s3s::route::S3Route;
use s3s::s3_error;
use sha2::Sha256;
use std::time::{SystemTime, UNIX_EPOCH};
use super::rpc::RPC_PREFIX;
use super::ADMIN_PREFIX;
use super::RUSTFS_ADMIN_PREFIX;
use super::rpc::RPC_PREFIX;
use iam::get_global_action_cred;
type HmacSha256 = Hmac<Sha256>;
const SIGNATURE_HEADER: &str = "x-rustfs-signature";
const TIMESTAMP_HEADER: &str = "x-rustfs-timestamp";
const SIGNATURE_VALID_DURATION: u64 = 300; // 5 minutes
/// Get the shared secret for HMAC signing
fn get_shared_secret() -> String {
if let Some(cred) = get_global_action_cred() {
cred.secret_key
} else {
// Fallback to environment variable if global credentials are not available
std::env::var("RUSTFS_RPC_SECRET").unwrap_or_else(|_| "rustfs-default-secret".to_string())
}
}
/// Generate HMAC-SHA256 signature for the given data
fn generate_signature(secret: &str, url: &str, method: &str, timestamp: u64) -> String {
let data = format!("{}|{}|{}", url, method, timestamp);
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC can take key of any size");
mac.update(data.as_bytes());
let result = mac.finalize();
general_purpose::STANDARD.encode(result.into_bytes())
}
/// Verify the request signature for RPC requests
fn verify_rpc_signature(req: &S3Request<Body>) -> S3Result<()> {
let secret = get_shared_secret();
// Get signature from header
let signature = req
.headers
.get(SIGNATURE_HEADER)
.and_then(|v| v.to_str().ok())
.ok_or_else(|| s3_error!(InvalidArgument, "Missing signature header"))?;
// Get timestamp from header
let timestamp_str = req
.headers
.get(TIMESTAMP_HEADER)
.and_then(|v| v.to_str().ok())
.ok_or_else(|| s3_error!(InvalidArgument, "Missing timestamp header"))?;
let timestamp: u64 = timestamp_str
.parse()
.map_err(|_| s3_error!(InvalidArgument, "Invalid timestamp format"))?;
// Check timestamp validity (prevent replay attacks)
let current_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
if current_time.saturating_sub(timestamp) > SIGNATURE_VALID_DURATION {
return Err(s3_error!(InvalidArgument, "Request timestamp expired"));
}
// Generate expected signature
let url = req.uri.to_string();
let method = req.method.as_str();
let expected_signature = generate_signature(&secret, &url, method, timestamp);
// Compare signatures
if signature != expected_signature {
return Err(s3_error!(AccessDenied, "Invalid signature"));
}
Ok(())
}
pub struct S3Router<T> {
router: Router<T>,
@@ -29,12 +100,12 @@ impl<T: Operation> S3Router<T> {
Self { router }
}
pub fn insert(&mut self, method: Method, path: &str, operation: T) -> Result<()> {
pub fn insert(&mut self, method: Method, path: &str, operation: T) -> std::io::Result<()> {
let path = Self::make_route_str(method, path);
// warn!("set uri {}", &path);
self.router.insert(path, operation)?;
self.router.insert(path, operation).map_err(std::io::Error::other)?;
Ok(())
}
@@ -85,10 +156,16 @@ where
// check_access before call
async fn check_access(&self, req: &mut S3Request<Body>) -> S3Result<()> {
// TODO: check access by req.credentials
// Check RPC signature verification
if req.uri.path().starts_with(RPC_PREFIX) {
// Skip signature verification for HEAD requests (health checks)
if req.method != Method::HEAD {
verify_rpc_signature(req)?;
}
return Ok(());
}
// For non-RPC admin requests, check credentials
match req.credentials {
Some(_) => Ok(()),
None => Err(s3_error!(AccessDenied, "Signature is required")),
+90 -12
View File
@@ -2,11 +2,11 @@ use super::router::AdminOperation;
use super::router::Operation;
use super::router::S3Router;
use crate::storage::ecfs::bytes_stream;
use common::error::Result;
use ecstore::disk::DiskAPI;
use ecstore::io::READ_BUFFER_SIZE;
use ecstore::disk::WalkDirOptions;
use ecstore::set_disk::DEFAULT_READ_BUFFER_SIZE;
use ecstore::store::find_local_disk;
use futures::TryStreamExt;
use futures::StreamExt;
use http::StatusCode;
use hyper::Method;
use matchit::Params;
@@ -17,24 +17,43 @@ use s3s::S3Request;
use s3s::S3Response;
use s3s::S3Result;
use serde_urlencoded::from_bytes;
use tokio::io::AsyncWriteExt;
use tokio_util::io::ReaderStream;
use tokio_util::io::StreamReader;
use tracing::warn;
pub const RPC_PREFIX: &str = "/rustfs/rpc";
pub fn register_rpc_route(r: &mut S3Router<AdminOperation>) -> Result<()> {
pub fn regist_rpc_route(r: &mut S3Router<AdminOperation>) -> std::io::Result<()> {
r.insert(
Method::GET,
format!("{}{}", RPC_PREFIX, "/read_file_stream").as_str(),
AdminOperation(&ReadFile {}),
)?;
r.insert(
Method::HEAD,
format!("{}{}", RPC_PREFIX, "/read_file_stream").as_str(),
AdminOperation(&ReadFile {}),
)?;
r.insert(
Method::PUT,
format!("{}{}", RPC_PREFIX, "/put_file_stream").as_str(),
AdminOperation(&PutFile {}),
)?;
r.insert(
Method::GET,
format!("{}{}", RPC_PREFIX, "/walk_dir").as_str(),
AdminOperation(&WalkDir {}),
)?;
r.insert(
Method::HEAD,
format!("{}{}", RPC_PREFIX, "/walk_dir").as_str(),
AdminOperation(&WalkDir {}),
)?;
Ok(())
}
@@ -51,6 +70,9 @@ pub struct ReadFile {}
#[async_trait::async_trait]
impl Operation for ReadFile {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
if req.method == Method::HEAD {
return Ok(S3Response::new((StatusCode::OK, Body::empty())));
}
let query = {
if let Some(query) = req.uri.query() {
let input: ReadFileQuery =
@@ -73,13 +95,68 @@ impl Operation for ReadFile {
Ok(S3Response::new((
StatusCode::OK,
Body::from(StreamingBlob::wrap(bytes_stream(
ReaderStream::with_capacity(file, READ_BUFFER_SIZE),
ReaderStream::with_capacity(file, DEFAULT_READ_BUFFER_SIZE),
query.length,
))),
)))
}
}
#[derive(Debug, Default, serde::Deserialize)]
pub struct WalkDirQuery {
disk: String,
}
pub struct WalkDir {}
#[async_trait::async_trait]
impl Operation for WalkDir {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
if req.method == Method::HEAD {
return Ok(S3Response::new((StatusCode::OK, Body::empty())));
}
let query = {
if let Some(query) = req.uri.query() {
let input: WalkDirQuery =
from_bytes(query.as_bytes()).map_err(|e| s3_error!(InvalidArgument, "get query failed1 {:?}", e))?;
input
} else {
WalkDirQuery::default()
}
};
let mut input = req.input;
let body = match input.store_all_unlimited().await {
Ok(b) => b,
Err(e) => {
warn!("get body failed, e: {:?}", e);
return Err(s3_error!(InvalidRequest, "get body failed"));
}
};
// let body_bytes = decrypt_data(input_cred.secret_key.expose().as_bytes(), &body)
// .map_err(|e| S3Error::with_message(S3ErrorCode::InvalidArgument, format!("decrypt_data err {}", e)))?;
let args: WalkDirOptions =
serde_json::from_slice(&body).map_err(|e| s3_error!(InternalError, "unmarshal body err {}", e))?;
let Some(disk) = find_local_disk(&query.disk).await else {
return Err(s3_error!(InvalidArgument, "disk not found"));
};
let (rd, mut wd) = tokio::io::duplex(DEFAULT_READ_BUFFER_SIZE);
tokio::spawn(async move {
if let Err(e) = disk.walk_dir(args, &mut wd).await {
warn!("walk dir err {}", e);
}
});
let body = Body::from(StreamingBlob::wrap(ReaderStream::with_capacity(rd, DEFAULT_READ_BUFFER_SIZE)));
Ok(S3Response::new((StatusCode::OK, body)))
}
}
// /rustfs/rpc/read_file_stream?disk={}&volume={}&path={}&offset={}&length={}"
#[derive(Debug, Default, serde::Deserialize)]
pub struct PutFileQuery {
@@ -87,7 +164,7 @@ pub struct PutFileQuery {
volume: String,
path: String,
append: bool,
size: usize,
size: i64,
}
pub struct PutFile {}
#[async_trait::async_trait]
@@ -117,11 +194,12 @@ impl Operation for PutFile {
.map_err(|e| s3_error!(InternalError, "read file err {}", e))?
};
let mut body = StreamReader::new(req.input.into_stream().map_err(std::io::Error::other));
tokio::io::copy(&mut body, &mut file)
.await
.map_err(|e| s3_error!(InternalError, "copy err {}", e))?;
let mut body = req.input;
while let Some(item) = body.next().await {
let bytes = item.map_err(|e| s3_error!(InternalError, "body stream err {}", e))?;
let result = file.write_all(&bytes).await;
result.map_err(|e| s3_error!(InternalError, "write file err {}", e))?;
}
Ok(S3Response::new((StatusCode::OK, Body::empty())))
}
+3 -3
View File
@@ -7,13 +7,13 @@ use iam::get_global_action_cred;
use iam::sys::SESSION_POLICY_NAME;
use policy::auth;
use policy::auth::get_claims_from_token_with_secret;
use s3s::S3Error;
use s3s::S3ErrorCode;
use s3s::S3Result;
use s3s::auth::S3Auth;
use s3s::auth::SecretKey;
use s3s::auth::SimpleAuth;
use s3s::s3_error;
use s3s::S3Error;
use s3s::S3ErrorCode;
use s3s::S3Result;
use serde_json::Value;
pub struct IAMAuth {
+4 -4
View File
@@ -1,18 +1,19 @@
use crate::license::get_license;
use axum::{
Router,
body::Body,
http::{Response, StatusCode},
response::IntoResponse,
routing::get,
Router,
};
use axum_extra::extract::Host;
use rustfs_config::{RUSTFS_TLS_CERT, RUSTFS_TLS_KEY};
use rustfs_utils::net::parse_and_resolve_address;
use std::io;
use axum::response::Redirect;
use axum_server::tls_rustls::RustlsConfig;
use http::{header, Uri};
use http::{Uri, header};
use mime_guess::from_path;
use rust_embed::RustEmbed;
use serde::Serialize;
@@ -239,8 +240,7 @@ pub async fn start_static_file_server(
.layer(tower_http::compression::CompressionLayer::new().gzip(true).deflate(true))
.layer(TraceLayer::new_for_http());
use ecstore::utils::net;
let server_addr = net::parse_and_resolve_address(addrs).expect("Failed to parse socket address");
let server_addr = parse_and_resolve_address(addrs).expect("Failed to parse socket address");
let server_port = server_addr.port();
let server_address = server_addr.to_string();
+331
View File
@@ -0,0 +1,331 @@
use ecstore::error::StorageError;
use s3s::{S3Error, S3ErrorCode};
#[derive(Debug)]
pub struct ApiError {
pub code: S3ErrorCode,
pub message: String,
pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
}
impl std::fmt::Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for ApiError {}
impl ApiError {
pub fn other<E>(error: E) -> Self
where
E: std::fmt::Display + Into<Box<dyn std::error::Error + Send + Sync>>,
{
ApiError {
code: S3ErrorCode::InternalError,
message: error.to_string(),
source: Some(error.into()),
}
}
}
impl From<ApiError> for S3Error {
fn from(err: ApiError) -> Self {
let mut s3e = S3Error::with_message(err.code, err.message);
if let Some(source) = err.source {
s3e.set_source(source);
}
s3e
}
}
impl From<StorageError> for ApiError {
fn from(err: StorageError) -> Self {
let code = match &err {
StorageError::NotImplemented => S3ErrorCode::NotImplemented,
StorageError::InvalidArgument(_, _, _) => S3ErrorCode::InvalidArgument,
StorageError::MethodNotAllowed => S3ErrorCode::MethodNotAllowed,
StorageError::BucketNotFound(_) => S3ErrorCode::NoSuchBucket,
StorageError::BucketNotEmpty(_) => S3ErrorCode::BucketNotEmpty,
StorageError::BucketNameInvalid(_) => S3ErrorCode::InvalidBucketName,
StorageError::ObjectNameInvalid(_, _) => S3ErrorCode::InvalidArgument,
StorageError::BucketExists(_) => S3ErrorCode::BucketAlreadyExists,
StorageError::StorageFull => S3ErrorCode::ServiceUnavailable,
StorageError::SlowDown => S3ErrorCode::SlowDown,
StorageError::PrefixAccessDenied(_, _) => S3ErrorCode::AccessDenied,
StorageError::InvalidUploadIDKeyCombination(_, _) => S3ErrorCode::InvalidArgument,
StorageError::ObjectNameTooLong(_, _) => S3ErrorCode::InvalidArgument,
StorageError::ObjectNamePrefixAsSlash(_, _) => S3ErrorCode::InvalidArgument,
StorageError::ObjectNotFound(_, _) => S3ErrorCode::NoSuchKey,
StorageError::ConfigNotFound => S3ErrorCode::NoSuchKey,
StorageError::VolumeNotFound => S3ErrorCode::NoSuchBucket,
StorageError::FileNotFound => S3ErrorCode::NoSuchKey,
StorageError::FileVersionNotFound => S3ErrorCode::NoSuchVersion,
StorageError::VersionNotFound(_, _, _) => S3ErrorCode::NoSuchVersion,
StorageError::InvalidUploadID(_, _, _) => S3ErrorCode::InvalidPart,
StorageError::InvalidVersionID(_, _, _) => S3ErrorCode::InvalidArgument,
StorageError::DataMovementOverwriteErr(_, _, _) => S3ErrorCode::InvalidArgument,
StorageError::ObjectExistsAsDirectory(_, _) => S3ErrorCode::InvalidArgument,
StorageError::InvalidPart(_, _, _) => S3ErrorCode::InvalidPart,
_ => S3ErrorCode::InternalError,
};
ApiError {
code,
message: err.to_string(),
source: Some(Box::new(err)),
}
}
}
impl From<std::io::Error> for ApiError {
fn from(err: std::io::Error) -> Self {
ApiError {
code: S3ErrorCode::InternalError,
message: err.to_string(),
source: Some(Box::new(err)),
}
}
}
impl From<iam::error::Error> for ApiError {
fn from(err: iam::error::Error) -> Self {
let serr: StorageError = err.into();
serr.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
use s3s::{S3Error, S3ErrorCode};
use std::io::{Error as IoError, ErrorKind};
#[test]
fn test_api_error_from_io_error() {
let io_error = IoError::new(ErrorKind::PermissionDenied, "permission denied");
let api_error: ApiError = io_error.into();
assert_eq!(api_error.code, S3ErrorCode::InternalError);
assert!(api_error.message.contains("permission denied"));
assert!(api_error.source.is_some());
}
#[test]
fn test_api_error_from_io_error_different_kinds() {
let test_cases = vec![
(ErrorKind::NotFound, "not found"),
(ErrorKind::InvalidInput, "invalid input"),
(ErrorKind::TimedOut, "timed out"),
(ErrorKind::WriteZero, "write zero"),
(ErrorKind::Other, "other error"),
];
for (kind, message) in test_cases {
let io_error = IoError::new(kind, message);
let api_error: ApiError = io_error.into();
assert_eq!(api_error.code, S3ErrorCode::InternalError);
assert!(api_error.message.contains(message));
assert!(api_error.source.is_some());
// Test that source can be downcast back to io::Error
let source = api_error.source.as_ref().unwrap();
let downcast_io_error = source.downcast_ref::<IoError>();
assert!(downcast_io_error.is_some());
assert_eq!(downcast_io_error.unwrap().kind(), kind);
}
}
#[test]
fn test_api_error_other_function() {
let custom_error = "Custom API error";
let api_error = ApiError::other(custom_error);
assert_eq!(api_error.code, S3ErrorCode::InternalError);
assert_eq!(api_error.message, custom_error);
assert!(api_error.source.is_some());
}
#[test]
fn test_api_error_other_function_with_complex_error() {
let io_error = IoError::new(ErrorKind::InvalidData, "complex error");
let api_error = ApiError::other(io_error);
assert_eq!(api_error.code, S3ErrorCode::InternalError);
assert!(api_error.message.contains("complex error"));
assert!(api_error.source.is_some());
// Test that source can be downcast back to io::Error
let source = api_error.source.as_ref().unwrap();
let downcast_io_error = source.downcast_ref::<IoError>();
assert!(downcast_io_error.is_some());
assert_eq!(downcast_io_error.unwrap().kind(), ErrorKind::InvalidData);
}
#[test]
fn test_api_error_from_storage_error() {
let storage_error = StorageError::BucketNotFound("test-bucket".to_string());
let api_error: ApiError = storage_error.into();
assert_eq!(api_error.code, S3ErrorCode::NoSuchBucket);
assert!(api_error.message.contains("test-bucket"));
assert!(api_error.source.is_some());
// Test that source can be downcast back to StorageError
let source = api_error.source.as_ref().unwrap();
let downcast_storage_error = source.downcast_ref::<StorageError>();
assert!(downcast_storage_error.is_some());
}
#[test]
fn test_api_error_from_storage_error_mappings() {
let test_cases = vec![
(StorageError::NotImplemented, S3ErrorCode::NotImplemented),
(
StorageError::InvalidArgument("test".into(), "test".into(), "test".into()),
S3ErrorCode::InvalidArgument,
),
(StorageError::MethodNotAllowed, S3ErrorCode::MethodNotAllowed),
(StorageError::BucketNotFound("test".into()), S3ErrorCode::NoSuchBucket),
(StorageError::BucketNotEmpty("test".into()), S3ErrorCode::BucketNotEmpty),
(StorageError::BucketNameInvalid("test".into()), S3ErrorCode::InvalidBucketName),
(
StorageError::ObjectNameInvalid("test".into(), "test".into()),
S3ErrorCode::InvalidArgument,
),
(StorageError::BucketExists("test".into()), S3ErrorCode::BucketAlreadyExists),
(StorageError::StorageFull, S3ErrorCode::ServiceUnavailable),
(StorageError::SlowDown, S3ErrorCode::SlowDown),
(StorageError::PrefixAccessDenied("test".into(), "test".into()), S3ErrorCode::AccessDenied),
(StorageError::ObjectNotFound("test".into(), "test".into()), S3ErrorCode::NoSuchKey),
(StorageError::ConfigNotFound, S3ErrorCode::NoSuchKey),
(StorageError::VolumeNotFound, S3ErrorCode::NoSuchBucket),
(StorageError::FileNotFound, S3ErrorCode::NoSuchKey),
(StorageError::FileVersionNotFound, S3ErrorCode::NoSuchVersion),
];
for (storage_error, expected_code) in test_cases {
let api_error: ApiError = storage_error.into();
assert_eq!(api_error.code, expected_code);
assert!(api_error.source.is_some());
}
}
#[test]
fn test_api_error_from_iam_error() {
let iam_error = iam::error::Error::other("IAM test error");
let api_error: ApiError = iam_error.into();
// IAM error is first converted to StorageError, then to ApiError
assert!(api_error.source.is_some());
assert!(api_error.message.contains("test error"));
}
#[test]
fn test_api_error_to_s3_error() {
let api_error = ApiError {
code: S3ErrorCode::NoSuchBucket,
message: "Bucket not found".to_string(),
source: Some(Box::new(IoError::new(ErrorKind::NotFound, "not found"))),
};
let s3_error: S3Error = api_error.into();
assert_eq!(*s3_error.code(), S3ErrorCode::NoSuchBucket);
assert!(s3_error.message().unwrap_or("").contains("Bucket not found"));
assert!(s3_error.source().is_some());
}
#[test]
fn test_api_error_to_s3_error_without_source() {
let api_error = ApiError {
code: S3ErrorCode::InvalidArgument,
message: "Invalid argument".to_string(),
source: None,
};
let s3_error: S3Error = api_error.into();
assert_eq!(*s3_error.code(), S3ErrorCode::InvalidArgument);
assert!(s3_error.message().unwrap_or("").contains("Invalid argument"));
}
#[test]
fn test_api_error_display() {
let api_error = ApiError {
code: S3ErrorCode::InternalError,
message: "Test error message".to_string(),
source: None,
};
assert_eq!(api_error.to_string(), "Test error message");
}
#[test]
fn test_api_error_debug() {
let api_error = ApiError {
code: S3ErrorCode::NoSuchKey,
message: "Object not found".to_string(),
source: Some(Box::new(IoError::new(ErrorKind::NotFound, "file not found"))),
};
let debug_str = format!("{:?}", api_error);
assert!(debug_str.contains("NoSuchKey"));
assert!(debug_str.contains("Object not found"));
}
#[test]
fn test_api_error_roundtrip_through_io_error() {
let original_io_error = IoError::new(ErrorKind::PermissionDenied, "original permission error");
// Convert to ApiError
let api_error: ApiError = original_io_error.into();
// Verify the conversion preserved the information
assert_eq!(api_error.code, S3ErrorCode::InternalError);
assert!(api_error.message.contains("original permission error"));
assert!(api_error.source.is_some());
// Test that we can downcast back to the original io::Error
let source = api_error.source.as_ref().unwrap();
let downcast_io_error = source.downcast_ref::<IoError>();
assert!(downcast_io_error.is_some());
assert_eq!(downcast_io_error.unwrap().kind(), ErrorKind::PermissionDenied);
assert!(downcast_io_error.unwrap().to_string().contains("original permission error"));
}
#[test]
fn test_api_error_chain_conversion() {
// Start with an io::Error
let io_error = IoError::new(ErrorKind::InvalidData, "invalid data");
// Convert to StorageError (simulating what happens in the codebase)
let storage_error = StorageError::other(io_error);
// Convert to ApiError
let api_error: ApiError = storage_error.into();
// Verify the chain is preserved
assert!(api_error.source.is_some());
// Check that we can still access the original error information
let source = api_error.source.as_ref().unwrap();
let downcast_storage_error = source.downcast_ref::<StorageError>();
assert!(downcast_storage_error.is_some());
}
#[test]
fn test_api_error_error_trait_implementation() {
let api_error = ApiError {
code: S3ErrorCode::InternalError,
message: "Test error".to_string(),
source: Some(Box::new(IoError::other("source error"))),
};
// Test that it implements std::error::Error
let error: &dyn std::error::Error = &api_error;
assert_eq!(error.to_string(), "Test error");
// ApiError doesn't implement Error::source() properly, so this would be None
// This is expected because ApiError is not a typical Error implementation
assert!(error.source().is_none());
}
}
+163 -356
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -1,5 +1,5 @@
use appauth::token::Token;
use common::error::{Error, Result};
use std::io::{Error, Result};
use std::sync::OnceLock;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
@@ -37,7 +37,7 @@ pub fn license_check() -> Result<()> {
let invalid_license = LICENSE.get().map(|token| {
if token.expired < SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() {
error!("License expired");
return Err(Error::from_string("Incorrect license, please contact RustFS.".to_string()));
return Err(Error::other("Incorrect license, please contact RustFS."));
}
info!("License is valid ! expired at {}", token.expired);
Ok(())
@@ -46,12 +46,12 @@ pub fn license_check() -> Result<()> {
// let invalid_license = config::get_config().license.as_ref().map(|license| {
// if license.is_empty() {
// error!("License is empty");
// return Err(Error::from_string("Incorrect license, please contact RustFS.".to_string()));
// return Err(Error::other("Incorrect license, please contact RustFS.".to_string()));
// }
// let token = appauth::token::parse_license(license)?;
// if token.expired < SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() {
// error!("License expired");
// return Err(Error::from_string("Incorrect license, please contact RustFS.".to_string()));
// return Err(Error::other("Incorrect license, please contact RustFS.".to_string()));
// }
// info!("License is valid ! expired at {}", token.expired);
@@ -59,7 +59,7 @@ pub fn license_check() -> Result<()> {
// });
if invalid_license.is_none() || invalid_license.is_some_and(|v| v.is_err()) {
return Err(Error::from_string("Incorrect license, please contact RustFS.".to_string()));
return Err(Error::other("Incorrect license, please contact RustFS."));
}
Ok(())
+20 -18
View File
@@ -2,6 +2,7 @@ mod admin;
mod auth;
mod config;
mod console;
mod error;
mod event;
mod grpc;
pub mod license;
@@ -18,7 +19,7 @@ use bytes::Bytes;
use chrono::Datelike;
use clap::Parser;
use common::{
error::{Error, Result},
// error::{Error, Result},
globals::set_global_addr,
};
use ecstore::bucket::metadata_sys::init_bucket_metadata_sys;
@@ -27,7 +28,6 @@ use ecstore::config as ecconfig;
use ecstore::config::GLOBAL_ConfigSys;
use ecstore::heal::background_heal_ops::init_auto_heal;
use ecstore::store_api::BucketOptions;
use ecstore::utils::net;
use ecstore::StorageAPI;
use ecstore::{
endpoints::EndpointServerPools,
@@ -50,10 +50,12 @@ use license::init_license;
use protos::proto_gen::node_service::node_service_server::NodeServiceServer;
use rustfs_config::{DEFAULT_ACCESS_KEY, DEFAULT_SECRET_KEY, RUSTFS_TLS_CERT, RUSTFS_TLS_KEY};
use rustfs_obs::{init_obs, set_global_guard, SystemObserver};
use rustfs_utils::net::parse_and_resolve_address;
use rustls::ServerConfig;
use s3s::{host::MultiDomain, service::S3ServiceBuilder};
use service::hybrid;
use socket2::SockRef;
use std::io::{Error, Result};
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
@@ -73,7 +75,7 @@ const MI_B: usize = 1024 * 1024;
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
#[allow(clippy::result_large_err)]
fn check_auth(req: Request<()>) -> Result<Request<()>, Status> {
fn check_auth(req: Request<()>) -> std::result::Result<Request<()>, Status> {
let token: MetadataValue<_> = "rustfs rpc".parse().unwrap();
match req.metadata().get("authorization") {
@@ -107,7 +109,7 @@ async fn main() -> Result<()> {
let (_logger, guard) = init_obs(Some(opt.clone().obs_endpoint)).await;
// Store in global storage
set_global_guard(guard)?;
set_global_guard(guard).map_err(Error::other)?;
// Run parameters
run(opt).await
@@ -120,7 +122,7 @@ async fn run(opt: config::Opt) -> Result<()> {
// Initialize event notifier
// event::init_event_notifier(opt.event_config).await;
let server_addr = net::parse_and_resolve_address(opt.address.as_str())?;
let server_addr = parse_and_resolve_address(opt.address.as_str()).map_err(Error::other)?;
let server_port = server_addr.port();
let server_address = server_addr.to_string();
@@ -139,8 +141,8 @@ async fn run(opt: config::Opt) -> Result<()> {
let local_ip = rustfs_utils::get_local_ip().ok_or(local_addr.ip()).unwrap();
// For RPC
let (endpoint_pools, setup_type) = EndpointServerPools::from_volumes(server_address.clone().as_str(), opt.volumes.clone())
.map_err(|err| Error::from_string(err.to_string()))?;
let (endpoint_pools, setup_type) =
EndpointServerPools::from_volumes(server_address.clone().as_str(), opt.volumes.clone()).map_err(Error::other)?;
// Print RustFS-style logging for pool formatting
for (i, eps) in endpoint_pools.as_ref().iter().enumerate() {
@@ -164,7 +166,10 @@ async fn run(opt: config::Opt) -> Result<()> {
info!(" RootUser: {}", opt.access_key.clone());
info!(" RootPass: {}", opt.secret_key.clone());
if DEFAULT_ACCESS_KEY.eq(&opt.access_key) && DEFAULT_SECRET_KEY.eq(&opt.secret_key) {
warn!("Detected default credentials '{}:{}', we recommend that you change these values with 'RUSTFS_ACCESS_KEY' and 'RUSTFS_SECRET_KEY' environment variables", DEFAULT_ACCESS_KEY, DEFAULT_SECRET_KEY);
warn!(
"Detected default credentials '{}:{}', we recommend that you change these values with 'RUSTFS_ACCESS_KEY' and 'RUSTFS_SECRET_KEY' environment variables",
DEFAULT_ACCESS_KEY, DEFAULT_SECRET_KEY
);
}
for (i, eps) in endpoint_pools.as_ref().iter().enumerate() {
@@ -184,9 +189,7 @@ async fn run(opt: config::Opt) -> Result<()> {
update_erasure_type(setup_type).await;
// Initialize the local disk
init_local_disks(endpoint_pools.clone())
.await
.map_err(|err| Error::from_string(err.to_string()))?;
init_local_disks(endpoint_pools.clone()).await.map_err(Error::other)?;
// Setup S3 service
// This project uses the S3S library to implement S3 services
@@ -208,7 +211,7 @@ async fn run(opt: config::Opt) -> Result<()> {
if !opt.server_domains.is_empty() {
info!("virtual-hosted-style requests are enabled use domain_name {:?}", &opt.server_domains);
b.set_host(MultiDomain::new(&opt.server_domains)?);
b.set_host(MultiDomain::new(&opt.server_domains).map_err(Error::other)?);
}
// // Enable parsing virtual-hosted-style requests
@@ -501,9 +504,8 @@ async fn run(opt: config::Opt) -> Result<()> {
// init store
let store = ECStore::new(server_addr.clone(), endpoint_pools.clone())
.await
.map_err(|err| {
error!("ECStore::new {:?}", &err);
Error::from_string(err.to_string())
.inspect_err(|err| {
error!("ECStore::new {:?}", err);
})?;
ecconfig::init();
@@ -522,7 +524,7 @@ async fn run(opt: config::Opt) -> Result<()> {
..Default::default()
})
.await
.map_err(|err| Error::from_string(err.to_string()))?;
.map_err(Error::other)?;
let buckets = buckets_list.into_iter().map(|v| v.name).collect();
@@ -532,7 +534,7 @@ async fn run(opt: config::Opt) -> Result<()> {
new_global_notification_sys(endpoint_pools.clone()).await.map_err(|err| {
error!("new_global_notification_sys failed {:?}", &err);
Error::from_string(err.to_string())
Error::other(err)
})?;
// init scanner
@@ -558,7 +560,7 @@ async fn run(opt: config::Opt) -> Result<()> {
if console_address.is_empty() {
error!("console_address is empty");
return Err(Error::from_string("console_address is empty".to_string()));
return Err(Error::other("console_address is empty".to_string()));
}
tokio::spawn(async move {
+2 -2
View File
@@ -1,6 +1,6 @@
mod service_state;
pub(crate) use service_state::wait_for_shutdown;
pub(crate) use service_state::SHUTDOWN_TIMEOUT;
pub(crate) use service_state::ServiceState;
pub(crate) use service_state::ServiceStateManager;
pub(crate) use service_state::ShutdownSignal;
pub(crate) use service_state::SHUTDOWN_TIMEOUT;
pub(crate) use service_state::wait_for_shutdown;
+3 -3
View File
@@ -1,8 +1,8 @@
use atomic_enum::atomic_enum;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::time::Duration;
use tokio::signal::unix::{signal, SignalKind};
use tokio::signal::unix::{SignalKind, signal};
use tracing::info;
// a configurable shutdown timeout
@@ -10,7 +10,7 @@ pub(crate) const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(1);
#[cfg(target_os = "linux")]
fn notify_systemd(state: &str) {
use libsystemd::daemon::{notify, NotifyState};
use libsystemd::daemon::{NotifyState, notify};
use tracing::{debug, error};
let notify_state = match state {
"ready" => NotifyState::Ready,
+4 -6
View File
@@ -7,7 +7,7 @@ use policy::auth;
use policy::policy::action::{Action, S3Action};
use policy::policy::{Args, BucketPolicyArgs};
use s3s::access::{S3Access, S3AccessContext};
use s3s::{dto::*, s3_error, S3Error, S3ErrorCode, S3Request, S3Result};
use s3s::{S3Error, S3ErrorCode, S3Request, S3Result, dto::*, s3_error};
use std::collections::HashMap;
#[allow(dead_code)]
@@ -218,11 +218,9 @@ impl S3Access for FS {
let req_info = req.extensions.get_mut::<ReqInfo>().expect("ReqInfo not found");
let (src_bucket, src_key, version_id) = match &req.input.copy_source {
CopySource::AccessPoint { .. } => return Err(s3_error!(NotImplemented)),
CopySource::Bucket {
ref bucket,
ref key,
version_id,
} => (bucket.to_string(), key.to_string(), version_id.as_ref().map(|v| v.to_string())),
CopySource::Bucket { bucket, key, version_id } => {
(bucket.to_string(), key.to_string(), version_id.as_ref().map(|v| v.to_string()))
}
};
req_info.bucket = Some(src_bucket);
+243 -170
View File
File diff suppressed because it is too large Load Diff
+26 -39
View File
@@ -1,6 +1,6 @@
use common::error::Error;
use ecstore::{bucket::error::BucketMetadataError, disk::error::is_err_file_not_found, store_err::StorageError};
use s3s::{s3_error, S3Error, S3ErrorCode};
use ecstore::error::StorageError;
use s3s::{S3Error, S3ErrorCode, s3_error};
pub fn to_s3_error(err: Error) -> S3Error {
if let Some(storage_err) = err.downcast_ref::<StorageError>() {
return match storage_err {
@@ -56,18 +56,6 @@ pub fn to_s3_error(err: Error) -> S3Error {
StorageError::ObjectExistsAsDirectory(bucket, object) => {
s3_error!(InvalidArgument, "Object exists on :{} as directory {}", bucket, object)
}
StorageError::InsufficientReadQuorum => {
s3_error!(SlowDown, "Storage resources are insufficient for the read operation")
}
StorageError::InsufficientWriteQuorum => {
s3_error!(SlowDown, "Storage resources are insufficient for the write operation")
}
StorageError::DecommissionNotStarted => s3_error!(InvalidArgument, "Decommission Not Started"),
StorageError::DecommissionAlreadyRunning => s3_error!(InternalError, "Decommission already running"),
StorageError::VolumeNotFound(bucket) => {
s3_error!(NoSuchBucket, "bucket not found {}", bucket)
}
StorageError::InvalidPart(bucket, object, version_id) => {
s3_error!(
InvalidPart,
@@ -80,15 +68,6 @@ pub fn to_s3_error(err: Error) -> S3Error {
StorageError::DoneForNow => s3_error!(InternalError, "DoneForNow"),
};
}
//需要添加 not found bucket replication config
if let Some(meta_err) = err.downcast_ref::<BucketMetadataError>() {
return match meta_err {
BucketMetadataError::BucketReplicationConfigNotFound => {
S3Error::with_message(S3ErrorCode::ReplicationConfigurationNotFoundError, format!("{}", err))
}
_ => S3Error::with_message(S3ErrorCode::InternalError, format!("{}", err)), // 处理其他情况
};
}
if is_err_file_not_found(&err) {
return S3Error::with_message(S3ErrorCode::NoSuchKey, format!(" ec err {}", err));
@@ -196,10 +175,12 @@ mod tests {
let s3_err = to_s3_error(err);
assert_eq!(*s3_err.code(), S3ErrorCode::ServiceUnavailable);
assert!(s3_err
.message()
.unwrap()
.contains("Storage reached its minimum free drive threshold"));
assert!(
s3_err
.message()
.unwrap()
.contains("Storage reached its minimum free drive threshold")
);
}
#[test]
@@ -266,10 +247,12 @@ mod tests {
let s3_err = to_s3_error(err);
assert_eq!(*s3_err.code(), S3ErrorCode::InvalidArgument);
assert!(s3_err
.message()
.unwrap()
.contains("Object name contains forward slash as prefix"));
assert!(
s3_err
.message()
.unwrap()
.contains("Object name contains forward slash as prefix")
);
assert!(s3_err.message().unwrap().contains("test-bucket"));
assert!(s3_err.message().unwrap().contains("/invalid-object"));
}
@@ -367,10 +350,12 @@ mod tests {
let s3_err = to_s3_error(err);
assert_eq!(*s3_err.code(), S3ErrorCode::SlowDown);
assert!(s3_err
.message()
.unwrap()
.contains("Storage resources are insufficient for the read operation"));
assert!(
s3_err
.message()
.unwrap()
.contains("Storage resources are insufficient for the read operation")
);
}
#[test]
@@ -380,10 +365,12 @@ mod tests {
let s3_err = to_s3_error(err);
assert_eq!(*s3_err.code(), S3ErrorCode::SlowDown);
assert!(s3_err
.message()
.unwrap()
.contains("Storage resources are insufficient for the write operation"));
assert!(
s3_err
.message()
.unwrap()
.contains("Storage resources are insufficient for the write operation")
);
}
#[test]
+1 -1
View File
@@ -1,5 +1,5 @@
pub mod access;
pub mod ecfs;
pub mod error;
// pub mod error;
mod event;
pub mod options;
+30 -60
View File
@@ -1,10 +1,10 @@
use common::error::{Error, Result};
use ecstore::bucket::versioning_sys::BucketVersioningSys;
use ecstore::error::Result;
use ecstore::error::StorageError;
use ecstore::store_api::ObjectOptions;
use ecstore::store_err::StorageError;
use ecstore::utils::path::is_dir_object;
use http::{HeaderMap, HeaderValue};
use lazy_static::lazy_static;
use rustfs_utils::path::is_dir_object;
use std::collections::HashMap;
use uuid::Uuid;
@@ -25,24 +25,16 @@ pub async fn del_opts(
if let Some(ref id) = vid {
if let Err(_err) = Uuid::parse_str(id.as_str()) {
return Err(Error::new(StorageError::InvalidVersionID(
bucket.to_owned(),
object.to_owned(),
id.clone(),
)));
return Err(StorageError::InvalidVersionID(bucket.to_owned(), object.to_owned(), id.clone()));
}
if !versioned {
return Err(Error::new(StorageError::InvalidArgument(
bucket.to_owned(),
object.to_owned(),
id.clone(),
)));
return Err(StorageError::InvalidArgument(bucket.to_owned(), object.to_owned(), id.clone()));
}
}
let mut opts = put_opts_from_headers(headers, metadata)
.map_err(|err| Error::new(StorageError::InvalidArgument(bucket.to_owned(), object.to_owned(), err.to_string())))?;
.map_err(|err| StorageError::InvalidArgument(bucket.to_owned(), object.to_owned(), err.to_string()))?;
opts.version_id = {
if is_dir_object(object) && vid.is_none() {
@@ -72,24 +64,16 @@ pub async fn get_opts(
if let Some(ref id) = vid {
if let Err(_err) = Uuid::parse_str(id.as_str()) {
return Err(Error::new(StorageError::InvalidVersionID(
bucket.to_owned(),
object.to_owned(),
id.clone(),
)));
return Err(StorageError::InvalidVersionID(bucket.to_owned(), object.to_owned(), id.clone()));
}
if !versioned {
return Err(Error::new(StorageError::InvalidArgument(
bucket.to_owned(),
object.to_owned(),
id.clone(),
)));
return Err(StorageError::InvalidArgument(bucket.to_owned(), object.to_owned(), id.clone()));
}
}
let mut opts = get_default_opts(headers, None, false)
.map_err(|err| Error::new(StorageError::InvalidArgument(bucket.to_owned(), object.to_owned(), err.to_string())))?;
.map_err(|err| StorageError::InvalidArgument(bucket.to_owned(), object.to_owned(), err.to_string()))?;
opts.version_id = {
if is_dir_object(object) && vid.is_none() {
@@ -122,24 +106,16 @@ pub async fn put_opts(
if let Some(ref id) = vid {
if let Err(_err) = Uuid::parse_str(id.as_str()) {
return Err(Error::new(StorageError::InvalidVersionID(
bucket.to_owned(),
object.to_owned(),
id.clone(),
)));
return Err(StorageError::InvalidVersionID(bucket.to_owned(), object.to_owned(), id.clone()));
}
if !versioned {
return Err(Error::new(StorageError::InvalidArgument(
bucket.to_owned(),
object.to_owned(),
id.clone(),
)));
return Err(StorageError::InvalidArgument(bucket.to_owned(), object.to_owned(), id.clone()));
}
}
let mut opts = put_opts_from_headers(headers, metadata)
.map_err(|err| Error::new(StorageError::InvalidArgument(bucket.to_owned(), object.to_owned(), err.to_string())))?;
.map_err(|err| StorageError::InvalidArgument(bucket.to_owned(), object.to_owned(), err.to_string()))?;
opts.version_id = {
if is_dir_object(object) && vid.is_none() {
@@ -317,15 +293,13 @@ mod tests {
assert!(result.is_err());
if let Err(err) = result {
if let Some(storage_err) = err.downcast_ref::<StorageError>() {
match storage_err {
StorageError::InvalidVersionID(bucket, object, version) => {
assert_eq!(bucket, "test-bucket");
assert_eq!(object, "test-object");
assert_eq!(version, "invalid-uuid");
}
_ => panic!("Expected InvalidVersionID error"),
match err {
StorageError::InvalidVersionID(bucket, object, version) => {
assert_eq!(bucket, "test-bucket");
assert_eq!(object, "test-object");
assert_eq!(version, "invalid-uuid");
}
_ => panic!("Expected InvalidVersionID error"),
}
}
}
@@ -373,15 +347,13 @@ mod tests {
assert!(result.is_err());
if let Err(err) = result {
if let Some(storage_err) = err.downcast_ref::<StorageError>() {
match storage_err {
StorageError::InvalidVersionID(bucket, object, version) => {
assert_eq!(bucket, "test-bucket");
assert_eq!(object, "test-object");
assert_eq!(version, "invalid-uuid");
}
_ => panic!("Expected InvalidVersionID error"),
match err {
StorageError::InvalidVersionID(bucket, object, version) => {
assert_eq!(bucket, "test-bucket");
assert_eq!(object, "test-object");
assert_eq!(version, "invalid-uuid");
}
_ => panic!("Expected InvalidVersionID error"),
}
}
}
@@ -419,15 +391,13 @@ mod tests {
assert!(result.is_err());
if let Err(err) = result {
if let Some(storage_err) = err.downcast_ref::<StorageError>() {
match storage_err {
StorageError::InvalidVersionID(bucket, object, version) => {
assert_eq!(bucket, "test-bucket");
assert_eq!(object, "test-object");
assert_eq!(version, "invalid-uuid");
}
_ => panic!("Expected InvalidVersionID error"),
match err {
StorageError::InvalidVersionID(bucket, object, version) => {
assert_eq!(bucket, "test-bucket");
assert_eq!(object, "test-object");
assert_eq!(version, "invalid-uuid");
}
_ => panic!("Expected InvalidVersionID error"),
}
}
}