Files
rustfs/crates/ecstore/src/error/mod.rs
T
Zhengchao An 7f5873dac8 fix(ecstore): resolve erasure parity per pool (#4801) (#5015)
* fix(ecstore): add fallible erasure construction

(cherry picked from commit bd148b20f7)

* fix(ecstore): resolve storage parity per pool

(cherry picked from commit c05c2cb24b)

* fix(ecstore): keep carved per-pool parity core self-contained on main

Fixups so the cherry-picked fallible-erasure + per-pool-parity core builds standalone on current main without the excluded scope-creep commits:
- runtime/sources.rs: re-add backend_storage_class_parities (removed by the per-pool commit; its rebalance caller was updated in an unrelated reporting commit that was left out). Reimplemented over the snapshot API, behavior-identical.
- config/mod.rs: rename the storage-class publish test module (main independently added a mod tests, so the cherry-pick collided).
- rustfs storage_api.rs + startup_storage.rs: route the storage-class ENV consts through the startup storage facade and use a local const for the erasure-set-drive-count env name, satisfying the layer/facade guardrail (main's guardrail is stricter than when the core was authored).

* fix(ecstore): use struct-init in erasure test helper to satisfy clippy field_reassign_with_default

The cherry-picked fallible-erasure commit's `erasure_with_invalid_dimensions` test helper built `Erasure` via `default()` then reassigned fields, which trips `clippy::field_reassign_with_default` under `-D warnings` (only surfaced by `--all-targets`, which lints test code). #4977 fixed this in a later commit that was not part of the carved core. Use struct-init with `..Default::default()`, matching #4977's final form.

* fix(rustfs): gate the test-only storage-class ENV facade re-export behind cfg(test)

The ENV constants (INLINE_BLOCK_ENV/OPTIMIZE_ENV/RRS_ENV/STANDARD_ENV) re-exported through the startup storage facade are only consumed by a #[cfg(test)] test in startup_storage.rs, so in a non-test lib build the re-export is unused and trips -D unused-imports under clippy --all-targets. Gate it with #[cfg(test)], matching #4977's final form.

---------

Co-authored-by: cxymds <cxymds@gmail.com>
2026-07-19 03:06:46 +00:00

1782 lines
81 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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.
// #730: error taxonomy still exposes compatibility variants while callers move to contracts.
#![allow(dead_code)]
use crate::bucket::error::BucketMetadataError;
use crate::disk::error::DiskError;
use crate::storage_api_contracts::{error::StorageErrorCode, range::HTTPRangeError};
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("maximum versions exceeded, please delete few versions to proceed")]
MaxVersionsExceeded,
#[error("inconsistent drive found")]
InconsistentDisk,
#[error("drive does not support O_DIRECT")]
UnsupportedDisk,
#[error("disk not a dir")]
DiskNotDir,
#[error("drive still did not complete the request")]
DiskOngoingReq,
#[error("path not found")]
PathNotFound,
#[error("bit-rot hash algorithm is invalid")]
BitrotHashAlgoInvalid,
#[error("Rename across devices not allowed, please fix your backend configuration")]
CrossDeviceLink,
#[error("less data available than what was requested")]
LessData,
#[error("more data was sent than what was advertised")]
MoreData,
#[error("outdated XL meta")]
OutdatedXLMeta,
#[error("part missing or corrupt")]
PartMissingOrCorrupt,
#[error("short write")]
ShortWrite,
#[error("source stalled")]
SourceStalled,
#[error("timeout")]
Timeout,
#[error("invalid path")]
InvalidPath,
#[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(#[source] std::io::Error),
#[error("Lock error: {0}")]
Lock(#[from] rustfs_lock::LockError),
}
impl From<crate::erasure::coding::ErasureConstructionError> for StorageError {
fn from(error: crate::erasure::coding::ErasureConstructionError) -> Self {
Self::Io(error.into_io_error())
}
}
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<HTTPRangeError> for StorageError {
fn from(err: HTTPRangeError) -> Self {
match err {
HTTPRangeError::InvalidRangeSpec(message) => Self::InvalidRangeSpec(message),
}
}
}
impl From<DiskError> for StorageError {
fn from(e: DiskError) -> Self {
match e {
DiskError::Io(io_error) => StorageError::Io(io_error),
DiskError::MaxVersionsExceeded => StorageError::MaxVersionsExceeded,
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,
DiskError::ShortWrite => StorageError::ShortWrite,
DiskError::SourceStalled => StorageError::SourceStalled,
DiskError::Timeout => StorageError::Timeout,
DiskError::InvalidPath => StorageError::InvalidPath,
}
}
}
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::MaxVersionsExceeded => DiskError::MaxVersionsExceeded,
StorageError::InconsistentDisk => DiskError::InconsistentDisk,
StorageError::UnsupportedDisk => DiskError::UnsupportedDisk,
StorageError::DiskNotDir => DiskError::DiskNotDir,
StorageError::DiskOngoingReq => DiskError::DiskOngoingReq,
StorageError::PathNotFound => DiskError::PathNotFound,
StorageError::BitrotHashAlgoInvalid => DiskError::BitrotHashAlgoInvalid,
StorageError::CrossDeviceLink => DiskError::CrossDeviceLink,
StorageError::LessData => DiskError::LessData,
StorageError::MoreData => DiskError::MoreData,
StorageError::OutdatedXLMeta => DiskError::OutdatedXLMeta,
StorageError::PartMissingOrCorrupt => DiskError::PartMissingOrCorrupt,
StorageError::ShortWrite => DiskError::ShortWrite,
StorageError::SourceStalled => DiskError::SourceStalled,
StorageError::Timeout => DiskError::Timeout,
StorageError::InvalidPath => DiskError::InvalidPath,
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::MaxVersionsExceeded => StorageError::MaxVersionsExceeded,
StorageError::InconsistentDisk => StorageError::InconsistentDisk,
StorageError::UnsupportedDisk => StorageError::UnsupportedDisk,
StorageError::DiskNotDir => StorageError::DiskNotDir,
StorageError::DiskOngoingReq => StorageError::DiskOngoingReq,
StorageError::PathNotFound => StorageError::PathNotFound,
StorageError::BitrotHashAlgoInvalid => StorageError::BitrotHashAlgoInvalid,
StorageError::CrossDeviceLink => StorageError::CrossDeviceLink,
StorageError::LessData => StorageError::LessData,
StorageError::MoreData => StorageError::MoreData,
StorageError::OutdatedXLMeta => StorageError::OutdatedXLMeta,
StorageError::PartMissingOrCorrupt => StorageError::PartMissingOrCorrupt,
StorageError::ShortWrite => StorageError::ShortWrite,
StorageError::SourceStalled => StorageError::SourceStalled,
StorageError::Timeout => StorageError::Timeout,
StorageError::InvalidPath => StorageError::InvalidPath,
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 {
fn code(&self) -> StorageErrorCode {
match self {
StorageError::Io(_) => StorageErrorCode::Io,
StorageError::FaultyDisk => StorageErrorCode::FaultyDisk,
StorageError::DiskFull => StorageErrorCode::DiskFull,
StorageError::VolumeNotFound => StorageErrorCode::VolumeNotFound,
StorageError::VolumeExists => StorageErrorCode::VolumeExists,
StorageError::FileNotFound => StorageErrorCode::FileNotFound,
StorageError::FileVersionNotFound => StorageErrorCode::FileVersionNotFound,
StorageError::FileNameTooLong => StorageErrorCode::FileNameTooLong,
StorageError::FileAccessDenied => StorageErrorCode::FileAccessDenied,
StorageError::FileCorrupt => StorageErrorCode::FileCorrupt,
StorageError::IsNotRegular => StorageErrorCode::IsNotRegular,
StorageError::VolumeNotEmpty => StorageErrorCode::VolumeNotEmpty,
StorageError::VolumeAccessDenied => StorageErrorCode::VolumeAccessDenied,
StorageError::CorruptedFormat => StorageErrorCode::CorruptedFormat,
StorageError::CorruptedBackend => StorageErrorCode::CorruptedBackend,
StorageError::UnformattedDisk => StorageErrorCode::UnformattedDisk,
StorageError::DiskNotFound => StorageErrorCode::DiskNotFound,
StorageError::MaxVersionsExceeded => StorageErrorCode::MaxVersionsExceeded,
StorageError::InconsistentDisk => StorageErrorCode::InconsistentDisk,
StorageError::UnsupportedDisk => StorageErrorCode::UnsupportedDisk,
StorageError::DiskNotDir => StorageErrorCode::DiskNotDir,
StorageError::DiskOngoingReq => StorageErrorCode::DiskOngoingReq,
StorageError::PathNotFound => StorageErrorCode::PathNotFound,
StorageError::BitrotHashAlgoInvalid => StorageErrorCode::BitrotHashAlgoInvalid,
StorageError::CrossDeviceLink => StorageErrorCode::CrossDeviceLink,
StorageError::LessData => StorageErrorCode::LessData,
StorageError::MoreData => StorageErrorCode::MoreData,
StorageError::OutdatedXLMeta => StorageErrorCode::OutdatedXLMeta,
StorageError::PartMissingOrCorrupt => StorageErrorCode::PartMissingOrCorrupt,
StorageError::ShortWrite => StorageErrorCode::ShortWrite,
StorageError::SourceStalled => StorageErrorCode::SourceStalled,
StorageError::Timeout => StorageErrorCode::Timeout,
StorageError::InvalidPath => StorageErrorCode::InvalidPath,
StorageError::DriveIsRoot => StorageErrorCode::DriveIsRoot,
StorageError::FaultyRemoteDisk => StorageErrorCode::FaultyRemoteDisk,
StorageError::DiskAccessDenied => StorageErrorCode::DiskAccessDenied,
StorageError::Unexpected => StorageErrorCode::Unexpected,
StorageError::NotImplemented => StorageErrorCode::NotImplemented,
StorageError::InvalidArgument(_, _, _) => StorageErrorCode::InvalidArgument,
StorageError::MethodNotAllowed => StorageErrorCode::MethodNotAllowed,
StorageError::BucketNotFound(_) => StorageErrorCode::BucketNotFound,
StorageError::BucketNotEmpty(_) => StorageErrorCode::BucketNotEmpty,
StorageError::BucketNameInvalid(_) => StorageErrorCode::BucketNameInvalid,
StorageError::ObjectNameInvalid(_, _) => StorageErrorCode::ObjectNameInvalid,
StorageError::BucketExists(_) => StorageErrorCode::BucketExists,
StorageError::StorageFull => StorageErrorCode::StorageFull,
StorageError::SlowDown => StorageErrorCode::SlowDown,
StorageError::PrefixAccessDenied(_, _) => StorageErrorCode::PrefixAccessDenied,
StorageError::InvalidUploadIDKeyCombination(_, _) => StorageErrorCode::InvalidUploadIDKeyCombination,
StorageError::MalformedUploadID(_) => StorageErrorCode::MalformedUploadID,
StorageError::ObjectNameTooLong(_, _) => StorageErrorCode::ObjectNameTooLong,
StorageError::ObjectNamePrefixAsSlash(_, _) => StorageErrorCode::ObjectNamePrefixAsSlash,
StorageError::ObjectNotFound(_, _) => StorageErrorCode::ObjectNotFound,
StorageError::VersionNotFound(_, _, _) => StorageErrorCode::VersionNotFound,
StorageError::InvalidUploadID(_, _, _) => StorageErrorCode::InvalidUploadID,
StorageError::InvalidVersionID(_, _, _) => StorageErrorCode::InvalidVersionID,
StorageError::DataMovementOverwriteErr(_, _, _) => StorageErrorCode::DataMovementOverwriteErr,
StorageError::ObjectExistsAsDirectory(_, _) => StorageErrorCode::ObjectExistsAsDirectory,
StorageError::DecommissionNotStarted => StorageErrorCode::DecommissionNotStarted,
StorageError::InvalidPart(_, _, _) => StorageErrorCode::InvalidPart,
StorageError::DoneForNow => StorageErrorCode::DoneForNow,
StorageError::DecommissionAlreadyRunning => StorageErrorCode::DecommissionAlreadyRunning,
StorageError::RebalanceAlreadyRunning => StorageErrorCode::RebalanceAlreadyRunning,
StorageError::OperationCanceled => StorageErrorCode::OperationCanceled,
StorageError::ErasureReadQuorum => StorageErrorCode::ErasureReadQuorum,
StorageError::ErasureWriteQuorum => StorageErrorCode::ErasureWriteQuorum,
StorageError::NotFirstDisk => StorageErrorCode::NotFirstDisk,
StorageError::FirstDiskWait => StorageErrorCode::FirstDiskWait,
StorageError::ConfigNotFound => StorageErrorCode::ConfigNotFound,
StorageError::TooManyOpenFiles => StorageErrorCode::TooManyOpenFiles,
StorageError::NoHealRequired => StorageErrorCode::NoHealRequired,
StorageError::Lock(_) => StorageErrorCode::Lock,
StorageError::InsufficientReadQuorum(_, _) => StorageErrorCode::InsufficientReadQuorum,
StorageError::InsufficientWriteQuorum(_, _) => StorageErrorCode::InsufficientWriteQuorum,
StorageError::PreconditionFailed => StorageErrorCode::PreconditionFailed,
StorageError::EntityTooSmall(_, _, _) => StorageErrorCode::EntityTooSmall,
StorageError::InvalidRangeSpec(_) => StorageErrorCode::InvalidRangeSpec,
StorageError::NotModified => StorageErrorCode::NotModified,
StorageError::InvalidPartNumber(_) => StorageErrorCode::InvalidPartNumber,
StorageError::NamespaceLockQuorumUnavailable { .. } => StorageErrorCode::NamespaceLockQuorumUnavailable,
}
}
pub fn to_u32(&self) -> u32 {
self.code().as_u32()
}
pub fn from_u32(error: u32) -> Option<Self> {
match StorageErrorCode::from_u32(error)? {
StorageErrorCode::Io => Some(StorageError::Io(std::io::Error::other("Io error"))),
StorageErrorCode::FaultyDisk => Some(StorageError::FaultyDisk),
StorageErrorCode::DiskFull => Some(StorageError::DiskFull),
StorageErrorCode::VolumeNotFound => Some(StorageError::VolumeNotFound),
StorageErrorCode::VolumeExists => Some(StorageError::VolumeExists),
StorageErrorCode::FileNotFound => Some(StorageError::FileNotFound),
StorageErrorCode::FileVersionNotFound => Some(StorageError::FileVersionNotFound),
StorageErrorCode::FileNameTooLong => Some(StorageError::FileNameTooLong),
StorageErrorCode::FileAccessDenied => Some(StorageError::FileAccessDenied),
StorageErrorCode::FileCorrupt => Some(StorageError::FileCorrupt),
StorageErrorCode::IsNotRegular => Some(StorageError::IsNotRegular),
StorageErrorCode::VolumeNotEmpty => Some(StorageError::VolumeNotEmpty),
StorageErrorCode::VolumeAccessDenied => Some(StorageError::VolumeAccessDenied),
StorageErrorCode::CorruptedFormat => Some(StorageError::CorruptedFormat),
StorageErrorCode::CorruptedBackend => Some(StorageError::CorruptedBackend),
StorageErrorCode::UnformattedDisk => Some(StorageError::UnformattedDisk),
StorageErrorCode::DiskNotFound => Some(StorageError::DiskNotFound),
StorageErrorCode::MaxVersionsExceeded => Some(StorageError::MaxVersionsExceeded),
StorageErrorCode::InconsistentDisk => Some(StorageError::InconsistentDisk),
StorageErrorCode::UnsupportedDisk => Some(StorageError::UnsupportedDisk),
StorageErrorCode::DiskNotDir => Some(StorageError::DiskNotDir),
StorageErrorCode::DiskOngoingReq => Some(StorageError::DiskOngoingReq),
StorageErrorCode::PathNotFound => Some(StorageError::PathNotFound),
StorageErrorCode::BitrotHashAlgoInvalid => Some(StorageError::BitrotHashAlgoInvalid),
StorageErrorCode::CrossDeviceLink => Some(StorageError::CrossDeviceLink),
StorageErrorCode::LessData => Some(StorageError::LessData),
StorageErrorCode::MoreData => Some(StorageError::MoreData),
StorageErrorCode::OutdatedXLMeta => Some(StorageError::OutdatedXLMeta),
StorageErrorCode::PartMissingOrCorrupt => Some(StorageError::PartMissingOrCorrupt),
StorageErrorCode::ShortWrite => Some(StorageError::ShortWrite),
StorageErrorCode::SourceStalled => Some(StorageError::SourceStalled),
StorageErrorCode::Timeout => Some(StorageError::Timeout),
StorageErrorCode::InvalidPath => Some(StorageError::InvalidPath),
StorageErrorCode::DriveIsRoot => Some(StorageError::DriveIsRoot),
StorageErrorCode::FaultyRemoteDisk => Some(StorageError::FaultyRemoteDisk),
StorageErrorCode::DiskAccessDenied => Some(StorageError::DiskAccessDenied),
StorageErrorCode::Unexpected => Some(StorageError::Unexpected),
StorageErrorCode::NotImplemented => Some(StorageError::NotImplemented),
StorageErrorCode::InvalidArgument => {
Some(StorageError::InvalidArgument(Default::default(), Default::default(), Default::default()))
}
StorageErrorCode::MethodNotAllowed => Some(StorageError::MethodNotAllowed),
StorageErrorCode::BucketNotFound => Some(StorageError::BucketNotFound(Default::default())),
StorageErrorCode::BucketNotEmpty => Some(StorageError::BucketNotEmpty(Default::default())),
StorageErrorCode::BucketNameInvalid => Some(StorageError::BucketNameInvalid(Default::default())),
StorageErrorCode::ObjectNameInvalid => Some(StorageError::ObjectNameInvalid(Default::default(), Default::default())),
StorageErrorCode::BucketExists => Some(StorageError::BucketExists(Default::default())),
StorageErrorCode::StorageFull => Some(StorageError::StorageFull),
StorageErrorCode::SlowDown => Some(StorageError::SlowDown),
StorageErrorCode::PrefixAccessDenied => {
Some(StorageError::PrefixAccessDenied(Default::default(), Default::default()))
}
StorageErrorCode::InvalidUploadIDKeyCombination => {
Some(StorageError::InvalidUploadIDKeyCombination(Default::default(), Default::default()))
}
StorageErrorCode::MalformedUploadID => Some(StorageError::MalformedUploadID(Default::default())),
StorageErrorCode::ObjectNameTooLong => Some(StorageError::ObjectNameTooLong(Default::default(), Default::default())),
StorageErrorCode::ObjectNamePrefixAsSlash => {
Some(StorageError::ObjectNamePrefixAsSlash(Default::default(), Default::default()))
}
StorageErrorCode::ObjectNotFound => Some(StorageError::ObjectNotFound(Default::default(), Default::default())),
StorageErrorCode::VersionNotFound => {
Some(StorageError::VersionNotFound(Default::default(), Default::default(), Default::default()))
}
StorageErrorCode::InvalidUploadID => {
Some(StorageError::InvalidUploadID(Default::default(), Default::default(), Default::default()))
}
StorageErrorCode::InvalidVersionID => {
Some(StorageError::InvalidVersionID(Default::default(), Default::default(), Default::default()))
}
StorageErrorCode::DataMovementOverwriteErr => Some(StorageError::DataMovementOverwriteErr(
Default::default(),
Default::default(),
Default::default(),
)),
StorageErrorCode::ObjectExistsAsDirectory => {
Some(StorageError::ObjectExistsAsDirectory(Default::default(), Default::default()))
}
StorageErrorCode::DecommissionNotStarted => Some(StorageError::DecommissionNotStarted),
StorageErrorCode::InvalidPart => {
Some(StorageError::InvalidPart(Default::default(), Default::default(), Default::default()))
}
StorageErrorCode::DoneForNow => Some(StorageError::DoneForNow),
StorageErrorCode::DecommissionAlreadyRunning => Some(StorageError::DecommissionAlreadyRunning),
StorageErrorCode::RebalanceAlreadyRunning => Some(StorageError::RebalanceAlreadyRunning),
StorageErrorCode::OperationCanceled => Some(StorageError::OperationCanceled),
StorageErrorCode::ErasureReadQuorum => Some(StorageError::ErasureReadQuorum),
StorageErrorCode::ErasureWriteQuorum => Some(StorageError::ErasureWriteQuorum),
StorageErrorCode::NotFirstDisk => Some(StorageError::NotFirstDisk),
StorageErrorCode::FirstDiskWait => Some(StorageError::FirstDiskWait),
StorageErrorCode::ConfigNotFound => Some(StorageError::ConfigNotFound),
StorageErrorCode::TooManyOpenFiles => Some(StorageError::TooManyOpenFiles),
StorageErrorCode::NoHealRequired => Some(StorageError::NoHealRequired),
StorageErrorCode::Lock => {
Some(StorageError::Lock(rustfs_lock::LockError::internal("Generic lock error".to_string())))
}
StorageErrorCode::InsufficientReadQuorum => {
Some(StorageError::InsufficientReadQuorum(Default::default(), Default::default()))
}
StorageErrorCode::InsufficientWriteQuorum => {
Some(StorageError::InsufficientWriteQuorum(Default::default(), Default::default()))
}
StorageErrorCode::PreconditionFailed => Some(StorageError::PreconditionFailed),
StorageErrorCode::EntityTooSmall => {
Some(StorageError::EntityTooSmall(Default::default(), Default::default(), Default::default()))
}
StorageErrorCode::InvalidRangeSpec => Some(StorageError::InvalidRangeSpec(Default::default())),
StorageErrorCode::NotModified => Some(StorageError::NotModified),
StorageErrorCode::InvalidPartNumber => Some(StorageError::InvalidPartNumber(Default::default())),
StorageErrorCode::NamespaceLockQuorumUnavailable => Some(StorageError::NamespaceLockQuorumUnavailable {
mode: "write",
bucket: Default::default(),
object: Default::default(),
required: Default::default(),
achieved: Default::default(),
}),
}
}
}
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(_))
}
/// Strict "not found" predicate that only matches genuine object/version/volume
/// absence errors: `FileNotFound`/`VolumeNotFound`/`FileVersionNotFound`/
/// `ObjectNotFound`/`VersionNotFound`.
///
/// Unlike [`is_err_bucket_not_found`], this deliberately excludes
/// `DiskNotFound` so that an all-drives-unreachable error slice is treated as an
/// availability/read-quorum failure rather than "the object does not exist".
/// This mirrors MinIO's `isAllNotFound` and the sibling
/// [`crate::disk::error::DiskError::is_all_not_found`].
pub fn is_err_strict_not_found(err: &Error) -> bool {
is_err_object_not_found(err) || is_err_version_not_found(err) || matches!(err, &StorageError::VolumeNotFound)
}
pub fn is_all_not_found(errs: &[Option<Error>]) -> bool {
for err in errs.iter() {
if let Some(err) = err {
if is_err_strict_not_found(err) {
continue;
}
return false;
}
return false;
}
!errs.is_empty()
}
/// Strict "volume not found" predicate matching only `VolumeNotFound`/
/// `BucketNotFound`.
///
/// This is [`is_err_bucket_not_found`] minus `DiskNotFound`: an unreachable
/// drive must not be mistaken for a missing volume/bucket. Unlike
/// [`is_err_strict_not_found`] it deliberately excludes file/version level
/// not-found so that "the object is missing under an existing volume" is not
/// escalated to "the volume itself is missing".
pub fn is_err_strict_volume_not_found(err: &Error) -> bool {
matches!(err, &StorageError::VolumeNotFound) || matches!(err, &StorageError::BucketNotFound(_))
}
pub fn is_all_volume_not_found(errs: &[Option<Error>]) -> bool {
for err in errs.iter() {
if let Some(err) = err {
if is_err_strict_volume_not_found(err) {
continue;
}
return false;
}
return false;
}
!errs.is_empty()
}
/// Returns true when every entry is `DiskNotFound` (all drives offline /
/// drive-id mismatch across all sets) and the slice is non-empty.
///
/// Used to distinguish a full-availability failure from a genuine empty listing
/// so it can be surfaced as an error instead of being swallowed as "no entries".
pub fn is_all_disk_not_found(errs: &[Option<Error>]) -> bool {
for err in errs.iter() {
match err {
Some(err) if matches!(err, &StorageError::DiskNotFound) => continue,
_ => 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 other_preserves_erasure_construction_source_chain() {
use crate::erasure::coding::ErasureConstructionError;
use std::error::Error as _;
let error = StorageError::from(ErasureConstructionError::ModernEncoder {
source: reed_solomon_erasure::Error::TooManyShards,
});
let io_source = error.source().expect("StorageError::Io must expose its io::Error source");
assert!(io_source.is::<std::io::Error>());
let construction_source = io_source
.source()
.expect("io::Error must expose the erasure construction error");
assert!(construction_source.is::<ErasureConstructionError>());
let encoder_source = construction_source
.source()
.expect("construction error must expose the encoder error");
assert!(encoder_source.is::<reed_solomon_erasure::Error>());
}
// Regression for #952 (ECA-11): an all-`DiskNotFound` slice (every drive in
// every set unreachable) must NOT be classified as "all not found",
// otherwise ListObjects silently returns an empty listing and masks a full
// availability failure as "the bucket is empty".
#[test]
fn is_all_not_found_rejects_all_disk_not_found() {
let errs = vec![Some(StorageError::DiskNotFound), Some(StorageError::DiskNotFound)];
assert!(!is_all_not_found(&errs), "all-DiskNotFound must not be treated as all-not-found");
assert!(
!is_all_volume_not_found(&errs),
"all-DiskNotFound must not be treated as all-volume-not-found"
);
assert!(
is_all_disk_not_found(&errs),
"all-DiskNotFound must be recognised as a full availability failure"
);
}
// Genuine not-found errors must still be classified as such so real
// "object/version absent" semantics do not regress into availability errors.
#[test]
fn is_all_not_found_accepts_genuine_not_found() {
let object_errs = vec![
Some(StorageError::FileNotFound),
Some(StorageError::ObjectNotFound("bucket".into(), "object".into())),
Some(StorageError::FileVersionNotFound),
Some(StorageError::VersionNotFound("bucket".into(), "object".into(), "vid".into())),
Some(StorageError::VolumeNotFound),
];
assert!(
is_all_not_found(&object_errs),
"genuine object/version/volume not-found must stay all-not-found"
);
assert!(!is_all_disk_not_found(&object_errs));
let volume_errs = vec![Some(StorageError::VolumeNotFound), Some(StorageError::VolumeNotFound)];
assert!(is_all_not_found(&volume_errs));
assert!(
is_all_volume_not_found(&volume_errs),
"all-VolumeNotFound must remain all-volume-not-found"
);
}
// `is_all_volume_not_found` must escalate only volume/bucket absence, not a
// missing object under an existing volume, and must exclude DiskNotFound.
#[test]
fn is_all_volume_not_found_semantics() {
assert!(is_all_volume_not_found(&[Some(StorageError::BucketNotFound("bucket".into()))]));
assert!(
!is_all_volume_not_found(&[Some(StorageError::FileNotFound), Some(StorageError::VolumeNotFound)]),
"a missing object under an existing volume must not escalate to all-volume-not-found"
);
assert!(!is_all_volume_not_found(&[Some(StorageError::DiskNotFound)]));
}
// A `None` entry (a healthy set) short-circuits both predicates to false so a
// partial outage is never mistaken for an all-not-found / all-offline slice.
#[test]
fn not_found_predicates_reject_partial_and_empty() {
let partial = vec![None, Some(StorageError::DiskNotFound)];
assert!(!is_all_not_found(&partial));
assert!(!is_all_volume_not_found(&partial));
assert!(!is_all_disk_not_found(&partial));
assert!(!is_all_not_found(&[]));
assert!(!is_all_volume_not_found(&[]));
assert!(!is_all_disk_not_found(&[]));
}
#[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_code_contract_matches_storage_api() {
assert_eq!(StorageError::DiskFull.to_u32(), StorageErrorCode::DiskFull.as_u32());
assert_eq!(
StorageError::ObjectNotFound("bucket".to_string(), "object".to_string()).to_u32(),
StorageErrorCode::ObjectNotFound.as_u32()
);
assert!(matches!(
StorageError::from_u32(StorageErrorCode::ErasureReadQuorum.as_u32()),
Some(StorageError::ErasureReadQuorum)
));
assert!(matches!(
StorageError::from_u32(StorageErrorCode::NamespaceLockQuorumUnavailable.as_u32()),
Some(StorageError::NamespaceLockQuorumUnavailable { .. })
));
assert!(StorageError::from_u32(0x2B).is_none());
assert!(StorageError::from_u32(0x2C).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_disk_error_specific_variants_do_not_degrade_to_io() {
let cases = vec![
(DiskError::MaxVersionsExceeded, StorageError::MaxVersionsExceeded),
(DiskError::InconsistentDisk, StorageError::InconsistentDisk),
(DiskError::UnsupportedDisk, StorageError::UnsupportedDisk),
(DiskError::DiskNotDir, StorageError::DiskNotDir),
(DiskError::DiskOngoingReq, StorageError::DiskOngoingReq),
(DiskError::PathNotFound, StorageError::PathNotFound),
(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::ShortWrite, StorageError::ShortWrite),
(DiskError::SourceStalled, StorageError::SourceStalled),
(DiskError::Timeout, StorageError::Timeout),
(DiskError::InvalidPath, StorageError::InvalidPath),
];
for (disk_error, expected_storage_error) in cases {
let storage_error: StorageError = disk_error.into();
assert!(
!matches!(&storage_error, StorageError::Io(_)),
"expected {expected_storage_error:?}, got generic Io"
);
assert_eq!(storage_error, expected_storage_error);
}
}
#[test]
fn test_storage_error_disk_preservation_codes_roundtrip() {
let errors = vec![
StorageError::MaxVersionsExceeded,
StorageError::InconsistentDisk,
StorageError::UnsupportedDisk,
StorageError::DiskNotDir,
StorageError::DiskOngoingReq,
StorageError::PathNotFound,
StorageError::BitrotHashAlgoInvalid,
StorageError::CrossDeviceLink,
StorageError::LessData,
StorageError::MoreData,
StorageError::OutdatedXLMeta,
StorageError::PartMissingOrCorrupt,
StorageError::ShortWrite,
StorageError::SourceStalled,
StorageError::Timeout,
StorageError::InvalidPath,
];
for original_error in errors {
let code = original_error.to_u32();
let recovered_error =
StorageError::from_u32(code).unwrap_or_else(|| panic!("failed to recover error from code: {code:#x}"));
assert_eq!(std::mem::discriminant(&original_error), std::mem::discriminant(&recovered_error));
}
}
#[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);
}
}
#[test]
fn test_storage_error_display_snapshot() {
// Snapshot test to detect unexpected changes in error display format
let 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()),
StorageError::DiskFull,
StorageError::FaultyDisk,
StorageError::FileNotFound,
StorageError::VolumeNotFound,
StorageError::ErasureReadQuorum,
StorageError::ErasureWriteQuorum,
StorageError::DecommissionAlreadyRunning,
StorageError::RebalanceAlreadyRunning,
StorageError::OperationCanceled,
StorageError::NamespaceLockQuorumUnavailable {
mode: "write",
bucket: "bucket".into(),
object: "object".into(),
required: 3,
achieved: 2,
},
];
let error_messages: Vec<String> = errors.iter().map(|e| e.to_string()).collect();
insta::assert_yaml_snapshot!("storage_error_display", error_messages);
}
}