// 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 = core::result::Result; /// 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 for StorageError { fn from(error: crate::erasure::coding::ErasureConstructionError) -> Self { Self::Io(error.into_io_error()) } } impl StorageError { pub fn other(error: E) -> Self where E: Into>, { 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 for StorageError { fn from(err: HTTPRangeError) -> Self { match err { HTTPRangeError::InvalidRangeSpec(message) => Self::InvalidRangeSpec(message), } } } impl From 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 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 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 for StorageError { fn from(e: std::io::Error) -> Self { match e.downcast::() { Ok(storage_error) => storage_error, Err(io_error) => match io_error.downcast::() { Ok(disk_error) => disk_error.into(), Err(io_error) => StorageError::Io(io_error), }, } } } impl From 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 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 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 { 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 for StorageError { fn from(e: tokio::task::JoinError) -> Self { StorageError::other(e) } } impl From for StorageError { fn from(e: serde_json::Error) -> Self { StorageError::other(e) } } impl From for Error { fn from(e: rmp_serde::encode::Error) -> Self { Error::other(e) } } impl From for Error { fn from(e: rmp::encode::ValueWriteError) -> Self { Error::other(e) } } impl From for Error { fn from(e: rmp::decode::ValueReadError) -> Self { Error::other(e) } } impl From for Error { fn from(e: std::string::FromUtf8Error) -> Self { Error::other(e) } } impl From for Error { fn from(e: rmp::decode::NumValueReadError) -> Self { Error::other(e) } } impl From for Error { fn from(e: rmp_serde::decode::Error) -> Self { Error::other(e) } } impl From for Error { fn from(e: s3s::xml::SerError) -> Self { Error::other(e) } } impl From for Error { fn from(e: s3s::xml::DeError) -> Self { Error::other(e) } } impl From for Error { fn from(e: tonic::Status) -> Self { Error::other(e.to_string()) } } impl From for Error { fn from(e: uuid::Error) -> Self { Error::other(e) } } impl From 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]) -> 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]) -> 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]) -> 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, pub bucket_name: Option, pub region: Option, pub request_id: Option, 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::()); let construction_source = io_source .source() .expect("io::Error must expose the erasure construction error"); assert!(construction_source.is::()); let encoder_source = construction_source .source() .expect("construction error must expose the encoder error"); assert!(encoder_source.is::()); } // 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::(); 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 = errors.iter().map(|e| e.to_string()).collect(); insta::assert_yaml_snapshot!("storage_error_display", error_messages); } }