mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-06 13:27:43 +00:00
5dfc1b5f07
* fix(readiness): gate on lock quorum health Include distributed lock quorum in runtime readiness decisions and expose the signal in health responses. Map namespace lock quorum failures to ServiceUnavailable instead of InternalError so clients can safely retry while keeping quorum enforcement unchanged. * fix(ecstore): restore namespace lock error decoding Add the missing from_u32 arm for NamespaceLockQuorumUnavailable and cover the numeric roundtrip in error tests.
1399 lines
60 KiB
Rust
1399 lines
60 KiB
Rust
// Copyright 2024 RustFS Team
|
||
//
|
||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||
// you may not use this file except in compliance with the License.
|
||
// You may obtain a copy of the License at
|
||
//
|
||
// http://www.apache.org/licenses/LICENSE-2.0
|
||
//
|
||
// Unless required by applicable law or agreed to in writing, software
|
||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
// See the License for the specific language governing permissions and
|
||
// limitations under the License.
|
||
|
||
use crate::bucket::error::BucketMetadataError;
|
||
use crate::disk::error::DiskError;
|
||
use rustfs_utils::path::decode_dir_object;
|
||
use s3s::{S3Error, S3ErrorCode};
|
||
|
||
pub type Error = StorageError;
|
||
pub type Result<T> = core::result::Result<T, Error>;
|
||
|
||
/// Storage layer error type covering disk, volume, bucket, object, multipart,
|
||
/// erasure-coding, and operational error conditions.
|
||
///
|
||
/// Variants are organized by domain:
|
||
/// - **Disk / Volume** – low-level storage I/O
|
||
/// - **Bucket** – bucket-level operations
|
||
/// - **Object** – object-level operations (including versioning)
|
||
/// - **Multipart** – multipart upload lifecycle
|
||
/// - **Erasure / Quorum** – erasure coding and quorum failures
|
||
/// - **Operational** – decommission, healing, rate-limiting, etc.
|
||
/// - **Generic** – I/O, locks, and catch-all errors
|
||
#[derive(Debug, thiserror::Error)]
|
||
pub enum StorageError {
|
||
// ── Disk / Volume ────────────────────────────────────────────────
|
||
#[error("Faulty disk")]
|
||
FaultyDisk,
|
||
#[error("Faulty remote disk")]
|
||
FaultyRemoteDisk,
|
||
#[error("Disk full")]
|
||
DiskFull,
|
||
#[error("Disk not found")]
|
||
DiskNotFound,
|
||
#[error("Disk access denied")]
|
||
DiskAccessDenied,
|
||
#[error("Drive is root")]
|
||
DriveIsRoot,
|
||
#[error("Unformatted disk")]
|
||
UnformattedDisk,
|
||
#[error("Corrupted format")]
|
||
CorruptedFormat,
|
||
#[error("Corrupted backend")]
|
||
CorruptedBackend,
|
||
#[error("Too many open files")]
|
||
TooManyOpenFiles,
|
||
|
||
#[error("Volume not found")]
|
||
VolumeNotFound,
|
||
#[error("Volume exists")]
|
||
VolumeExists,
|
||
#[error("Volume not empty")]
|
||
VolumeNotEmpty,
|
||
#[error("Volume access denied")]
|
||
VolumeAccessDenied,
|
||
|
||
#[error("File not found")]
|
||
FileNotFound,
|
||
#[error("File version not found")]
|
||
FileVersionNotFound,
|
||
#[error("File name too long")]
|
||
FileNameTooLong,
|
||
#[error("File access denied")]
|
||
FileAccessDenied,
|
||
#[error("File is corrupted")]
|
||
FileCorrupt,
|
||
#[error("Not a regular file")]
|
||
IsNotRegular,
|
||
|
||
// ── Bucket ───────────────────────────────────────────────────────
|
||
#[error("Bucket not found: {0}")]
|
||
BucketNotFound(String),
|
||
#[error("Bucket exists: {0}")]
|
||
BucketExists(String),
|
||
#[error("Bucket not empty: {0}")]
|
||
BucketNotEmpty(String),
|
||
#[error("Bucket name invalid: {0}")]
|
||
BucketNameInvalid(String),
|
||
|
||
// ── Object ───────────────────────────────────────────────────────
|
||
#[error("Object not found: {0}/{1}")]
|
||
ObjectNotFound(String, String),
|
||
#[error("Object name invalid: {0}/{1}")]
|
||
ObjectNameInvalid(String, String),
|
||
#[error("Object name too long: {0}/{1}")]
|
||
ObjectNameTooLong(String, String),
|
||
#[error("Object name contains forward slash as prefix: {0}/{1}")]
|
||
ObjectNamePrefixAsSlash(String, String),
|
||
#[error("Object exists on :{0} as directory {1}")]
|
||
ObjectExistsAsDirectory(String, String),
|
||
#[error("Version not found: {0}/{1}-{2}")]
|
||
VersionNotFound(String, String, String),
|
||
#[error("Invalid version id: {0}/{1}-{2}")]
|
||
InvalidVersionID(String, String, String),
|
||
#[error("invalid data movement operation, source and destination pool are the same for : {0}/{1}-{2}")]
|
||
DataMovementOverwriteErr(String, String, String),
|
||
#[error("Prefix access is denied:{0}/{1}")]
|
||
PrefixAccessDenied(String, String),
|
||
|
||
// ── Multipart ────────────────────────────────────────────────────
|
||
#[error("Invalid upload id: {0}/{1}-{2}")]
|
||
InvalidUploadID(String, String, String),
|
||
#[error("Invalid UploadID KeyCombination: {0}/{1}")]
|
||
InvalidUploadIDKeyCombination(String, String),
|
||
#[error("Malformed UploadID: {0}")]
|
||
MalformedUploadID(String),
|
||
#[error("Specified part could not be found. PartNumber {0}, Expected {1}, got {2}")]
|
||
InvalidPart(usize, String, String),
|
||
#[error("Invalid part number: {0}")]
|
||
InvalidPartNumber(usize),
|
||
#[error("Your proposed upload is smaller than the minimum allowed size. Part {0} size {1} is less than minimum {2}")]
|
||
EntityTooSmall(usize, i64, i64),
|
||
|
||
// ── Erasure / Quorum ─────────────────────────────────────────────
|
||
#[error("erasure read quorum")]
|
||
ErasureReadQuorum,
|
||
#[error("erasure write quorum")]
|
||
ErasureWriteQuorum,
|
||
#[error("Storage resources are insufficient for the read operation: {0}/{1}")]
|
||
InsufficientReadQuorum(String, String),
|
||
#[error("Storage resources are insufficient for the write operation: {0}/{1}")]
|
||
InsufficientWriteQuorum(String, String),
|
||
#[error("not first disk")]
|
||
NotFirstDisk,
|
||
#[error("first disk wait")]
|
||
FirstDiskWait,
|
||
|
||
// ── Operational ──────────────────────────────────────────────────
|
||
#[error("Storage reached its minimum free drive threshold.")]
|
||
StorageFull,
|
||
#[error("Please reduce your request rate")]
|
||
SlowDown,
|
||
#[error("Decommission not started")]
|
||
DecommissionNotStarted,
|
||
#[error("Decommission already running")]
|
||
DecommissionAlreadyRunning,
|
||
#[error("Rebalance already running")]
|
||
RebalanceAlreadyRunning,
|
||
#[error("Operation canceled")]
|
||
OperationCanceled,
|
||
#[error("No heal required")]
|
||
NoHealRequired,
|
||
#[error("DoneForNow")]
|
||
DoneForNow,
|
||
#[error("Config not found")]
|
||
ConfigNotFound,
|
||
#[error("Precondition failed")]
|
||
PreconditionFailed,
|
||
#[error("Not modified")]
|
||
NotModified,
|
||
#[error("Invalid range specified: {0}")]
|
||
InvalidRangeSpec(String),
|
||
#[error("Namespace lock quorum unavailable for {mode} lock on {bucket}/{object}: required {required}, achieved {achieved}")]
|
||
NamespaceLockQuorumUnavailable {
|
||
mode: &'static str,
|
||
bucket: String,
|
||
object: String,
|
||
required: usize,
|
||
achieved: usize,
|
||
},
|
||
|
||
// ── Generic ──────────────────────────────────────────────────────
|
||
#[error("Unexpected error")]
|
||
Unexpected,
|
||
#[error("not implemented")]
|
||
NotImplemented,
|
||
#[error("Invalid arguments provided for {0}/{1}-{2}")]
|
||
InvalidArgument(String, String, String),
|
||
#[error("method not allowed")]
|
||
MethodNotAllowed,
|
||
#[error("Io error: {0}")]
|
||
Io(std::io::Error),
|
||
#[error("Lock error: {0}")]
|
||
Lock(#[from] rustfs_lock::LockError),
|
||
}
|
||
|
||
impl StorageError {
|
||
pub fn other<E>(error: E) -> Self
|
||
where
|
||
E: Into<Box<dyn std::error::Error + Send + Sync>>,
|
||
{
|
||
StorageError::Io(std::io::Error::other(error))
|
||
}
|
||
|
||
pub fn is_not_found(&self) -> bool {
|
||
matches!(
|
||
self,
|
||
StorageError::FileNotFound
|
||
| StorageError::ObjectNotFound(_, _)
|
||
| StorageError::FileVersionNotFound
|
||
| StorageError::VersionNotFound(_, _, _)
|
||
| StorageError::VolumeNotFound
|
||
| StorageError::DiskNotFound
|
||
| StorageError::BucketNotFound(_)
|
||
)
|
||
}
|
||
|
||
pub fn is_quorum_error(&self) -> bool {
|
||
matches!(
|
||
self,
|
||
StorageError::ErasureReadQuorum
|
||
| StorageError::ErasureWriteQuorum
|
||
| StorageError::InsufficientReadQuorum(_, _)
|
||
| StorageError::InsufficientWriteQuorum(_, _)
|
||
| StorageError::NamespaceLockQuorumUnavailable { .. }
|
||
)
|
||
}
|
||
}
|
||
|
||
impl From<DiskError> for StorageError {
|
||
fn from(e: DiskError) -> Self {
|
||
match e {
|
||
DiskError::Io(io_error) => StorageError::Io(io_error),
|
||
// DiskError::MaxVersionsExceeded => todo!(),
|
||
DiskError::Unexpected => StorageError::Unexpected,
|
||
DiskError::CorruptedFormat => StorageError::CorruptedFormat,
|
||
DiskError::CorruptedBackend => StorageError::CorruptedBackend,
|
||
DiskError::UnformattedDisk => StorageError::UnformattedDisk,
|
||
// DiskError::InconsistentDisk => StorageError::InconsistentDisk,
|
||
// DiskError::UnsupportedDisk => StorageError::UnsupportedDisk,
|
||
DiskError::DiskFull => StorageError::DiskFull,
|
||
// DiskError::DiskNotDir => StorageError::DiskNotDir,
|
||
DiskError::DiskNotFound => StorageError::DiskNotFound,
|
||
// DiskError::DiskOngoingReq => StorageError::DiskOngoingReq,
|
||
DiskError::DriveIsRoot => StorageError::DriveIsRoot,
|
||
DiskError::FaultyRemoteDisk => StorageError::FaultyRemoteDisk,
|
||
DiskError::FaultyDisk => StorageError::FaultyDisk,
|
||
DiskError::DiskAccessDenied => StorageError::DiskAccessDenied,
|
||
DiskError::FileNotFound => StorageError::FileNotFound,
|
||
DiskError::FileVersionNotFound => StorageError::FileVersionNotFound,
|
||
DiskError::TooManyOpenFiles => StorageError::TooManyOpenFiles,
|
||
DiskError::FileNameTooLong => StorageError::FileNameTooLong,
|
||
DiskError::VolumeExists => StorageError::VolumeExists,
|
||
DiskError::IsNotRegular => StorageError::IsNotRegular,
|
||
// DiskError::PathNotFound => StorageError::PathNotFound,
|
||
DiskError::VolumeNotFound => StorageError::VolumeNotFound,
|
||
DiskError::VolumeNotEmpty => StorageError::VolumeNotEmpty,
|
||
DiskError::VolumeAccessDenied => StorageError::VolumeAccessDenied,
|
||
DiskError::FileAccessDenied => StorageError::FileAccessDenied,
|
||
DiskError::FileCorrupt => StorageError::FileCorrupt,
|
||
// DiskError::BitrotHashAlgoInvalid => StorageError::BitrotHashAlgoInvalid,
|
||
// DiskError::CrossDeviceLink => StorageError::CrossDeviceLink,
|
||
// DiskError::LessData => StorageError::LessData,
|
||
// DiskError::MoreData => StorageError::MoreData,
|
||
// DiskError::OutdatedXLMeta => StorageError::OutdatedXLMeta,
|
||
// DiskError::PartMissingOrCorrupt => StorageError::PartMissingOrCorrupt,
|
||
DiskError::NoHealRequired => StorageError::NoHealRequired,
|
||
DiskError::MethodNotAllowed => StorageError::MethodNotAllowed,
|
||
DiskError::ErasureReadQuorum => StorageError::ErasureReadQuorum,
|
||
DiskError::ErasureWriteQuorum => StorageError::ErasureWriteQuorum,
|
||
_ => StorageError::Io(std::io::Error::other(e)),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<StorageError> for DiskError {
|
||
fn from(val: StorageError) -> Self {
|
||
match val {
|
||
StorageError::Io(io_error) => io_error.into(),
|
||
StorageError::Unexpected => DiskError::Unexpected,
|
||
StorageError::FileNotFound => DiskError::FileNotFound,
|
||
StorageError::FileVersionNotFound => DiskError::FileVersionNotFound,
|
||
StorageError::FileCorrupt => DiskError::FileCorrupt,
|
||
StorageError::MethodNotAllowed => DiskError::MethodNotAllowed,
|
||
StorageError::StorageFull => DiskError::DiskFull,
|
||
StorageError::SlowDown => DiskError::TooManyOpenFiles,
|
||
StorageError::ErasureReadQuorum => DiskError::ErasureReadQuorum,
|
||
StorageError::ErasureWriteQuorum => DiskError::ErasureWriteQuorum,
|
||
StorageError::TooManyOpenFiles => DiskError::TooManyOpenFiles,
|
||
StorageError::NoHealRequired => DiskError::NoHealRequired,
|
||
StorageError::CorruptedFormat => DiskError::CorruptedFormat,
|
||
StorageError::CorruptedBackend => DiskError::CorruptedBackend,
|
||
StorageError::UnformattedDisk => DiskError::UnformattedDisk,
|
||
StorageError::DiskNotFound => DiskError::DiskNotFound,
|
||
StorageError::FaultyDisk => DiskError::FaultyDisk,
|
||
StorageError::DiskFull => DiskError::DiskFull,
|
||
StorageError::VolumeNotFound => DiskError::VolumeNotFound,
|
||
StorageError::VolumeExists => DiskError::VolumeExists,
|
||
StorageError::FileNameTooLong => DiskError::FileNameTooLong,
|
||
_ => DiskError::other(val),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<BucketMetadataError> for Error {
|
||
fn from(e: BucketMetadataError) -> Self {
|
||
match e {
|
||
BucketMetadataError::TaggingNotFound => Error::ConfigNotFound,
|
||
BucketMetadataError::BucketPolicyNotFound => Error::ConfigNotFound,
|
||
BucketMetadataError::BucketObjectLockConfigNotFound => Error::ConfigNotFound,
|
||
BucketMetadataError::BucketLifecycleNotFound => Error::ConfigNotFound,
|
||
BucketMetadataError::BucketSSEConfigNotFound => Error::ConfigNotFound,
|
||
BucketMetadataError::BucketQuotaConfigNotFound => Error::ConfigNotFound,
|
||
BucketMetadataError::BucketReplicationConfigNotFound => Error::ConfigNotFound,
|
||
BucketMetadataError::BucketRemoteTargetNotFound => Error::ConfigNotFound,
|
||
_ => Error::other(e),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<std::io::Error> for StorageError {
|
||
fn from(e: std::io::Error) -> Self {
|
||
match e.downcast::<StorageError>() {
|
||
Ok(storage_error) => storage_error,
|
||
Err(io_error) => match io_error.downcast::<DiskError>() {
|
||
Ok(disk_error) => disk_error.into(),
|
||
Err(io_error) => StorageError::Io(io_error),
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<StorageError> for std::io::Error {
|
||
fn from(e: StorageError) -> Self {
|
||
match e {
|
||
StorageError::Io(io_error) => io_error,
|
||
e => std::io::Error::other(e),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<rustfs_filemeta::Error> for StorageError {
|
||
fn from(e: rustfs_filemeta::Error) -> Self {
|
||
match e {
|
||
rustfs_filemeta::Error::DoneForNow => StorageError::DoneForNow,
|
||
rustfs_filemeta::Error::MethodNotAllowed => StorageError::MethodNotAllowed,
|
||
rustfs_filemeta::Error::VolumeNotFound => StorageError::VolumeNotFound,
|
||
rustfs_filemeta::Error::FileNotFound => StorageError::FileNotFound,
|
||
rustfs_filemeta::Error::FileVersionNotFound => StorageError::FileVersionNotFound,
|
||
rustfs_filemeta::Error::FileCorrupt => StorageError::FileCorrupt,
|
||
rustfs_filemeta::Error::Unexpected => StorageError::Unexpected,
|
||
rustfs_filemeta::Error::Io(io_error) => io_error.into(),
|
||
_ => StorageError::Io(std::io::Error::other(e)),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<StorageError> for rustfs_filemeta::Error {
|
||
fn from(val: StorageError) -> Self {
|
||
match val {
|
||
StorageError::Unexpected => rustfs_filemeta::Error::Unexpected,
|
||
StorageError::FileNotFound => rustfs_filemeta::Error::FileNotFound,
|
||
StorageError::FileVersionNotFound => rustfs_filemeta::Error::FileVersionNotFound,
|
||
StorageError::FileCorrupt => rustfs_filemeta::Error::FileCorrupt,
|
||
StorageError::DoneForNow => rustfs_filemeta::Error::DoneForNow,
|
||
StorageError::MethodNotAllowed => rustfs_filemeta::Error::MethodNotAllowed,
|
||
StorageError::VolumeNotFound => rustfs_filemeta::Error::VolumeNotFound,
|
||
StorageError::Io(io_error) => io_error.into(),
|
||
_ => rustfs_filemeta::Error::other(val),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl PartialEq for StorageError {
|
||
fn eq(&self, other: &Self) -> bool {
|
||
match (self, other) {
|
||
(StorageError::Io(e1), StorageError::Io(e2)) => e1.kind() == e2.kind() && e1.to_string() == e2.to_string(),
|
||
(e1, e2) => e1.to_u32() == e2.to_u32(),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Clone for StorageError {
|
||
fn clone(&self) -> Self {
|
||
match self {
|
||
StorageError::Io(e) => StorageError::Io(std::io::Error::new(e.kind(), e.to_string())),
|
||
StorageError::FaultyDisk => StorageError::FaultyDisk,
|
||
StorageError::DiskFull => StorageError::DiskFull,
|
||
StorageError::VolumeNotFound => StorageError::VolumeNotFound,
|
||
StorageError::VolumeExists => StorageError::VolumeExists,
|
||
StorageError::FileNotFound => StorageError::FileNotFound,
|
||
StorageError::FileVersionNotFound => StorageError::FileVersionNotFound,
|
||
StorageError::FileNameTooLong => StorageError::FileNameTooLong,
|
||
StorageError::FileAccessDenied => StorageError::FileAccessDenied,
|
||
StorageError::FileCorrupt => StorageError::FileCorrupt,
|
||
StorageError::IsNotRegular => StorageError::IsNotRegular,
|
||
StorageError::VolumeNotEmpty => StorageError::VolumeNotEmpty,
|
||
StorageError::VolumeAccessDenied => StorageError::VolumeAccessDenied,
|
||
StorageError::CorruptedFormat => StorageError::CorruptedFormat,
|
||
StorageError::CorruptedBackend => StorageError::CorruptedBackend,
|
||
StorageError::UnformattedDisk => StorageError::UnformattedDisk,
|
||
StorageError::DiskNotFound => StorageError::DiskNotFound,
|
||
StorageError::DriveIsRoot => StorageError::DriveIsRoot,
|
||
StorageError::FaultyRemoteDisk => StorageError::FaultyRemoteDisk,
|
||
StorageError::DiskAccessDenied => StorageError::DiskAccessDenied,
|
||
StorageError::Unexpected => StorageError::Unexpected,
|
||
StorageError::ConfigNotFound => StorageError::ConfigNotFound,
|
||
StorageError::NotImplemented => StorageError::NotImplemented,
|
||
StorageError::InvalidArgument(a, b, c) => StorageError::InvalidArgument(a.clone(), b.clone(), c.clone()),
|
||
StorageError::MethodNotAllowed => StorageError::MethodNotAllowed,
|
||
StorageError::BucketNotFound(a) => StorageError::BucketNotFound(a.clone()),
|
||
StorageError::BucketNotEmpty(a) => StorageError::BucketNotEmpty(a.clone()),
|
||
StorageError::BucketNameInvalid(a) => StorageError::BucketNameInvalid(a.clone()),
|
||
StorageError::ObjectNameInvalid(a, b) => StorageError::ObjectNameInvalid(a.clone(), b.clone()),
|
||
StorageError::BucketExists(a) => StorageError::BucketExists(a.clone()),
|
||
StorageError::StorageFull => StorageError::StorageFull,
|
||
StorageError::SlowDown => StorageError::SlowDown,
|
||
StorageError::PrefixAccessDenied(a, b) => StorageError::PrefixAccessDenied(a.clone(), b.clone()),
|
||
StorageError::InvalidUploadIDKeyCombination(a, b) => {
|
||
StorageError::InvalidUploadIDKeyCombination(a.clone(), b.clone())
|
||
}
|
||
StorageError::MalformedUploadID(a) => StorageError::MalformedUploadID(a.clone()),
|
||
StorageError::ObjectNameTooLong(a, b) => StorageError::ObjectNameTooLong(a.clone(), b.clone()),
|
||
StorageError::ObjectNamePrefixAsSlash(a, b) => StorageError::ObjectNamePrefixAsSlash(a.clone(), b.clone()),
|
||
StorageError::ObjectNotFound(a, b) => StorageError::ObjectNotFound(a.clone(), b.clone()),
|
||
StorageError::VersionNotFound(a, b, c) => StorageError::VersionNotFound(a.clone(), b.clone(), c.clone()),
|
||
StorageError::InvalidUploadID(a, b, c) => StorageError::InvalidUploadID(a.clone(), b.clone(), c.clone()),
|
||
StorageError::InvalidVersionID(a, b, c) => StorageError::InvalidVersionID(a.clone(), b.clone(), c.clone()),
|
||
StorageError::DataMovementOverwriteErr(a, b, c) => {
|
||
StorageError::DataMovementOverwriteErr(a.clone(), b.clone(), c.clone())
|
||
}
|
||
StorageError::ObjectExistsAsDirectory(a, b) => StorageError::ObjectExistsAsDirectory(a.clone(), b.clone()),
|
||
// StorageError::InsufficientReadQuorum => StorageError::InsufficientReadQuorum,
|
||
// StorageError::InsufficientWriteQuorum => StorageError::InsufficientWriteQuorum,
|
||
StorageError::DecommissionNotStarted => StorageError::DecommissionNotStarted,
|
||
StorageError::InvalidPart(a, b, c) => StorageError::InvalidPart(*a, b.clone(), c.clone()),
|
||
StorageError::EntityTooSmall(a, b, c) => StorageError::EntityTooSmall(*a, *b, *c),
|
||
StorageError::DoneForNow => StorageError::DoneForNow,
|
||
StorageError::DecommissionAlreadyRunning => StorageError::DecommissionAlreadyRunning,
|
||
StorageError::RebalanceAlreadyRunning => StorageError::RebalanceAlreadyRunning,
|
||
StorageError::OperationCanceled => StorageError::OperationCanceled,
|
||
StorageError::ErasureReadQuorum => StorageError::ErasureReadQuorum,
|
||
StorageError::ErasureWriteQuorum => StorageError::ErasureWriteQuorum,
|
||
StorageError::NotFirstDisk => StorageError::NotFirstDisk,
|
||
StorageError::FirstDiskWait => StorageError::FirstDiskWait,
|
||
StorageError::TooManyOpenFiles => StorageError::TooManyOpenFiles,
|
||
StorageError::NoHealRequired => StorageError::NoHealRequired,
|
||
StorageError::Lock(e) => StorageError::Lock(e.clone()),
|
||
StorageError::InsufficientReadQuorum(a, b) => StorageError::InsufficientReadQuorum(a.clone(), b.clone()),
|
||
StorageError::InsufficientWriteQuorum(a, b) => StorageError::InsufficientWriteQuorum(a.clone(), b.clone()),
|
||
StorageError::PreconditionFailed => StorageError::PreconditionFailed,
|
||
StorageError::NotModified => StorageError::NotModified,
|
||
StorageError::InvalidPartNumber(a) => StorageError::InvalidPartNumber(*a),
|
||
StorageError::InvalidRangeSpec(a) => StorageError::InvalidRangeSpec(a.clone()),
|
||
StorageError::NamespaceLockQuorumUnavailable {
|
||
mode,
|
||
bucket,
|
||
object,
|
||
required,
|
||
achieved,
|
||
} => StorageError::NamespaceLockQuorumUnavailable {
|
||
mode,
|
||
bucket: bucket.clone(),
|
||
object: object.clone(),
|
||
required: *required,
|
||
achieved: *achieved,
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
impl StorageError {
|
||
pub fn to_u32(&self) -> u32 {
|
||
match self {
|
||
StorageError::Io(_) => 0x01,
|
||
StorageError::FaultyDisk => 0x02,
|
||
StorageError::DiskFull => 0x03,
|
||
StorageError::VolumeNotFound => 0x04,
|
||
StorageError::VolumeExists => 0x05,
|
||
StorageError::FileNotFound => 0x06,
|
||
StorageError::FileVersionNotFound => 0x07,
|
||
StorageError::FileNameTooLong => 0x08,
|
||
StorageError::FileAccessDenied => 0x09,
|
||
StorageError::FileCorrupt => 0x0A,
|
||
StorageError::IsNotRegular => 0x0B,
|
||
StorageError::VolumeNotEmpty => 0x0C,
|
||
StorageError::VolumeAccessDenied => 0x0D,
|
||
StorageError::CorruptedFormat => 0x0E,
|
||
StorageError::CorruptedBackend => 0x0F,
|
||
StorageError::UnformattedDisk => 0x10,
|
||
StorageError::DiskNotFound => 0x11,
|
||
StorageError::DriveIsRoot => 0x12,
|
||
StorageError::FaultyRemoteDisk => 0x13,
|
||
StorageError::DiskAccessDenied => 0x14,
|
||
StorageError::Unexpected => 0x15,
|
||
StorageError::NotImplemented => 0x16,
|
||
StorageError::InvalidArgument(_, _, _) => 0x17,
|
||
StorageError::MethodNotAllowed => 0x18,
|
||
StorageError::BucketNotFound(_) => 0x19,
|
||
StorageError::BucketNotEmpty(_) => 0x1A,
|
||
StorageError::BucketNameInvalid(_) => 0x1B,
|
||
StorageError::ObjectNameInvalid(_, _) => 0x1C,
|
||
StorageError::BucketExists(_) => 0x1D,
|
||
StorageError::StorageFull => 0x1E,
|
||
StorageError::SlowDown => 0x1F,
|
||
StorageError::PrefixAccessDenied(_, _) => 0x20,
|
||
StorageError::InvalidUploadIDKeyCombination(_, _) => 0x21,
|
||
StorageError::MalformedUploadID(_) => 0x22,
|
||
StorageError::ObjectNameTooLong(_, _) => 0x23,
|
||
StorageError::ObjectNamePrefixAsSlash(_, _) => 0x24,
|
||
StorageError::ObjectNotFound(_, _) => 0x25,
|
||
StorageError::VersionNotFound(_, _, _) => 0x26,
|
||
StorageError::InvalidUploadID(_, _, _) => 0x27,
|
||
StorageError::InvalidVersionID(_, _, _) => 0x28,
|
||
StorageError::DataMovementOverwriteErr(_, _, _) => 0x29,
|
||
StorageError::ObjectExistsAsDirectory(_, _) => 0x2A,
|
||
// StorageError::InsufficientReadQuorum => 0x2B,
|
||
// StorageError::InsufficientWriteQuorum => 0x2C,
|
||
StorageError::DecommissionNotStarted => 0x2D,
|
||
StorageError::InvalidPart(_, _, _) => 0x2E,
|
||
StorageError::DoneForNow => 0x2F,
|
||
StorageError::DecommissionAlreadyRunning => 0x30,
|
||
StorageError::RebalanceAlreadyRunning => 0x40,
|
||
StorageError::OperationCanceled => 0x41,
|
||
StorageError::ErasureReadQuorum => 0x31,
|
||
StorageError::ErasureWriteQuorum => 0x32,
|
||
StorageError::NotFirstDisk => 0x33,
|
||
StorageError::FirstDiskWait => 0x34,
|
||
StorageError::ConfigNotFound => 0x35,
|
||
StorageError::TooManyOpenFiles => 0x36,
|
||
StorageError::NoHealRequired => 0x37,
|
||
StorageError::Lock(_) => 0x38,
|
||
StorageError::InsufficientReadQuorum(_, _) => 0x39,
|
||
StorageError::InsufficientWriteQuorum(_, _) => 0x3A,
|
||
StorageError::PreconditionFailed => 0x3B,
|
||
StorageError::EntityTooSmall(_, _, _) => 0x3C,
|
||
StorageError::InvalidRangeSpec(_) => 0x3D,
|
||
StorageError::NotModified => 0x3E,
|
||
StorageError::InvalidPartNumber(_) => 0x3F,
|
||
StorageError::NamespaceLockQuorumUnavailable { .. } => 0x42,
|
||
}
|
||
}
|
||
|
||
pub fn from_u32(error: u32) -> Option<Self> {
|
||
match error {
|
||
0x01 => Some(StorageError::Io(std::io::Error::other("Io error"))),
|
||
0x02 => Some(StorageError::FaultyDisk),
|
||
0x03 => Some(StorageError::DiskFull),
|
||
0x04 => Some(StorageError::VolumeNotFound),
|
||
0x05 => Some(StorageError::VolumeExists),
|
||
0x06 => Some(StorageError::FileNotFound),
|
||
0x07 => Some(StorageError::FileVersionNotFound),
|
||
0x08 => Some(StorageError::FileNameTooLong),
|
||
0x09 => Some(StorageError::FileAccessDenied),
|
||
0x0A => Some(StorageError::FileCorrupt),
|
||
0x0B => Some(StorageError::IsNotRegular),
|
||
0x0C => Some(StorageError::VolumeNotEmpty),
|
||
0x0D => Some(StorageError::VolumeAccessDenied),
|
||
0x0E => Some(StorageError::CorruptedFormat),
|
||
0x0F => Some(StorageError::CorruptedBackend),
|
||
0x10 => Some(StorageError::UnformattedDisk),
|
||
0x11 => Some(StorageError::DiskNotFound),
|
||
0x12 => Some(StorageError::DriveIsRoot),
|
||
0x13 => Some(StorageError::FaultyRemoteDisk),
|
||
0x14 => Some(StorageError::DiskAccessDenied),
|
||
0x15 => Some(StorageError::Unexpected),
|
||
0x16 => Some(StorageError::NotImplemented),
|
||
0x17 => Some(StorageError::InvalidArgument(Default::default(), Default::default(), Default::default())),
|
||
0x18 => Some(StorageError::MethodNotAllowed),
|
||
0x19 => Some(StorageError::BucketNotFound(Default::default())),
|
||
0x1A => Some(StorageError::BucketNotEmpty(Default::default())),
|
||
0x1B => Some(StorageError::BucketNameInvalid(Default::default())),
|
||
0x1C => Some(StorageError::ObjectNameInvalid(Default::default(), Default::default())),
|
||
0x1D => Some(StorageError::BucketExists(Default::default())),
|
||
0x1E => Some(StorageError::StorageFull),
|
||
0x1F => Some(StorageError::SlowDown),
|
||
0x20 => Some(StorageError::PrefixAccessDenied(Default::default(), Default::default())),
|
||
0x21 => Some(StorageError::InvalidUploadIDKeyCombination(Default::default(), Default::default())),
|
||
0x22 => Some(StorageError::MalformedUploadID(Default::default())),
|
||
0x23 => Some(StorageError::ObjectNameTooLong(Default::default(), Default::default())),
|
||
0x24 => Some(StorageError::ObjectNamePrefixAsSlash(Default::default(), Default::default())),
|
||
0x25 => Some(StorageError::ObjectNotFound(Default::default(), Default::default())),
|
||
0x26 => Some(StorageError::VersionNotFound(Default::default(), Default::default(), Default::default())),
|
||
0x27 => Some(StorageError::InvalidUploadID(Default::default(), Default::default(), Default::default())),
|
||
0x28 => Some(StorageError::InvalidVersionID(Default::default(), Default::default(), Default::default())),
|
||
0x29 => Some(StorageError::DataMovementOverwriteErr(
|
||
Default::default(),
|
||
Default::default(),
|
||
Default::default(),
|
||
)),
|
||
0x2A => Some(StorageError::ObjectExistsAsDirectory(Default::default(), Default::default())),
|
||
// 0x2B => Some(StorageError::InsufficientReadQuorum),
|
||
// 0x2C => Some(StorageError::InsufficientWriteQuorum),
|
||
0x2D => Some(StorageError::DecommissionNotStarted),
|
||
0x2E => Some(StorageError::InvalidPart(Default::default(), Default::default(), Default::default())),
|
||
0x2F => Some(StorageError::DoneForNow),
|
||
0x30 => Some(StorageError::DecommissionAlreadyRunning),
|
||
0x40 => Some(StorageError::RebalanceAlreadyRunning),
|
||
0x41 => Some(StorageError::OperationCanceled),
|
||
0x31 => Some(StorageError::ErasureReadQuorum),
|
||
0x32 => Some(StorageError::ErasureWriteQuorum),
|
||
0x33 => Some(StorageError::NotFirstDisk),
|
||
0x34 => Some(StorageError::FirstDiskWait),
|
||
0x35 => Some(StorageError::ConfigNotFound),
|
||
0x36 => Some(StorageError::TooManyOpenFiles),
|
||
0x37 => Some(StorageError::NoHealRequired),
|
||
0x38 => Some(StorageError::Lock(rustfs_lock::LockError::internal("Generic lock error".to_string()))),
|
||
0x39 => Some(StorageError::InsufficientReadQuorum(Default::default(), Default::default())),
|
||
0x3A => Some(StorageError::InsufficientWriteQuorum(Default::default(), Default::default())),
|
||
0x3B => Some(StorageError::PreconditionFailed),
|
||
0x3C => Some(StorageError::EntityTooSmall(Default::default(), Default::default(), Default::default())),
|
||
0x3D => Some(StorageError::InvalidRangeSpec(Default::default())),
|
||
0x3E => Some(StorageError::NotModified),
|
||
0x3F => Some(StorageError::InvalidPartNumber(Default::default())),
|
||
0x42 => Some(StorageError::NamespaceLockQuorumUnavailable {
|
||
mode: "write",
|
||
bucket: Default::default(),
|
||
object: Default::default(),
|
||
required: Default::default(),
|
||
achieved: Default::default(),
|
||
}),
|
||
_ => None,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<tokio::task::JoinError> for StorageError {
|
||
fn from(e: tokio::task::JoinError) -> Self {
|
||
StorageError::other(e)
|
||
}
|
||
}
|
||
|
||
impl From<serde_json::Error> for StorageError {
|
||
fn from(e: serde_json::Error) -> Self {
|
||
StorageError::other(e)
|
||
}
|
||
}
|
||
|
||
impl From<rmp_serde::encode::Error> for Error {
|
||
fn from(e: rmp_serde::encode::Error) -> Self {
|
||
Error::other(e)
|
||
}
|
||
}
|
||
|
||
impl From<rmp::encode::ValueWriteError> for Error {
|
||
fn from(e: rmp::encode::ValueWriteError) -> Self {
|
||
Error::other(e)
|
||
}
|
||
}
|
||
|
||
impl From<rmp::decode::ValueReadError> for Error {
|
||
fn from(e: rmp::decode::ValueReadError) -> Self {
|
||
Error::other(e)
|
||
}
|
||
}
|
||
|
||
impl From<std::string::FromUtf8Error> for Error {
|
||
fn from(e: std::string::FromUtf8Error) -> Self {
|
||
Error::other(e)
|
||
}
|
||
}
|
||
|
||
impl From<rmp::decode::NumValueReadError> for Error {
|
||
fn from(e: rmp::decode::NumValueReadError) -> Self {
|
||
Error::other(e)
|
||
}
|
||
}
|
||
|
||
impl From<rmp_serde::decode::Error> for Error {
|
||
fn from(e: rmp_serde::decode::Error) -> Self {
|
||
Error::other(e)
|
||
}
|
||
}
|
||
|
||
impl From<s3s::xml::SerError> for Error {
|
||
fn from(e: s3s::xml::SerError) -> Self {
|
||
Error::other(e)
|
||
}
|
||
}
|
||
|
||
impl From<s3s::xml::DeError> for Error {
|
||
fn from(e: s3s::xml::DeError) -> Self {
|
||
Error::other(e)
|
||
}
|
||
}
|
||
|
||
impl From<tonic::Status> for Error {
|
||
fn from(e: tonic::Status) -> Self {
|
||
Error::other(e.to_string())
|
||
}
|
||
}
|
||
|
||
impl From<uuid::Error> for Error {
|
||
fn from(e: uuid::Error) -> Self {
|
||
Error::other(e)
|
||
}
|
||
}
|
||
|
||
impl From<time::error::ComponentRange> for Error {
|
||
fn from(e: time::error::ComponentRange) -> Self {
|
||
Error::other(e)
|
||
}
|
||
}
|
||
|
||
pub fn is_err_object_not_found(err: &Error) -> bool {
|
||
matches!(err, &Error::FileNotFound) || matches!(err, &Error::ObjectNotFound(_, _))
|
||
}
|
||
|
||
pub fn is_err_version_not_found(err: &Error) -> bool {
|
||
matches!(err, &Error::FileVersionNotFound) || matches!(err, &Error::VersionNotFound(_, _, _))
|
||
}
|
||
|
||
pub fn is_err_bucket_exists(err: &Error) -> bool {
|
||
matches!(err, &StorageError::BucketExists(_))
|
||
}
|
||
|
||
pub fn is_err_read_quorum(err: &Error) -> bool {
|
||
matches!(err, &StorageError::ErasureReadQuorum)
|
||
}
|
||
|
||
pub fn classify_system_path_failure_reason(err: &Error) -> &'static str {
|
||
match err {
|
||
StorageError::ConfigNotFound => "config_not_found",
|
||
StorageError::ErasureReadQuorum | StorageError::InsufficientReadQuorum(_, _) => "read_quorum",
|
||
StorageError::Io(io_err) => match io_err.kind() {
|
||
std::io::ErrorKind::TimedOut => "timeout",
|
||
_ => "io",
|
||
},
|
||
_ => "other",
|
||
}
|
||
}
|
||
|
||
pub fn is_err_invalid_upload_id(err: &Error) -> bool {
|
||
matches!(err, &StorageError::InvalidUploadID(_, _, _))
|
||
}
|
||
|
||
pub fn is_err_bucket_not_found(err: &Error) -> bool {
|
||
matches!(err, &StorageError::VolumeNotFound)
|
||
| matches!(err, &StorageError::DiskNotFound)
|
||
| matches!(err, &StorageError::BucketNotFound(_))
|
||
}
|
||
|
||
pub fn is_err_data_movement_overwrite(err: &Error) -> bool {
|
||
matches!(err, &StorageError::DataMovementOverwriteErr(_, _, _))
|
||
}
|
||
|
||
pub fn is_err_decommission_running(err: &Error) -> bool {
|
||
matches!(err, &StorageError::DecommissionAlreadyRunning)
|
||
}
|
||
|
||
pub fn is_err_rebalance_running(err: &Error) -> bool {
|
||
matches!(err, &StorageError::RebalanceAlreadyRunning)
|
||
}
|
||
|
||
pub fn is_err_operation_canceled(err: &Error) -> bool {
|
||
matches!(err, &StorageError::OperationCanceled)
|
||
}
|
||
|
||
pub fn is_err_not_initialized(err: &Error) -> bool {
|
||
err.to_string().contains("errServerNotInitialized") || err.to_string().contains("ServerNotInitialized")
|
||
}
|
||
|
||
pub fn is_err_io(err: &Error) -> bool {
|
||
matches!(err, &StorageError::Io(_))
|
||
}
|
||
|
||
pub fn is_all_not_found(errs: &[Option<Error>]) -> bool {
|
||
for err in errs.iter() {
|
||
if let Some(err) = err {
|
||
if is_err_object_not_found(err) || is_err_version_not_found(err) || is_err_bucket_not_found(err) {
|
||
continue;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
!errs.is_empty()
|
||
}
|
||
|
||
pub fn is_all_volume_not_found(errs: &[Option<Error>]) -> bool {
|
||
for err in errs.iter() {
|
||
if let Some(err) = err {
|
||
if is_err_bucket_not_found(err) {
|
||
continue;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
!errs.is_empty()
|
||
}
|
||
|
||
pub fn to_object_err(err: Error, params: Vec<&str>) -> Error {
|
||
match err {
|
||
StorageError::DiskFull => StorageError::StorageFull,
|
||
|
||
StorageError::FileNotFound => {
|
||
let bucket = params.first().cloned().unwrap_or_default().to_owned();
|
||
let object = params.get(1).cloned().map(decode_dir_object).unwrap_or_default();
|
||
StorageError::ObjectNotFound(bucket, object)
|
||
}
|
||
StorageError::FileVersionNotFound => {
|
||
let bucket = params.first().cloned().unwrap_or_default().to_owned();
|
||
let object = params.get(1).cloned().map(decode_dir_object).unwrap_or_default();
|
||
let version = params.get(2).cloned().unwrap_or_default().to_owned();
|
||
|
||
StorageError::VersionNotFound(bucket, object, version)
|
||
}
|
||
StorageError::TooManyOpenFiles => StorageError::SlowDown,
|
||
StorageError::FileNameTooLong => {
|
||
let bucket = params.first().cloned().unwrap_or_default().to_owned();
|
||
let object = params.get(1).cloned().map(decode_dir_object).unwrap_or_default();
|
||
|
||
StorageError::ObjectNameInvalid(bucket, object)
|
||
}
|
||
StorageError::VolumeExists => {
|
||
let bucket = params.first().cloned().unwrap_or_default().to_owned();
|
||
StorageError::BucketExists(bucket)
|
||
}
|
||
StorageError::IsNotRegular => {
|
||
let bucket = params.first().cloned().unwrap_or_default().to_owned();
|
||
let object = params.get(1).cloned().map(decode_dir_object).unwrap_or_default();
|
||
|
||
StorageError::ObjectExistsAsDirectory(bucket, object)
|
||
}
|
||
|
||
StorageError::VolumeNotFound => {
|
||
let bucket = params.first().cloned().unwrap_or_default().to_owned();
|
||
StorageError::BucketNotFound(bucket)
|
||
}
|
||
StorageError::VolumeNotEmpty => {
|
||
let bucket = params.first().cloned().unwrap_or_default().to_owned();
|
||
StorageError::BucketNotEmpty(bucket)
|
||
}
|
||
|
||
StorageError::FileAccessDenied => {
|
||
let bucket = params.first().cloned().unwrap_or_default().to_owned();
|
||
let object = params.get(1).cloned().map(decode_dir_object).unwrap_or_default();
|
||
|
||
StorageError::PrefixAccessDenied(bucket, object)
|
||
}
|
||
|
||
StorageError::ErasureReadQuorum => {
|
||
let bucket = params.first().cloned().unwrap_or_default().to_owned();
|
||
let object = params.get(1).cloned().map(decode_dir_object).unwrap_or_default();
|
||
StorageError::InsufficientReadQuorum(bucket, object)
|
||
}
|
||
StorageError::ErasureWriteQuorum => {
|
||
let bucket = params.first().cloned().unwrap_or_default().to_owned();
|
||
let object = params.get(1).cloned().map(decode_dir_object).unwrap_or_default();
|
||
StorageError::InsufficientWriteQuorum(bucket, object)
|
||
}
|
||
|
||
_ => err,
|
||
}
|
||
}
|
||
|
||
pub fn is_network_or_host_down(err: &str, _expect_timeouts: bool) -> bool {
|
||
err.contains("Connection closed by foreign host")
|
||
|| err.contains("TLS handshake timeout")
|
||
|| err.contains("i/o timeout")
|
||
|| err.contains("connection timed out")
|
||
|| err.contains("connection reset by peer")
|
||
|| err.contains("broken pipe")
|
||
|| err.to_lowercase().contains("503 service unavailable")
|
||
|| err.contains("use of closed network connection")
|
||
|| err.contains("An existing connection was forcibly closed by the remote host")
|
||
|| err.contains("client error (Connect)")
|
||
}
|
||
|
||
#[derive(Debug, Default, PartialEq, Eq)]
|
||
pub struct GenericError {
|
||
pub bucket: String,
|
||
pub object: String,
|
||
pub version_id: String,
|
||
//pub err: Error,
|
||
}
|
||
|
||
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
||
pub enum ObjectApiError {
|
||
#[error("Operation timed out")]
|
||
OperationTimedOut,
|
||
|
||
#[error("etag of the object has changed")]
|
||
InvalidETag,
|
||
|
||
#[error("BackendDown")]
|
||
BackendDown(String),
|
||
|
||
#[error("Unsupported headers in Metadata")]
|
||
UnsupportedMetadata,
|
||
|
||
#[error("Method not allowed: {}/{}", .0.bucket, .0.object)]
|
||
MethodNotAllowed(GenericError),
|
||
|
||
#[error("The operation is not valid for the current state of the object {}/{}({})", .0.bucket, .0.object, .0.version_id)]
|
||
InvalidObjectState(GenericError),
|
||
}
|
||
|
||
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
||
#[error("{}", .message)]
|
||
pub struct ErrorResponse {
|
||
pub code: S3ErrorCode,
|
||
pub message: String,
|
||
pub key: Option<String>,
|
||
pub bucket_name: Option<String>,
|
||
pub region: Option<String>,
|
||
pub request_id: Option<String>,
|
||
pub host_id: String,
|
||
}
|
||
|
||
pub fn error_resp_to_object_err(err: ErrorResponse, params: Vec<&str>) -> std::io::Error {
|
||
let mut bucket = "";
|
||
let mut object = "";
|
||
let mut version_id = "";
|
||
if !params.is_empty() {
|
||
bucket = params[0];
|
||
}
|
||
if params.len() >= 2 {
|
||
object = params[1];
|
||
}
|
||
if params.len() >= 3 {
|
||
version_id = params[2];
|
||
}
|
||
|
||
if is_network_or_host_down(&err.to_string(), false) {
|
||
return std::io::Error::other(ObjectApiError::BackendDown(format!("{err}")));
|
||
}
|
||
|
||
let err_ = std::io::Error::other(err.to_string());
|
||
let r_err = err;
|
||
let err;
|
||
let bucket = bucket.to_string();
|
||
let object = object.to_string();
|
||
let version_id = version_id.to_string();
|
||
|
||
match r_err.code {
|
||
S3ErrorCode::BucketNotEmpty => {
|
||
err = std::io::Error::other(StorageError::BucketNotEmpty("".to_string()).to_string());
|
||
}
|
||
S3ErrorCode::InvalidBucketName => {
|
||
err = std::io::Error::other(StorageError::BucketNameInvalid(bucket));
|
||
}
|
||
S3ErrorCode::InvalidPart => {
|
||
err = std::io::Error::other(StorageError::InvalidPart(0, bucket, object /* , version_id */));
|
||
}
|
||
S3ErrorCode::NoSuchBucket => {
|
||
err = std::io::Error::other(StorageError::BucketNotFound(bucket));
|
||
}
|
||
S3ErrorCode::NoSuchKey => {
|
||
if !object.is_empty() {
|
||
err = std::io::Error::other(StorageError::ObjectNotFound(bucket, object));
|
||
} else {
|
||
err = std::io::Error::other(StorageError::BucketNotFound(bucket));
|
||
}
|
||
}
|
||
S3ErrorCode::NoSuchVersion => {
|
||
if !object.is_empty() {
|
||
err = std::io::Error::other(StorageError::ObjectNotFound(bucket, object)); //, version_id);
|
||
} else {
|
||
err = std::io::Error::other(StorageError::BucketNotFound(bucket));
|
||
}
|
||
}
|
||
S3ErrorCode::AccessDenied => {
|
||
err = std::io::Error::other(StorageError::PrefixAccessDenied(bucket, object));
|
||
}
|
||
S3ErrorCode::NoSuchUpload => {
|
||
err = std::io::Error::other(StorageError::InvalidUploadID(bucket, object, version_id));
|
||
}
|
||
_ => {
|
||
err = err_;
|
||
}
|
||
}
|
||
|
||
err
|
||
}
|
||
|
||
pub fn storage_to_object_err(err: Error, params: Vec<&str>) -> S3Error {
|
||
let storage_err = &err;
|
||
let mut bucket: String = "".to_string();
|
||
let mut object: String = "".to_string();
|
||
if !params.is_empty() {
|
||
bucket = params[0].to_string();
|
||
}
|
||
if params.len() >= 2 {
|
||
object = decode_dir_object(params[1]);
|
||
}
|
||
match storage_err {
|
||
StorageError::MethodNotAllowed => S3Error::with_message(
|
||
S3ErrorCode::MethodNotAllowed,
|
||
ObjectApiError::MethodNotAllowed(GenericError {
|
||
bucket,
|
||
object,
|
||
..Default::default()
|
||
})
|
||
.to_string(),
|
||
),
|
||
_ => s3s::S3Error::with_message(S3ErrorCode::Custom("err".into()), err.to_string()),
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use std::io::{Error as IoError, ErrorKind};
|
||
|
||
#[test]
|
||
fn test_storage_error_to_u32() {
|
||
// Test Io error uses 0x01
|
||
let io_error = StorageError::Io(IoError::other("test"));
|
||
assert_eq!(io_error.to_u32(), 0x01);
|
||
|
||
// Test other errors have correct codes
|
||
assert_eq!(StorageError::FaultyDisk.to_u32(), 0x02);
|
||
assert_eq!(StorageError::DiskFull.to_u32(), 0x03);
|
||
assert_eq!(StorageError::VolumeNotFound.to_u32(), 0x04);
|
||
assert_eq!(StorageError::VolumeExists.to_u32(), 0x05);
|
||
assert_eq!(StorageError::FileNotFound.to_u32(), 0x06);
|
||
assert_eq!(StorageError::DecommissionAlreadyRunning.to_u32(), 0x30);
|
||
assert_eq!(StorageError::RebalanceAlreadyRunning.to_u32(), 0x40);
|
||
assert_eq!(StorageError::OperationCanceled.to_u32(), 0x41);
|
||
assert_eq!(
|
||
StorageError::NamespaceLockQuorumUnavailable {
|
||
mode: "write",
|
||
bucket: "bucket".into(),
|
||
object: "object".into(),
|
||
required: 3,
|
||
achieved: 2,
|
||
}
|
||
.to_u32(),
|
||
0x42
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_storage_error_from_u32() {
|
||
// Test Io error conversion
|
||
assert!(matches!(StorageError::from_u32(0x01), Some(StorageError::Io(_))));
|
||
|
||
// Test other error conversions
|
||
assert!(matches!(StorageError::from_u32(0x02), Some(StorageError::FaultyDisk)));
|
||
assert!(matches!(StorageError::from_u32(0x03), Some(StorageError::DiskFull)));
|
||
assert!(matches!(StorageError::from_u32(0x04), Some(StorageError::VolumeNotFound)));
|
||
assert!(matches!(StorageError::from_u32(0x30), Some(StorageError::DecommissionAlreadyRunning)));
|
||
assert!(matches!(StorageError::from_u32(0x40), Some(StorageError::RebalanceAlreadyRunning)));
|
||
assert!(matches!(StorageError::from_u32(0x41), Some(StorageError::OperationCanceled)));
|
||
assert!(matches!(
|
||
StorageError::from_u32(0x42),
|
||
Some(StorageError::NamespaceLockQuorumUnavailable { .. })
|
||
));
|
||
|
||
// Test invalid code returns None
|
||
assert!(StorageError::from_u32(0xFF).is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn test_storage_error_partial_eq() {
|
||
// Test IO error comparison
|
||
let io1 = StorageError::Io(IoError::new(ErrorKind::NotFound, "file not found"));
|
||
let io2 = StorageError::Io(IoError::new(ErrorKind::NotFound, "file not found"));
|
||
let io3 = StorageError::Io(IoError::new(ErrorKind::PermissionDenied, "access denied"));
|
||
|
||
assert_eq!(io1, io2);
|
||
assert_ne!(io1, io3);
|
||
|
||
// Test non-IO error comparison
|
||
let bucket1 = StorageError::BucketExists("test".to_string());
|
||
let bucket2 = StorageError::BucketExists("different".to_string());
|
||
assert_eq!(bucket1, bucket2); // Same error type, different parameters
|
||
|
||
let disk_error = StorageError::DiskFull;
|
||
assert_ne!(bucket1, disk_error);
|
||
}
|
||
|
||
#[test]
|
||
fn test_error_running_state_helpers() {
|
||
assert!(is_err_decommission_running(&StorageError::DecommissionAlreadyRunning));
|
||
assert!(!is_err_decommission_running(&StorageError::RebalanceAlreadyRunning));
|
||
|
||
assert!(is_err_rebalance_running(&StorageError::RebalanceAlreadyRunning));
|
||
assert!(!is_err_rebalance_running(&StorageError::DecommissionAlreadyRunning));
|
||
assert!(is_err_operation_canceled(&StorageError::OperationCanceled));
|
||
assert!(!is_err_operation_canceled(&StorageError::RebalanceAlreadyRunning));
|
||
assert!(is_err_not_initialized(&StorageError::other("errServerNotInitialized")));
|
||
assert!(is_err_not_initialized(&StorageError::other("ServerNotInitialized")));
|
||
assert!(!is_err_not_initialized(&StorageError::DecommissionAlreadyRunning));
|
||
}
|
||
|
||
#[test]
|
||
fn test_classify_system_path_failure_reason() {
|
||
assert_eq!(classify_system_path_failure_reason(&StorageError::ConfigNotFound), "config_not_found");
|
||
assert_eq!(classify_system_path_failure_reason(&StorageError::ErasureReadQuorum), "read_quorum");
|
||
assert_eq!(
|
||
classify_system_path_failure_reason(&StorageError::InsufficientReadQuorum(
|
||
"bucket".to_string(),
|
||
"object".to_string()
|
||
)),
|
||
"read_quorum"
|
||
);
|
||
assert_eq!(
|
||
classify_system_path_failure_reason(&StorageError::Io(IoError::new(ErrorKind::TimedOut, "probe"))),
|
||
"timeout"
|
||
);
|
||
assert_eq!(
|
||
classify_system_path_failure_reason(&StorageError::Io(IoError::new(ErrorKind::PermissionDenied, "probe"))),
|
||
"io"
|
||
);
|
||
assert_eq!(classify_system_path_failure_reason(&StorageError::DiskFull), "other");
|
||
}
|
||
|
||
#[test]
|
||
fn test_storage_error_from_disk_error() {
|
||
// Test conversion from DiskError
|
||
let disk_io = DiskError::Io(IoError::other("disk io error"));
|
||
let storage_error: StorageError = disk_io.into();
|
||
assert!(matches!(storage_error, StorageError::Io(_)));
|
||
|
||
let disk_full = DiskError::DiskFull;
|
||
let storage_error: StorageError = disk_full.into();
|
||
assert_eq!(storage_error, StorageError::DiskFull);
|
||
|
||
let file_not_found = DiskError::FileNotFound;
|
||
let storage_error: StorageError = file_not_found.into();
|
||
assert_eq!(storage_error, StorageError::FileNotFound);
|
||
}
|
||
|
||
#[test]
|
||
fn test_storage_error_from_io_error() {
|
||
// Test direct IO error conversion
|
||
let io_error = IoError::new(ErrorKind::NotFound, "test error");
|
||
let storage_error: StorageError = io_error.into();
|
||
assert!(matches!(storage_error, StorageError::Io(_)));
|
||
|
||
// Test IO error containing DiskError
|
||
let disk_error = DiskError::DiskFull;
|
||
let io_with_disk_error = IoError::other(disk_error);
|
||
let storage_error: StorageError = io_with_disk_error.into();
|
||
assert_eq!(storage_error, StorageError::DiskFull);
|
||
|
||
// Test IO error containing StorageError
|
||
let original_storage_error = StorageError::BucketNotFound("test".to_string());
|
||
let io_with_storage_error = IoError::other(original_storage_error.clone());
|
||
let recovered_storage_error: StorageError = io_with_storage_error.into();
|
||
assert_eq!(recovered_storage_error, original_storage_error);
|
||
}
|
||
|
||
#[test]
|
||
fn test_storage_error_to_io_error() {
|
||
// Test conversion to IO error
|
||
let storage_error = StorageError::DiskFull;
|
||
let io_error: IoError = storage_error.into();
|
||
assert_eq!(io_error.kind(), ErrorKind::Other);
|
||
|
||
// Test IO error round trip
|
||
let original_io = IoError::new(ErrorKind::PermissionDenied, "access denied");
|
||
let storage_error = StorageError::Io(original_io);
|
||
let converted_io: IoError = storage_error.into();
|
||
assert_eq!(converted_io.kind(), ErrorKind::PermissionDenied);
|
||
}
|
||
|
||
#[test]
|
||
fn test_bucket_and_object_errors() {
|
||
let bucket_not_found = StorageError::BucketNotFound("mybucket".to_string());
|
||
let object_not_found = StorageError::ObjectNotFound("mybucket".to_string(), "myobject".to_string());
|
||
let version_not_found = StorageError::VersionNotFound("mybucket".to_string(), "myobject".to_string(), "v1".to_string());
|
||
|
||
// Test different error codes
|
||
assert_ne!(bucket_not_found.to_u32(), object_not_found.to_u32());
|
||
assert_ne!(object_not_found.to_u32(), version_not_found.to_u32());
|
||
|
||
// Test error messages contain expected information
|
||
assert!(bucket_not_found.to_string().contains("mybucket"));
|
||
assert!(object_not_found.to_string().contains("mybucket"));
|
||
assert!(object_not_found.to_string().contains("myobject"));
|
||
assert!(version_not_found.to_string().contains("v1"));
|
||
}
|
||
|
||
#[test]
|
||
fn test_upload_id_errors() {
|
||
let invalid_upload = StorageError::InvalidUploadID("bucket".to_string(), "object".to_string(), "uploadid".to_string());
|
||
let malformed_upload = StorageError::MalformedUploadID("badid".to_string());
|
||
|
||
assert_ne!(invalid_upload.to_u32(), malformed_upload.to_u32());
|
||
assert!(invalid_upload.to_string().contains("uploadid"));
|
||
assert!(malformed_upload.to_string().contains("badid"));
|
||
}
|
||
|
||
#[test]
|
||
fn test_round_trip_conversion() {
|
||
// Test that to_u32 and from_u32 are consistent for all variants
|
||
let test_errors = vec![
|
||
StorageError::FaultyDisk,
|
||
StorageError::DiskFull,
|
||
StorageError::VolumeNotFound,
|
||
StorageError::BucketExists("test".to_string()),
|
||
StorageError::ObjectNotFound("bucket".to_string(), "object".to_string()),
|
||
StorageError::DecommissionAlreadyRunning,
|
||
StorageError::RebalanceAlreadyRunning,
|
||
StorageError::OperationCanceled,
|
||
];
|
||
|
||
for original_error in test_errors {
|
||
let code = original_error.to_u32();
|
||
if let Some(recovered_error) = StorageError::from_u32(code) {
|
||
// For errors with parameters, we only check the variant type
|
||
assert_eq!(std::mem::discriminant(&original_error), std::mem::discriminant(&recovered_error));
|
||
} else {
|
||
panic!("Failed to recover error from code: {code:#x}");
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_storage_error_io_roundtrip() {
|
||
// Test StorageError -> std::io::Error -> StorageError roundtrip conversion
|
||
let original_storage_errors = vec![
|
||
StorageError::FileNotFound,
|
||
StorageError::VolumeNotFound,
|
||
StorageError::DiskFull,
|
||
StorageError::FileCorrupt,
|
||
StorageError::MethodNotAllowed,
|
||
StorageError::BucketExists("test-bucket".to_string()),
|
||
StorageError::ObjectNotFound("bucket".to_string(), "object".to_string()),
|
||
];
|
||
|
||
for original_error in original_storage_errors {
|
||
// Convert to io::Error and back
|
||
let io_error: std::io::Error = original_error.clone().into();
|
||
let recovered_error: StorageError = io_error.into();
|
||
|
||
// Check that conversion preserves the essential error information
|
||
match &original_error {
|
||
StorageError::Io(_) => {
|
||
// Io errors should maintain their inner structure
|
||
assert!(matches!(recovered_error, StorageError::Io(_)));
|
||
}
|
||
_ => {
|
||
// Other errors should be recoverable via downcast or match to equivalent type
|
||
assert_eq!(original_error.to_u32(), recovered_error.to_u32());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_io_error_with_storage_error_inside() {
|
||
// Test that io::Error containing StorageError can be properly converted back
|
||
let original_storage_error = StorageError::FileNotFound;
|
||
let io_with_storage_error = std::io::Error::other(original_storage_error.clone());
|
||
|
||
// Convert io::Error back to StorageError
|
||
let recovered_storage_error: StorageError = io_with_storage_error.into();
|
||
assert_eq!(original_storage_error, recovered_storage_error);
|
||
}
|
||
|
||
#[test]
|
||
fn test_io_error_with_disk_error_inside() {
|
||
// Test io::Error containing DiskError -> StorageError conversion
|
||
let original_disk_error = DiskError::FileNotFound;
|
||
let io_with_disk_error = std::io::Error::other(original_disk_error);
|
||
|
||
// Convert io::Error to StorageError
|
||
let storage_error: StorageError = io_with_disk_error.into();
|
||
assert_eq!(storage_error, StorageError::FileNotFound);
|
||
}
|
||
|
||
#[test]
|
||
fn test_nested_error_conversion_chain() {
|
||
// Test complex conversion chain: DiskError -> StorageError -> io::Error -> StorageError
|
||
let original_disk_error = DiskError::DiskFull;
|
||
let storage_error1: StorageError = original_disk_error.into();
|
||
let io_error: std::io::Error = storage_error1.into();
|
||
let storage_error2: StorageError = io_error.into();
|
||
|
||
assert_eq!(storage_error2, StorageError::DiskFull);
|
||
}
|
||
|
||
#[test]
|
||
fn test_storage_error_different_io_kinds() {
|
||
use std::io::ErrorKind;
|
||
|
||
let test_cases = vec![
|
||
(ErrorKind::NotFound, "not found"),
|
||
(ErrorKind::PermissionDenied, "permission denied"),
|
||
(ErrorKind::ConnectionRefused, "connection refused"),
|
||
(ErrorKind::TimedOut, "timed out"),
|
||
(ErrorKind::InvalidInput, "invalid input"),
|
||
(ErrorKind::BrokenPipe, "broken pipe"),
|
||
];
|
||
|
||
for (kind, message) in test_cases {
|
||
let io_error = std::io::Error::new(kind, message);
|
||
let storage_error: StorageError = io_error.into();
|
||
|
||
// Should become StorageError::Io with the same kind and message
|
||
match storage_error {
|
||
StorageError::Io(inner_io) => {
|
||
assert_eq!(inner_io.kind(), kind);
|
||
assert!(inner_io.to_string().contains(message));
|
||
}
|
||
_ => panic!("Expected StorageError::Io variant for kind: {kind:?}"),
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_storage_error_to_io_error_preserves_information() {
|
||
let test_cases = vec![
|
||
StorageError::FileNotFound,
|
||
StorageError::VolumeNotFound,
|
||
StorageError::DiskFull,
|
||
StorageError::FileCorrupt,
|
||
StorageError::MethodNotAllowed,
|
||
StorageError::StorageFull,
|
||
StorageError::SlowDown,
|
||
StorageError::BucketExists("test-bucket".to_string()),
|
||
];
|
||
|
||
for storage_error in test_cases {
|
||
let io_error: std::io::Error = storage_error.clone().into();
|
||
|
||
// Error message should be preserved
|
||
assert!(io_error.to_string().contains(&storage_error.to_string()));
|
||
|
||
// Should be able to downcast back to StorageError
|
||
let recovered_error = io_error.downcast::<StorageError>();
|
||
assert!(recovered_error.is_ok());
|
||
assert_eq!(recovered_error.unwrap(), storage_error);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_storage_error_io_variant_preservation() {
|
||
// Test StorageError::Io variant preserves original io::Error
|
||
let original_io = std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "unexpected eof");
|
||
let storage_error = StorageError::Io(original_io);
|
||
|
||
let converted_io: std::io::Error = storage_error.into();
|
||
assert_eq!(converted_io.kind(), std::io::ErrorKind::UnexpectedEof);
|
||
assert!(converted_io.to_string().contains("unexpected eof"));
|
||
}
|
||
|
||
#[test]
|
||
fn test_from_filemeta_error_conversions() {
|
||
// Test conversions from rustfs_filemeta::Error
|
||
use rustfs_filemeta::Error as FilemetaError;
|
||
|
||
let filemeta_errors = vec![
|
||
(FilemetaError::FileNotFound, StorageError::FileNotFound),
|
||
(FilemetaError::FileVersionNotFound, StorageError::FileVersionNotFound),
|
||
(FilemetaError::FileCorrupt, StorageError::FileCorrupt),
|
||
(FilemetaError::MethodNotAllowed, StorageError::MethodNotAllowed),
|
||
(FilemetaError::VolumeNotFound, StorageError::VolumeNotFound),
|
||
(FilemetaError::DoneForNow, StorageError::DoneForNow),
|
||
(FilemetaError::Unexpected, StorageError::Unexpected),
|
||
];
|
||
|
||
for (filemeta_error, expected_storage_error) in filemeta_errors {
|
||
let converted_storage_error: StorageError = filemeta_error.into();
|
||
assert_eq!(converted_storage_error, expected_storage_error);
|
||
|
||
// Test reverse conversion
|
||
let converted_back: rustfs_filemeta::Error = converted_storage_error.into();
|
||
assert_eq!(converted_back, expected_storage_error.into());
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_error_message_consistency() {
|
||
let storage_errors = vec![
|
||
StorageError::BucketNotFound("test-bucket".to_string()),
|
||
StorageError::ObjectNotFound("bucket".to_string(), "object".to_string()),
|
||
StorageError::VersionNotFound("bucket".to_string(), "object".to_string(), "v1".to_string()),
|
||
StorageError::InvalidUploadID("bucket".to_string(), "object".to_string(), "upload123".to_string()),
|
||
];
|
||
|
||
for storage_error in storage_errors {
|
||
let original_message = storage_error.to_string();
|
||
let io_error: std::io::Error = storage_error.clone().into();
|
||
|
||
// The io::Error should contain the original error message or info
|
||
assert!(io_error.to_string().contains(&original_message));
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_error_equality_after_conversion() {
|
||
let storage_errors = vec![
|
||
StorageError::FileNotFound,
|
||
StorageError::VolumeNotFound,
|
||
StorageError::DiskFull,
|
||
StorageError::MethodNotAllowed,
|
||
];
|
||
|
||
for original_error in storage_errors {
|
||
// Test that equality is preserved through conversion
|
||
let io_error: std::io::Error = original_error.clone().into();
|
||
let recovered_error: StorageError = io_error.into();
|
||
|
||
assert_eq!(original_error, recovered_error);
|
||
}
|
||
}
|
||
}
|