fix(ecstore): handle metadata-less bucket residue (#6591)

* fix(ecstore): handle metadata-less bucket residue

Diagnose metadata-less on-disk residue before non-force DeleteBucket reaches physical deletion, and keep scanner-discovered metadata-missing objects on a non-destructive heal path.

Add explicit heal --remove cleanup for unversioned metadata-less data directories, using the existing data-dir delete primitive and fail-closed shape checks so pre-commit or unknown residue is preserved.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(connect): adapt offline array validator

Wrap the filesystem summary validator in a closure so Option::is_some_and can pass the concrete array reference accepted by serde_json::Value::as_array.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(connect): remove redundant offline test clones

Move the temporary path into the swap closure after deriving the output path, keeping clippy's redundant-clone lint clean for offline bundle tests.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
houseme
2026-08-26 09:17:19 +08:00
committed by GitHub
parent 2bd83a5276
commit 0f987714a1
7 changed files with 595 additions and 21 deletions
+3 -3
View File
@@ -335,7 +335,7 @@ fn valid_payload(collector: OfflineCollector, value: &serde_json::Value) -> bool
&& object.get("totalBytes").and_then(serde_json::Value::as_u64).is_some()
&& object.get("underPressure").and_then(serde_json::Value::as_bool).is_some()
}),
OfflineCollector::FilesystemSummary => value.as_array().is_some_and(ordered_strings),
OfflineCollector::FilesystemSummary => value.as_array().is_some_and(|values| ordered_strings(values)),
OfflineCollector::NetworkSummary => value.as_object().is_some_and(|object| {
object.len() == 2
&& object.get("bondCount").and_then(serde_json::Value::as_u64).is_some()
@@ -902,7 +902,7 @@ mod tests {
let moved = temp.path().join("moved");
fs::create_dir(&original).expect("original output directory");
let swapped_output = original.join("bundle.zip");
let swap_original = original.clone();
let swap_original = original;
let swap_moved = moved.clone();
test_support::set(
Stage::BeforePublish,
@@ -933,7 +933,7 @@ mod tests {
let moved = temp.path().join("publish-moved");
fs::create_dir(&original).expect("original publish directory");
let swapped_output = original.join("bundle.zip");
let swap_original = original.clone();
let swap_original = original;
let swap_moved = moved.clone();
test_support::set(
Stage::Rename,
+8 -1
View File
@@ -307,7 +307,7 @@ impl From<StorageError> for ApiError {
StorageError::InvalidArgument(_, _, _) => S3ErrorCode::InvalidArgument,
StorageError::MethodNotAllowed => S3ErrorCode::MethodNotAllowed,
StorageError::BucketNotFound(_) => S3ErrorCode::NoSuchBucket,
StorageError::BucketNotEmpty(_) => S3ErrorCode::BucketNotEmpty,
StorageError::BucketNotEmpty(_) | StorageError::BucketNotEmptyWithDetails { .. } => S3ErrorCode::BucketNotEmpty,
StorageError::BucketNameInvalid(_) => S3ErrorCode::InvalidBucketName,
StorageError::ObjectNameInvalid(_, _) => S3ErrorCode::InvalidArgument,
StorageError::BucketExists(_) => S3ErrorCode::BucketAlreadyOwnedByYou,
@@ -706,6 +706,13 @@ mod tests {
(StorageError::MethodNotAllowed, S3ErrorCode::MethodNotAllowed),
(StorageError::BucketNotFound("test".into()), S3ErrorCode::NoSuchBucket),
(StorageError::BucketNotEmpty("test".into()), S3ErrorCode::BucketNotEmpty),
(
StorageError::BucketNotEmptyWithDetails {
bucket: "test".into(),
details: "metadata-less residue".into(),
},
S3ErrorCode::BucketNotEmpty,
),
(StorageError::BucketNameInvalid("test".into()), S3ErrorCode::InvalidBucketName),
(
StorageError::ObjectNameInvalid("test".into(), "test".into()),