mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-13 00:26:53 +00:00
305 lines
11 KiB
Rust
305 lines
11 KiB
Rust
// Copyright 2024 RustFS Team
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
use super::*;
|
|
use crate::bucket::{
|
|
metadata::{BUCKET_TABLE_RESERVED_PREFIX, table_bucket_catalog_metadata_prefix},
|
|
utils::is_meta_bucketname,
|
|
};
|
|
use crate::global::get_global_bucket_monitor;
|
|
use crate::set_disk::get_lock_acquire_timeout;
|
|
use rustfs_storage_api::NamespaceLocking as _;
|
|
|
|
fn should_override_created_from_metadata(created: OffsetDateTime) -> bool {
|
|
created != OffsetDateTime::UNIX_EPOCH
|
|
}
|
|
|
|
fn validate_table_bucket_delete_allowed(
|
|
bucket: &str,
|
|
table_bucket_enabled: bool,
|
|
table_catalog_metadata_exists: bool,
|
|
) -> Result<()> {
|
|
if table_bucket_enabled && table_catalog_metadata_exists {
|
|
return Err(StorageError::BucketNotEmpty(bucket.to_string()));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn table_catalog_metadata_exists(bucket: &str) -> bool {
|
|
let local_disks = all_local_disk().await;
|
|
for disk in local_disks.iter() {
|
|
let catalog_path = disk.path().join(bucket).join(BUCKET_TABLE_RESERVED_PREFIX);
|
|
if has_xlmeta_files(&catalog_path).await {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
async fn validate_table_bucket_delete_guard(bucket: &str) -> Result<()> {
|
|
let table_bucket_enabled = metadata_sys::get(bucket)
|
|
.await
|
|
.is_ok_and(|metadata| metadata.table_bucket_enabled());
|
|
if table_bucket_enabled {
|
|
validate_table_bucket_delete_allowed(bucket, true, table_catalog_metadata_exists(bucket).await)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn bucket_delete_metadata_cleanup_prefixes(bucket: &str) -> [String; 2] {
|
|
[
|
|
table_bucket_catalog_metadata_prefix(bucket),
|
|
format!("{BUCKET_META_PREFIX}/{bucket}"),
|
|
]
|
|
}
|
|
|
|
impl ECStore {
|
|
#[instrument(skip(self))]
|
|
pub(super) async fn handle_make_bucket(&self, bucket: &str, opts: &MakeBucketOptions) -> Result<()> {
|
|
if !is_meta_bucketname(bucket)
|
|
&& let Err(err) = check_valid_bucket_name_strict(bucket)
|
|
{
|
|
return Err(StorageError::BucketNameInvalid(err.to_string()));
|
|
}
|
|
|
|
let _ns_guard = if !opts.no_lock {
|
|
let ns_lock = self.new_ns_lock(bucket, bucket).await?;
|
|
Some(
|
|
ns_lock
|
|
.get_write_lock(get_lock_acquire_timeout())
|
|
.await
|
|
.map_err(|e| match e {
|
|
rustfs_lock::error::LockError::QuorumNotReached { required, achieved } => {
|
|
StorageError::NamespaceLockQuorumUnavailable {
|
|
mode: "write",
|
|
bucket: bucket.to_string(),
|
|
object: bucket.to_string(),
|
|
required,
|
|
achieved,
|
|
}
|
|
}
|
|
other => StorageError::Lock(other),
|
|
})?,
|
|
)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
if let Err(err) = self.peer_sys.make_bucket(bucket, opts).await {
|
|
let err = to_object_err(err.into(), vec![bucket]);
|
|
if is_err_bucket_exists(&err)
|
|
&& let Err(heal_err) = self
|
|
.handle_heal_bucket(
|
|
bucket,
|
|
&HealOpts {
|
|
recreate: true,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await
|
|
{
|
|
warn!("best-effort bucket heal after BucketExists failed: {heal_err}");
|
|
}
|
|
if !is_err_bucket_exists(&err) {
|
|
error!("make bucket failed: {err}");
|
|
let _ = self
|
|
.delete_bucket(
|
|
bucket,
|
|
&DeleteBucketOptions {
|
|
no_lock: true,
|
|
no_recreate: true,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await;
|
|
}
|
|
return Err(err);
|
|
};
|
|
|
|
let mut meta = BucketMetadata::new(bucket);
|
|
|
|
meta.set_created(opts.created_at);
|
|
|
|
if opts.lock_enabled {
|
|
meta.object_lock_config_xml = crate::bucket::utils::serialize::<ObjectLockConfiguration>(&enableObjcetLockConfig)?;
|
|
meta.versioning_config_xml = crate::bucket::utils::serialize::<VersioningConfiguration>(&enableVersioningConfig)?;
|
|
}
|
|
|
|
if opts.versioning_enabled {
|
|
meta.versioning_config_xml = crate::bucket::utils::serialize::<VersioningConfiguration>(&enableVersioningConfig)?;
|
|
}
|
|
|
|
meta.save().await?;
|
|
|
|
set_bucket_metadata(bucket.to_string(), meta).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[instrument(skip(self))]
|
|
pub(super) async fn handle_get_bucket_info(&self, bucket: &str, opts: &BucketOptions) -> Result<BucketInfo> {
|
|
let mut info = self.peer_sys.get_bucket_info(bucket, opts).await?;
|
|
|
|
if let Ok(sys) = metadata_sys::get(bucket).await {
|
|
if should_override_created_from_metadata(sys.created) {
|
|
info.created = Some(sys.created);
|
|
}
|
|
info.versioning = sys.versioning();
|
|
info.object_locking = sys.object_locking();
|
|
}
|
|
|
|
Ok(info)
|
|
}
|
|
|
|
#[instrument(skip(self))]
|
|
pub(super) async fn handle_list_bucket(&self, opts: &BucketOptions) -> Result<Vec<BucketInfo>> {
|
|
// TODO: opts.cached
|
|
|
|
let mut buckets = self.peer_sys.list_bucket(opts).await?;
|
|
|
|
if !opts.no_metadata {
|
|
for bucket in buckets.iter_mut() {
|
|
if let Ok(created) = metadata_sys::created_at(&bucket.name).await
|
|
&& should_override_created_from_metadata(created)
|
|
{
|
|
bucket.created = Some(created);
|
|
}
|
|
}
|
|
}
|
|
Ok(buckets)
|
|
}
|
|
|
|
#[instrument(skip(self))]
|
|
pub(super) async fn handle_delete_bucket(&self, bucket: &str, opts: &DeleteBucketOptions) -> Result<()> {
|
|
if is_meta_bucketname(bucket) {
|
|
return Err(StorageError::BucketNameInvalid(bucket.to_string()));
|
|
}
|
|
|
|
if let Err(err) = check_valid_bucket_name(bucket) {
|
|
return Err(StorageError::BucketNameInvalid(err.to_string()));
|
|
}
|
|
|
|
let _ns_guard = if !opts.no_lock {
|
|
let ns_lock = self.new_ns_lock(bucket, bucket).await?;
|
|
Some(
|
|
ns_lock
|
|
.get_write_lock(get_lock_acquire_timeout())
|
|
.await
|
|
.map_err(|e| match e {
|
|
rustfs_lock::error::LockError::QuorumNotReached { required, achieved } => {
|
|
StorageError::NamespaceLockQuorumUnavailable {
|
|
mode: "write",
|
|
bucket: bucket.to_string(),
|
|
object: bucket.to_string(),
|
|
required,
|
|
achieved,
|
|
}
|
|
}
|
|
other => StorageError::Lock(other),
|
|
})?,
|
|
)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// Check bucket exists before deletion (per S3 API spec)
|
|
// If bucket doesn't exist, return NoSuchBucket error
|
|
if let Err(err) = self.peer_sys.get_bucket_info(bucket, &BucketOptions::default()).await {
|
|
// Convert DiskError to StorageError for comparison
|
|
let storage_err: StorageError = err.into();
|
|
if is_err_bucket_not_found(&storage_err) {
|
|
return Err(StorageError::BucketNotFound(bucket.to_string()));
|
|
}
|
|
return Err(to_object_err(storage_err, vec![bucket]));
|
|
}
|
|
|
|
validate_table_bucket_delete_guard(bucket).await?;
|
|
|
|
// Check bucket is empty before deletion (per S3 API spec)
|
|
// If bucket is not empty (contains actual objects with xl.meta files) and force
|
|
// is not set, return BucketNotEmpty error.
|
|
// Note: Empty directories (left after object deletion) should NOT count as objects.
|
|
if !opts.force {
|
|
let local_disks = all_local_disk().await;
|
|
for disk in local_disks.iter() {
|
|
// Check if bucket directory contains any xl.meta files (actual objects)
|
|
// We recursively scan for xl.meta files to determine if bucket has objects
|
|
// Use the disk's root path to construct bucket path
|
|
let bucket_path = disk.path().join(bucket);
|
|
if has_xlmeta_files(&bucket_path).await {
|
|
return Err(StorageError::BucketNotEmpty(bucket.to_string()));
|
|
}
|
|
}
|
|
}
|
|
|
|
self.peer_sys
|
|
.delete_bucket(bucket, opts)
|
|
.await
|
|
.map_err(|e| to_object_err(e.into(), vec![bucket]))?;
|
|
|
|
// TODO: replication opts.srdelete_op
|
|
|
|
// Delete internal metadata after the bucket is gone so stale catalog records cannot be reused
|
|
// if the same bucket name is created again.
|
|
for prefix in bucket_delete_metadata_cleanup_prefixes(bucket) {
|
|
self.delete_all(RUSTFS_META_BUCKET, prefix.as_str()).await?;
|
|
}
|
|
if let Some(monitor) = get_global_bucket_monitor() {
|
|
monitor.delete_bucket(bucket);
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{
|
|
bucket_delete_metadata_cleanup_prefixes, should_override_created_from_metadata, validate_table_bucket_delete_allowed,
|
|
};
|
|
use crate::bucket::metadata::table_bucket_catalog_metadata_prefix;
|
|
use crate::error::StorageError;
|
|
use time::OffsetDateTime;
|
|
|
|
#[test]
|
|
fn should_not_override_when_metadata_created_is_unix_epoch() {
|
|
assert!(!should_override_created_from_metadata(OffsetDateTime::UNIX_EPOCH));
|
|
}
|
|
|
|
#[test]
|
|
fn should_override_when_metadata_created_is_valid_time() {
|
|
let created = OffsetDateTime::from_unix_timestamp(1704067200).expect("valid timestamp");
|
|
assert!(should_override_created_from_metadata(created));
|
|
}
|
|
|
|
#[test]
|
|
fn table_bucket_delete_guard_rejects_remaining_catalog_metadata() {
|
|
let err = validate_table_bucket_delete_allowed("table-bucket", true, true).unwrap_err();
|
|
|
|
assert!(matches!(err, StorageError::BucketNotEmpty(bucket) if bucket == "table-bucket"));
|
|
assert!(validate_table_bucket_delete_allowed("table-bucket", true, false).is_ok());
|
|
assert!(validate_table_bucket_delete_allowed("regular-bucket", false, true).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn bucket_delete_metadata_cleanup_removes_internal_table_catalog_prefix() {
|
|
let prefixes = bucket_delete_metadata_cleanup_prefixes("analytics");
|
|
|
|
assert!(prefixes.contains(&table_bucket_catalog_metadata_prefix("analytics")));
|
|
assert!(prefixes.contains(&"buckets/analytics".to_string()));
|
|
}
|
|
}
|