// 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. pub mod disk_store; pub mod endpoint; pub mod error; pub mod error_conv; pub mod error_reduce; pub mod format; pub mod fs; pub mod health_state; pub mod local; pub mod os; pub const RUSTFS_META_BUCKET: &str = ".rustfs.sys"; pub const MIGRATING_META_BUCKET: &str = ".minio.sys"; pub const RUSTFS_META_MULTIPART_BUCKET: &str = ".rustfs.sys/multipart"; pub const RUSTFS_META_TMP_BUCKET: &str = ".rustfs.sys/tmp"; pub const RUSTFS_META_TMP_DELETED_BUCKET: &str = ".rustfs.sys/tmp/.trash"; pub const BUCKET_META_PREFIX: &str = "buckets"; pub const FORMAT_CONFIG_FILE: &str = "format.json"; pub const STORAGE_FORMAT_FILE: &str = "xl.meta"; pub const STORAGE_FORMAT_FILE_BACKUP: &str = "xl.meta.bkp"; use crate::disk::disk_store::LocalDiskWrapper; use crate::disk::health_state::RuntimeDriveHealthState; use crate::disk::local::ScanGuard; use crate::rpc::RemoteDisk; use crate::rpc::build_internode_data_transport_from_env; use bytes::Bytes; use endpoint::Endpoint; use error::DiskError; use error::{Error, Result}; use local::LocalDisk; use rustfs_filemeta::{FileInfo, ObjectPartInfo, RawFileInfo}; use rustfs_madmin::info_commands::DiskMetrics; use serde::{Deserialize, Serialize}; use std::{fmt::Debug, path::PathBuf, sync::Arc}; use time::OffsetDateTime; use tokio::io::{AsyncRead, AsyncWrite}; use uuid::Uuid; pub type DiskStore = Arc; pub type FileReader = Box; pub type FileWriter = Box; #[derive(Debug)] pub enum Disk { Local(Box), Remote(Box), } #[async_trait::async_trait] impl DiskAPI for Disk { fn to_string(&self) -> String { match self { Disk::Local(local_disk) => local_disk.to_string(), Disk::Remote(remote_disk) => remote_disk.to_string(), } } async fn is_online(&self) -> bool { match self { Disk::Local(local_disk) => local_disk.is_online().await, Disk::Remote(remote_disk) => remote_disk.is_online().await, } } fn is_local(&self) -> bool { match self { Disk::Local(local_disk) => local_disk.is_local(), Disk::Remote(remote_disk) => remote_disk.is_local(), } } fn host_name(&self) -> String { match self { Disk::Local(local_disk) => local_disk.host_name(), Disk::Remote(remote_disk) => remote_disk.host_name(), } } fn endpoint(&self) -> Endpoint { match self { Disk::Local(local_disk) => local_disk.endpoint(), Disk::Remote(remote_disk) => remote_disk.endpoint(), } } async fn close(&self) -> Result<()> { match self { Disk::Local(local_disk) => local_disk.close().await, Disk::Remote(remote_disk) => remote_disk.close().await, } } async fn get_disk_id(&self) -> Result> { match self { Disk::Local(local_disk) => local_disk.get_disk_id().await, Disk::Remote(remote_disk) => remote_disk.get_disk_id().await, } } async fn set_disk_id(&self, id: Option) -> Result<()> { match self { Disk::Local(local_disk) => local_disk.set_disk_id(id).await, Disk::Remote(remote_disk) => remote_disk.set_disk_id(id).await, } } fn path(&self) -> PathBuf { match self { Disk::Local(local_disk) => local_disk.path(), Disk::Remote(remote_disk) => remote_disk.path(), } } fn get_disk_location(&self) -> DiskLocation { match self { Disk::Local(local_disk) => local_disk.get_disk_location(), Disk::Remote(remote_disk) => remote_disk.get_disk_location(), } } #[tracing::instrument(skip(self))] async fn make_volume(&self, volume: &str) -> Result<()> { match self { Disk::Local(local_disk) => local_disk.make_volume(volume).await, Disk::Remote(remote_disk) => remote_disk.make_volume(volume).await, } } #[tracing::instrument(skip(self))] async fn make_volumes(&self, volumes: Vec<&str>) -> Result<()> { match self { Disk::Local(local_disk) => local_disk.make_volumes(volumes).await, Disk::Remote(remote_disk) => remote_disk.make_volumes(volumes).await, } } #[tracing::instrument(skip(self))] async fn list_volumes(&self) -> Result> { match self { Disk::Local(local_disk) => local_disk.list_volumes().await, Disk::Remote(remote_disk) => remote_disk.list_volumes().await, } } async fn stat_volume(&self, volume: &str) -> Result { match self { Disk::Local(local_disk) => local_disk.stat_volume(volume).await, Disk::Remote(remote_disk) => remote_disk.stat_volume(volume).await, } } #[tracing::instrument(skip(self))] async fn delete_volume(&self, volume: &str) -> Result<()> { match self { Disk::Local(local_disk) => local_disk.delete_volume(volume).await, Disk::Remote(remote_disk) => remote_disk.delete_volume(volume).await, } } #[tracing::instrument(skip(self, wr))] async fn walk_dir(&self, opts: WalkDirOptions, wr: &mut W) -> Result<()> { match self { Disk::Local(local_disk) => local_disk.walk_dir(opts, wr).await, Disk::Remote(remote_disk) => remote_disk.walk_dir(opts, wr).await, } } #[tracing::instrument(skip(self))] async fn delete_version( &self, volume: &str, path: &str, fi: FileInfo, force_del_marker: bool, opts: DeleteOptions, ) -> Result<()> { match self { Disk::Local(local_disk) => local_disk.delete_version(volume, path, fi, force_del_marker, opts).await, Disk::Remote(remote_disk) => remote_disk.delete_version(volume, path, fi, force_del_marker, opts).await, } } #[tracing::instrument(skip(self))] async fn delete_versions(&self, volume: &str, versions: Vec, opts: DeleteOptions) -> Vec> { match self { Disk::Local(local_disk) => local_disk.delete_versions(volume, versions, opts).await, Disk::Remote(remote_disk) => remote_disk.delete_versions(volume, versions, opts).await, } } #[tracing::instrument(skip(self))] async fn delete_paths(&self, volume: &str, paths: &[String]) -> Result<()> { match self { Disk::Local(local_disk) => local_disk.delete_paths(volume, paths).await, Disk::Remote(remote_disk) => remote_disk.delete_paths(volume, paths).await, } } #[tracing::instrument(skip(self))] async fn write_metadata(&self, _org_volume: &str, volume: &str, path: &str, fi: FileInfo) -> Result<()> { match self { Disk::Local(local_disk) => local_disk.write_metadata(_org_volume, volume, path, fi).await, Disk::Remote(remote_disk) => remote_disk.write_metadata(_org_volume, volume, path, fi).await, } } #[tracing::instrument(skip(self))] async fn update_metadata(&self, volume: &str, path: &str, fi: FileInfo, opts: &UpdateMetadataOpts) -> Result<()> { match self { Disk::Local(local_disk) => local_disk.update_metadata(volume, path, fi, opts).await, Disk::Remote(remote_disk) => remote_disk.update_metadata(volume, path, fi, opts).await, } } #[tracing::instrument(level = "debug", skip(self))] async fn read_version( &self, _org_volume: &str, volume: &str, path: &str, version_id: &str, opts: &ReadOptions, ) -> Result { match self { Disk::Local(local_disk) => local_disk.read_version(_org_volume, volume, path, version_id, opts).await, Disk::Remote(remote_disk) => remote_disk.read_version(_org_volume, volume, path, version_id, opts).await, } } #[tracing::instrument(skip(self))] async fn read_xl(&self, volume: &str, path: &str, read_data: bool) -> Result { match self { Disk::Local(local_disk) => local_disk.read_xl(volume, path, read_data).await, Disk::Remote(remote_disk) => remote_disk.read_xl(volume, path, read_data).await, } } #[tracing::instrument(skip(self, fi))] async fn rename_data( &self, src_volume: &str, src_path: &str, fi: FileInfo, dst_volume: &str, dst_path: &str, ) -> Result { match self { Disk::Local(local_disk) => local_disk.rename_data(src_volume, src_path, fi, dst_volume, dst_path).await, Disk::Remote(remote_disk) => remote_disk.rename_data(src_volume, src_path, fi, dst_volume, dst_path).await, } } #[tracing::instrument(skip(self))] async fn list_dir(&self, _origvolume: &str, volume: &str, dir_path: &str, count: i32) -> Result> { match self { Disk::Local(local_disk) => local_disk.list_dir(_origvolume, volume, dir_path, count).await, Disk::Remote(remote_disk) => remote_disk.list_dir(_origvolume, volume, dir_path, count).await, } } #[tracing::instrument(skip(self))] async fn read_file(&self, volume: &str, path: &str) -> Result { match self { Disk::Local(local_disk) => local_disk.read_file(volume, path).await, Disk::Remote(remote_disk) => remote_disk.read_file(volume, path).await, } } #[tracing::instrument(skip(self))] async fn read_file_stream(&self, volume: &str, path: &str, offset: usize, length: usize) -> Result { match self { Disk::Local(local_disk) => local_disk.read_file_stream(volume, path, offset, length).await, Disk::Remote(remote_disk) => remote_disk.read_file_stream(volume, path, offset, length).await, } } #[tracing::instrument(skip(self))] async fn read_file_zero_copy(&self, volume: &str, path: &str, offset: usize, length: usize) -> Result { match self { Disk::Local(local_disk) => local_disk.read_file_zero_copy(volume, path, offset, length).await, Disk::Remote(remote_disk) => remote_disk.read_file_zero_copy(volume, path, offset, length).await, } } #[tracing::instrument(skip(self))] async fn append_file(&self, volume: &str, path: &str) -> Result { match self { Disk::Local(local_disk) => local_disk.append_file(volume, path).await, Disk::Remote(remote_disk) => remote_disk.append_file(volume, path).await, } } #[tracing::instrument(skip(self))] async fn create_file(&self, _origvolume: &str, volume: &str, path: &str, _file_size: i64) -> Result { match self { Disk::Local(local_disk) => local_disk.create_file(_origvolume, volume, path, _file_size).await, Disk::Remote(remote_disk) => remote_disk.create_file(_origvolume, volume, path, _file_size).await, } } #[tracing::instrument(skip(self))] async fn rename_file(&self, src_volume: &str, src_path: &str, dst_volume: &str, dst_path: &str) -> Result<()> { match self { Disk::Local(local_disk) => local_disk.rename_file(src_volume, src_path, dst_volume, dst_path).await, Disk::Remote(remote_disk) => remote_disk.rename_file(src_volume, src_path, dst_volume, dst_path).await, } } #[tracing::instrument(skip(self))] async fn read_parts(&self, bucket: &str, paths: &[String]) -> Result> { match self { Disk::Local(local_disk) => local_disk.read_parts(bucket, paths).await, Disk::Remote(remote_disk) => remote_disk.read_parts(bucket, paths).await, } } #[tracing::instrument(skip(self))] async fn rename_part(&self, src_volume: &str, src_path: &str, dst_volume: &str, dst_path: &str, meta: Bytes) -> Result<()> { match self { Disk::Local(local_disk) => local_disk.rename_part(src_volume, src_path, dst_volume, dst_path, meta).await, Disk::Remote(remote_disk) => { remote_disk .rename_part(src_volume, src_path, dst_volume, dst_path, meta) .await } } } #[tracing::instrument(skip(self))] async fn delete(&self, volume: &str, path: &str, opt: DeleteOptions) -> Result<()> { match self { Disk::Local(local_disk) => local_disk.delete(volume, path, opt).await, Disk::Remote(remote_disk) => remote_disk.delete(volume, path, opt).await, } } #[tracing::instrument(skip(self))] async fn verify_file(&self, volume: &str, path: &str, fi: &FileInfo) -> Result { match self { Disk::Local(local_disk) => local_disk.verify_file(volume, path, fi).await, Disk::Remote(remote_disk) => remote_disk.verify_file(volume, path, fi).await, } } #[tracing::instrument(skip(self))] async fn check_parts(&self, volume: &str, path: &str, fi: &FileInfo) -> Result { match self { Disk::Local(local_disk) => local_disk.check_parts(volume, path, fi).await, Disk::Remote(remote_disk) => remote_disk.check_parts(volume, path, fi).await, } } #[tracing::instrument(skip(self))] async fn read_multiple(&self, req: ReadMultipleReq) -> Result> { match self { Disk::Local(local_disk) => local_disk.read_multiple(req).await, Disk::Remote(remote_disk) => remote_disk.read_multiple(req).await, } } #[tracing::instrument(skip(self))] async fn write_all(&self, volume: &str, path: &str, data: Bytes) -> Result<()> { match self { Disk::Local(local_disk) => local_disk.write_all(volume, path, data).await, Disk::Remote(remote_disk) => remote_disk.write_all(volume, path, data).await, } } #[tracing::instrument(skip(self))] async fn read_all(&self, volume: &str, path: &str) -> Result { match self { Disk::Local(local_disk) => local_disk.read_all(volume, path).await, Disk::Remote(remote_disk) => remote_disk.read_all(volume, path).await, } } #[tracing::instrument(skip(self))] async fn disk_info(&self, opts: &DiskInfoOptions) -> Result { match self { Disk::Local(local_disk) => local_disk.disk_info(opts).await, Disk::Remote(remote_disk) => remote_disk.disk_info(opts).await, } } fn start_scan(&self) -> ScanGuard { match self { Disk::Local(local_disk) => local_disk.start_scan(), Disk::Remote(remote_disk) => remote_disk.start_scan(), } } async fn read_metadata(&self, volume: &str, path: &str) -> Result { match self { Disk::Local(local_disk) => local_disk.read_metadata(volume, path).await, Disk::Remote(remote_disk) => remote_disk.read_metadata(volume, path).await, } } } impl Disk { pub fn runtime_state(&self) -> RuntimeDriveHealthState { match self { Disk::Local(local_disk) => local_disk.runtime_state(), Disk::Remote(remote_disk) => remote_disk.runtime_state(), } } pub fn offline_duration_secs(&self) -> Option { match self { Disk::Local(local_disk) => local_disk.offline_duration_secs(), Disk::Remote(remote_disk) => remote_disk.offline_duration_secs(), } } pub fn last_capacity_snapshot(&self) -> Option<(u64, u64, u64, u64)> { match self { Disk::Local(local_disk) => local_disk.last_capacity_snapshot(), Disk::Remote(remote_disk) => remote_disk.last_capacity_snapshot(), } } pub fn record_capacity_probe(&self, total: u64, used: u64, free: u64) { match self { Disk::Local(local_disk) => local_disk.record_capacity_probe(total, used, free), Disk::Remote(remote_disk) => remote_disk.record_capacity_probe(total, used, free), } } #[cfg(test)] pub fn force_runtime_state_for_test(&self, state: RuntimeDriveHealthState) { match self { Disk::Local(local_disk) => local_disk.force_runtime_state_for_test(state), Disk::Remote(remote_disk) => remote_disk.force_runtime_state_for_test(state), } } } impl Disk { /// Reset drive health so `connect_load_init_formats` retries are not blocked by a prior /// transient mark-faulty (same disk handles are reused across retries). pub fn reset_health_for_store_init_retry(&self) { match self { Disk::Local(local_disk) => local_disk.reset_health_for_store_init_retry(), Disk::Remote(remote_disk) => remote_disk.reset_health_for_store_init_retry(), } } /// Enable health monitoring on this disk. /// Called after startup format loading completes so that remote peers /// have time to come online before being marked as faulty. pub fn enable_health_check(&self) { match self { Disk::Local(local_disk) => local_disk.enable_health_check(), Disk::Remote(remote_disk) => remote_disk.enable_health_check(), } } } pub async fn new_disk(ep: &Endpoint, opt: &DiskOption) -> Result { if ep.is_local { let s = LocalDisk::new(ep, opt.cleanup).await?; Ok(Arc::new(Disk::Local(Box::new(LocalDiskWrapper::new(Arc::new(s), opt.health_check))))) } else { let data_transport = build_internode_data_transport_from_env(); let remote_disk = RemoteDisk::new(ep, opt, data_transport?).await?; Ok(Arc::new(Disk::Remote(Box::new(remote_disk)))) } } #[async_trait::async_trait] pub trait DiskAPI: Debug + Send + Sync + 'static { fn to_string(&self) -> String; async fn is_online(&self) -> bool; fn is_local(&self) -> bool; // LastConn fn host_name(&self) -> String; fn endpoint(&self) -> Endpoint; async fn close(&self) -> Result<()>; async fn get_disk_id(&self) -> Result>; async fn set_disk_id(&self, id: Option) -> Result<()>; fn path(&self) -> PathBuf; fn get_disk_location(&self) -> DiskLocation; // Healing // DiskInfo // NSScanner // Volume operations. async fn make_volume(&self, volume: &str) -> Result<()>; async fn make_volumes(&self, volume: Vec<&str>) -> Result<()>; async fn list_volumes(&self) -> Result>; async fn stat_volume(&self, volume: &str) -> Result; async fn delete_volume(&self, volume: &str) -> Result<()>; // Concurrent read/write pipeline w <- MetaCacheEntry async fn walk_dir(&self, opts: WalkDirOptions, wr: &mut W) -> Result<()>; // Metadata operations async fn delete_version( &self, volume: &str, path: &str, fi: FileInfo, force_del_marker: bool, opts: DeleteOptions, ) -> Result<()>; async fn delete_versions(&self, volume: &str, versions: Vec, opts: DeleteOptions) -> Vec>; async fn delete_paths(&self, volume: &str, paths: &[String]) -> Result<()>; async fn write_metadata(&self, org_volume: &str, volume: &str, path: &str, fi: FileInfo) -> Result<()>; async fn update_metadata(&self, volume: &str, path: &str, fi: FileInfo, opts: &UpdateMetadataOpts) -> Result<()>; async fn read_version( &self, org_volume: &str, volume: &str, path: &str, version_id: &str, opts: &ReadOptions, ) -> Result; async fn read_xl(&self, volume: &str, path: &str, read_data: bool) -> Result; async fn read_metadata(&self, volume: &str, path: &str) -> Result; async fn rename_data( &self, src_volume: &str, src_path: &str, file_info: FileInfo, dst_volume: &str, dst_path: &str, ) -> Result; // File operations. // Read every file and directory within the folder async fn list_dir(&self, origvolume: &str, volume: &str, dir_path: &str, count: i32) -> Result>; async fn read_file(&self, volume: &str, path: &str) -> Result; async fn read_file_stream(&self, volume: &str, path: &str, offset: usize, length: usize) -> Result; /// Zero-copy file read using memory mapping (Unix) or efficient read (non-Unix). /// Returns Bytes that can be shared without copying. /// On Unix, this uses mmap for true zero-copy access. /// On other platforms, falls back to efficient read operations. async fn read_file_zero_copy(&self, volume: &str, path: &str, offset: usize, length: usize) -> Result; async fn append_file(&self, volume: &str, path: &str) -> Result; async fn create_file(&self, origvolume: &str, volume: &str, path: &str, file_size: i64) -> Result; // ReadFileStream async fn rename_file(&self, src_volume: &str, src_path: &str, dst_volume: &str, dst_path: &str) -> Result<()>; async fn rename_part(&self, src_volume: &str, src_path: &str, dst_volume: &str, dst_path: &str, meta: Bytes) -> Result<()>; async fn delete(&self, volume: &str, path: &str, opt: DeleteOptions) -> Result<()>; // VerifyFile async fn verify_file(&self, volume: &str, path: &str, fi: &FileInfo) -> Result; // CheckParts async fn check_parts(&self, volume: &str, path: &str, fi: &FileInfo) -> Result; // StatInfoFile async fn read_parts(&self, bucket: &str, paths: &[String]) -> Result>; async fn read_multiple(&self, req: ReadMultipleReq) -> Result>; // CleanAbandonedData async fn write_all(&self, volume: &str, path: &str, data: Bytes) -> Result<()>; async fn read_all(&self, volume: &str, path: &str) -> Result; async fn disk_info(&self, opts: &DiskInfoOptions) -> Result; fn start_scan(&self) -> ScanGuard; } #[derive(Debug, Default, Serialize, Deserialize)] pub struct CheckPartsResp { pub results: Vec, } #[derive(Debug, Serialize, Deserialize, Default)] pub struct UpdateMetadataOpts { pub no_persistence: bool, pub replace_user_metadata: bool, } pub struct DiskLocation { pub pool_idx: Option, pub set_idx: Option, pub disk_idx: Option, } impl DiskLocation { pub fn valid(&self) -> bool { self.pool_idx.is_some() && self.set_idx.is_some() && self.disk_idx.is_some() } } #[derive(Debug, Default, Serialize, Deserialize)] pub struct DiskInfoOptions { pub disk_id: String, pub metrics: bool, pub noop: bool, } #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct DiskInfo { pub total: u64, pub free: u64, pub used: u64, pub used_inodes: u64, pub free_inodes: u64, pub major: u64, pub minor: u64, pub nr_requests: u64, pub fs_type: String, pub root_disk: bool, pub healing: bool, pub scanning: bool, pub endpoint: String, pub mount_path: String, /// Leaf physical block devices backing this mount path when available. pub physical_device_ids: Vec, pub id: Option, pub rotational: bool, pub metrics: DiskMetrics, pub error: String, } #[derive(Clone, Debug, Default)] pub struct Info { pub total: u64, pub free: u64, pub used: u64, pub files: u64, pub ffree: u64, pub fstype: String, pub major: u64, pub minor: u64, pub name: String, pub rotational: bool, pub nrrequests: u64, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct FileInfoVersions { // Name of the volume. pub volume: String, // Name of the file. pub name: String, // Represents the latest mod time of the // latest version. pub latest_mod_time: Option, pub versions: Vec, pub free_versions: Vec, } impl FileInfoVersions { pub fn find_version_index(&self, v: &str) -> Option { if v.is_empty() { return None; } let vid = Uuid::parse_str(v).unwrap_or_default(); self.versions.iter().position(|v| v.version_id == Some(vid)) } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct WalkDirOptions { // Bucket to scanner pub bucket: String, // Directory inside the bucket. pub base_dir: String, // Do a full recursive scan. pub recursive: bool, // ReportNotFound will return errFileNotFound if all disks reports the BaseDir cannot be found. pub report_notfound: bool, // FilterPrefix will only return results with given prefix within folder. // Should never contain a slash. pub filter_prefix: Option, // ForwardTo will forward to the given object path. pub forward_to: Option, // Limit the number of returned objects if > 0. pub limit: i32, // DiskID contains the disk ID of the disk. // Leave empty to not check disk ID. pub disk_id: String, // Skip the wrapper-level total timeout for long streaming walks. #[serde(default)] pub skip_total_timeout: bool, } #[derive(Clone, Debug, Default)] pub struct DiskOption { pub cleanup: bool, pub health_check: bool, } #[derive(Debug, Default, Serialize, Deserialize)] pub struct RenameDataResp { pub old_data_dir: Option, pub sign: Option>, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct DeleteOptions { pub recursive: bool, pub immediate: bool, pub undo_write: bool, pub old_data_dir: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReadMultipleReq { pub bucket: String, pub prefix: String, pub files: Vec, pub max_size: usize, pub metadata_only: bool, pub abort404: bool, pub max_results: usize, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct ReadMultipleResp { pub bucket: String, pub prefix: String, pub file: String, pub exists: bool, pub error: String, pub data: Vec, pub mod_time: Option, } #[derive(Debug, Deserialize, Serialize)] pub struct VolumeInfo { pub name: String, pub created: Option, } #[derive(Deserialize, Serialize, Debug, Default, Clone)] pub struct ReadOptions { pub incl_free_versions: bool, pub read_data: bool, pub healing: bool, } pub const CHECK_PART_UNKNOWN: usize = 0; // Changing the order can cause a data loss // when running two nodes with incompatible versions pub const CHECK_PART_SUCCESS: usize = 1; pub const CHECK_PART_DISK_NOT_FOUND: usize = 2; pub const CHECK_PART_VOLUME_NOT_FOUND: usize = 3; pub const CHECK_PART_FILE_NOT_FOUND: usize = 4; pub const CHECK_PART_FILE_CORRUPT: usize = 5; pub fn conv_part_err_to_int(err: &Option) -> usize { match err { Some(DiskError::FileNotFound) | Some(DiskError::FileVersionNotFound) => CHECK_PART_FILE_NOT_FOUND, Some(DiskError::FileCorrupt) => CHECK_PART_FILE_CORRUPT, Some(DiskError::VolumeNotFound) => CHECK_PART_VOLUME_NOT_FOUND, Some(DiskError::DiskNotFound) => CHECK_PART_DISK_NOT_FOUND, None => CHECK_PART_SUCCESS, _ => { tracing::warn!("conv_part_err_to_int: unknown error: {err:?}"); CHECK_PART_UNKNOWN } } } pub fn has_part_err(part_errs: &[usize]) -> bool { part_errs.iter().any(|err| *err != CHECK_PART_SUCCESS) } pub fn count_part_not_success(part_errs: &[usize]) -> usize { part_errs.iter().filter(|err| **err != CHECK_PART_SUCCESS).count() } #[cfg(test)] mod tests { use super::*; use endpoint::Endpoint; use local::LocalDisk; use tokio::fs; use uuid::Uuid; /// Test DiskLocation validation #[test] fn test_disk_location_valid() { let valid_location = DiskLocation { pool_idx: Some(0), set_idx: Some(1), disk_idx: Some(2), }; assert!(valid_location.valid()); let invalid_location = DiskLocation { pool_idx: None, set_idx: None, disk_idx: None, }; assert!(!invalid_location.valid()); let partial_valid_location = DiskLocation { pool_idx: Some(0), set_idx: None, disk_idx: Some(2), }; assert!(!partial_valid_location.valid()); } /// Test FileInfoVersions find_version_index #[test] fn test_file_info_versions_find_version_index() { let mut versions = Vec::new(); let v1_uuid = Uuid::new_v4(); let v2_uuid = Uuid::new_v4(); let fi1 = FileInfo { version_id: Some(v1_uuid), ..Default::default() }; let fi2 = FileInfo { version_id: Some(v2_uuid), ..Default::default() }; versions.push(fi1); versions.push(fi2); let fiv = FileInfoVersions { volume: "test-bucket".to_string(), name: "test-object".to_string(), latest_mod_time: None, versions, free_versions: Vec::new(), }; assert_eq!(fiv.find_version_index(&v1_uuid.to_string()), Some(0)); assert_eq!(fiv.find_version_index(&v2_uuid.to_string()), Some(1)); assert_eq!(fiv.find_version_index("non-existent"), None); assert_eq!(fiv.find_version_index(""), None); } /// Test part error conversion functions #[test] fn test_conv_part_err_to_int() { assert_eq!(conv_part_err_to_int(&None), CHECK_PART_SUCCESS); assert_eq!( conv_part_err_to_int(&Some(Error::from(DiskError::DiskNotFound))), CHECK_PART_DISK_NOT_FOUND ); assert_eq!( conv_part_err_to_int(&Some(Error::from(DiskError::VolumeNotFound))), CHECK_PART_VOLUME_NOT_FOUND ); assert_eq!( conv_part_err_to_int(&Some(Error::from(DiskError::FileNotFound))), CHECK_PART_FILE_NOT_FOUND ); assert_eq!(conv_part_err_to_int(&Some(Error::from(DiskError::FileCorrupt))), CHECK_PART_FILE_CORRUPT); assert_eq!(conv_part_err_to_int(&Some(Error::from(DiskError::Unexpected))), CHECK_PART_UNKNOWN); } /// Test has_part_err function #[test] fn test_has_part_err() { assert!(!has_part_err(&[])); assert!(!has_part_err(&[CHECK_PART_SUCCESS])); assert!(!has_part_err(&[CHECK_PART_SUCCESS, CHECK_PART_SUCCESS])); assert!(has_part_err(&[CHECK_PART_FILE_NOT_FOUND])); assert!(has_part_err(&[CHECK_PART_SUCCESS, CHECK_PART_FILE_CORRUPT])); assert!(has_part_err(&[CHECK_PART_DISK_NOT_FOUND, CHECK_PART_VOLUME_NOT_FOUND])); } /// Test WalkDirOptions structure #[test] fn test_walk_dir_options() { let opts = WalkDirOptions { bucket: "test-bucket".to_string(), base_dir: "/path/to/dir".to_string(), recursive: true, report_notfound: false, filter_prefix: Some("prefix_".to_string()), forward_to: Some("object/path".to_string()), limit: 100, disk_id: "disk-123".to_string(), skip_total_timeout: false, }; assert_eq!(opts.bucket, "test-bucket"); assert_eq!(opts.base_dir, "/path/to/dir"); assert!(opts.recursive); assert!(!opts.report_notfound); assert_eq!(opts.filter_prefix, Some("prefix_".to_string())); assert_eq!(opts.forward_to, Some("object/path".to_string())); assert_eq!(opts.limit, 100); assert_eq!(opts.disk_id, "disk-123"); assert!(!opts.skip_total_timeout); } /// Test DeleteOptions structure #[test] fn test_delete_options() { let opts = DeleteOptions { recursive: true, immediate: false, undo_write: true, old_data_dir: Some(Uuid::new_v4()), }; assert!(opts.recursive); assert!(!opts.immediate); assert!(opts.undo_write); assert!(opts.old_data_dir.is_some()); } /// Test ReadOptions structure #[test] fn test_read_options() { let opts = ReadOptions { incl_free_versions: true, read_data: false, healing: true, }; assert!(opts.incl_free_versions); assert!(!opts.read_data); assert!(opts.healing); } /// Test UpdateMetadataOpts structure #[test] fn test_update_metadata_opts() { let opts = UpdateMetadataOpts { no_persistence: true, ..Default::default() }; assert!(opts.no_persistence); assert!(!opts.replace_user_metadata); } /// Test DiskOption structure #[test] fn test_disk_option() { let opt = DiskOption { cleanup: true, health_check: false, }; assert!(opt.cleanup); assert!(!opt.health_check); } /// Test DiskInfoOptions structure #[test] fn test_disk_info_options() { let opts = DiskInfoOptions { disk_id: "test-disk-id".to_string(), metrics: true, noop: false, }; assert_eq!(opts.disk_id, "test-disk-id"); assert!(opts.metrics); assert!(!opts.noop); } /// Test ReadMultipleReq structure #[test] fn test_read_multiple_req() { let req = ReadMultipleReq { bucket: "test-bucket".to_string(), prefix: "prefix/".to_string(), files: vec!["file1.txt".to_string(), "file2.txt".to_string()], max_size: 1024, metadata_only: false, abort404: true, max_results: 10, }; assert_eq!(req.bucket, "test-bucket"); assert_eq!(req.prefix, "prefix/"); assert_eq!(req.files.len(), 2); assert_eq!(req.max_size, 1024); assert!(!req.metadata_only); assert!(req.abort404); assert_eq!(req.max_results, 10); } /// Test ReadMultipleResp structure #[test] fn test_read_multiple_resp() { let resp = ReadMultipleResp { bucket: "test-bucket".to_string(), prefix: "prefix/".to_string(), file: "test-file.txt".to_string(), exists: true, error: "".to_string(), data: vec![1, 2, 3, 4], mod_time: Some(time::OffsetDateTime::now_utc()), }; assert_eq!(resp.bucket, "test-bucket"); assert_eq!(resp.prefix, "prefix/"); assert_eq!(resp.file, "test-file.txt"); assert!(resp.exists); assert!(resp.error.is_empty()); assert_eq!(resp.data, vec![1, 2, 3, 4]); assert!(resp.mod_time.is_some()); } /// Test VolumeInfo structure #[test] fn test_volume_info() { let now = time::OffsetDateTime::now_utc(); let vol_info = VolumeInfo { name: "test-volume".to_string(), created: Some(now), }; assert_eq!(vol_info.name, "test-volume"); assert_eq!(vol_info.created, Some(now)); } /// Test CheckPartsResp structure #[test] fn test_check_parts_resp() { let resp = CheckPartsResp { results: vec![CHECK_PART_SUCCESS, CHECK_PART_FILE_NOT_FOUND, CHECK_PART_FILE_CORRUPT], }; assert_eq!(resp.results.len(), 3); assert_eq!(resp.results[0], CHECK_PART_SUCCESS); assert_eq!(resp.results[1], CHECK_PART_FILE_NOT_FOUND); assert_eq!(resp.results[2], CHECK_PART_FILE_CORRUPT); } /// Test RenameDataResp structure #[test] fn test_rename_data_resp() { let uuid = Uuid::new_v4(); let signature = vec![0x01, 0x02, 0x03]; let resp = RenameDataResp { old_data_dir: Some(uuid), sign: Some(signature.clone()), }; assert_eq!(resp.old_data_dir, Some(uuid)); assert_eq!(resp.sign, Some(signature)); } /// Test constants #[test] fn test_constants() { assert_eq!(RUSTFS_META_BUCKET, ".rustfs.sys"); assert_eq!(RUSTFS_META_MULTIPART_BUCKET, ".rustfs.sys/multipart"); assert_eq!(RUSTFS_META_TMP_BUCKET, ".rustfs.sys/tmp"); assert_eq!(RUSTFS_META_TMP_DELETED_BUCKET, ".rustfs.sys/tmp/.trash"); assert_eq!(BUCKET_META_PREFIX, "buckets"); assert_eq!(FORMAT_CONFIG_FILE, "format.json"); assert_eq!(STORAGE_FORMAT_FILE, "xl.meta"); assert_eq!(STORAGE_FORMAT_FILE_BACKUP, "xl.meta.bkp"); assert_eq!(CHECK_PART_UNKNOWN, 0); assert_eq!(CHECK_PART_SUCCESS, 1); assert_eq!(CHECK_PART_DISK_NOT_FOUND, 2); assert_eq!(CHECK_PART_VOLUME_NOT_FOUND, 3); assert_eq!(CHECK_PART_FILE_NOT_FOUND, 4); assert_eq!(CHECK_PART_FILE_CORRUPT, 5); } /// Integration test for creating a local disk #[tokio::test] async fn test_new_disk_creation() { let test_dir = "./test_disk_creation"; fs::create_dir_all(&test_dir).await.unwrap(); let endpoint = Endpoint::try_from(test_dir).unwrap(); let opt = DiskOption { cleanup: false, health_check: true, }; let disk = new_disk(&endpoint, &opt).await; assert!(disk.is_ok()); let disk = disk.unwrap(); assert_eq!(disk.path(), rustfs_utils::canonicalize(test_dir).unwrap()); assert!(disk.is_local()); // Note: is_online() might return false for local disks without proper initialization // This is expected behavior for test environments // Clean up the test directory let _ = fs::remove_dir_all(&test_dir).await; } /// Test Disk enum pattern matching #[tokio::test] async fn test_disk_enum_methods() { let test_dir = "./test_disk_enum"; fs::create_dir_all(&test_dir).await.unwrap(); let endpoint = Endpoint::try_from(test_dir).unwrap(); let local_disk = LocalDisk::new(&endpoint, false).await.unwrap(); let disk = Disk::Local(Box::new(LocalDiskWrapper::new(Arc::new(local_disk), false))); // Test basic methods assert!(disk.is_local()); // Note: is_online() might return false for local disks without proper initialization // assert!(disk.is_online().await); // Note: host_name() for local disks might be empty or contain localhost // assert!(!disk.host_name().is_empty()); // Note: to_string() format might vary, so just check it's not empty assert!(!disk.to_string().is_empty()); // Test path method let path = disk.path(); assert!(path.exists()); // Test disk location let location = disk.get_disk_location(); assert!(location.valid() || (!location.valid() && endpoint.pool_idx < 0)); // Clean up the test directory let _ = fs::remove_dir_all(&test_dir).await; } #[tokio::test] async fn reset_health_for_store_init_retry_delegates_to_disk_variants() { let local_dir = tempfile::tempdir().unwrap(); let local_path = local_dir.path().to_str().expect("tempdir path should be utf8"); let mut local_endpoint = Endpoint::try_from(local_path).expect("local endpoint should parse"); local_endpoint.set_pool_index(0); local_endpoint.set_set_index(0); local_endpoint.set_disk_index(0); let local_disk = LocalDisk::new(&local_endpoint, false).await.unwrap(); let local_disk = Disk::Local(Box::new(LocalDiskWrapper::new(Arc::new(local_disk), false))); let mut remote_endpoint = Endpoint::try_from("http://remote-server:9000/data").expect("remote endpoint should parse"); remote_endpoint.set_pool_index(0); remote_endpoint.set_set_index(0); remote_endpoint.set_disk_index(1); let remote_disk = RemoteDisk::new( &remote_endpoint, &DiskOption { cleanup: false, health_check: false, }, Arc::new(crate::rpc::TcpHttpInternodeDataTransport), ) .await .unwrap(); let remote_disk = Disk::Remote(Box::new(remote_disk)); for disk in [&local_disk, &remote_disk] { disk.force_runtime_state_for_test(RuntimeDriveHealthState::Offline); assert_eq!(disk.runtime_state(), RuntimeDriveHealthState::Offline); disk.reset_health_for_store_init_retry(); assert_eq!(disk.runtime_state(), RuntimeDriveHealthState::Online); } } }