From 87059053197e91ad771798e6f348d129808c4cfa Mon Sep 17 00:00:00 2001 From: niksis02 Date: Wed, 2 Apr 2025 22:31:05 +0400 Subject: [PATCH] fix: Fixes non empty directory objects deletion Fixes #1181 `DeleteObjects` should remove non-empty directory objects, which has been uploaded as a separate object. e.g Upload -> `foo/bar` Upload -> `foo/` Delete -> `foo/` The last action call should succeed. The PR introduces changes which removes `ETag` from the directory object attempting to `delete`, which has been uploaded as a separate object. --- backend/posix/posix.go | 17 +++++++++++++- tests/integration/group-tests.go | 2 ++ tests/integration/tests.go | 40 ++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/backend/posix/posix.go b/backend/posix/posix.go index c357e2d1..d7113c9f 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -3224,7 +3224,22 @@ func (p *Posix) DeleteObject(ctx context.Context, input *s3.DeleteObjectInput) ( return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) } if errors.Is(err, syscall.ENOTEMPTY) { - return nil, s3err.GetAPIError(s3err.ErrDirectoryNotEmpty) + // If the directory object has been uploaded explicitly + // remove the directory object (remove the ETag) + _, err = p.meta.RetrieveAttribute(nil, objpath, "", etagkey) + if err != nil && !errors.Is(err, meta.ErrNoSuchKey) { + return nil, fmt.Errorf("get object etag: %w", err) + } + if errors.Is(err, meta.ErrNoSuchKey) { + return nil, s3err.GetAPIError(s3err.ErrDirectoryNotEmpty) + } + + err = p.meta.DeleteAttribute(objpath, "", etagkey) + if err != nil { + return nil, fmt.Errorf("delete object etag: %w", err) + } + + return &s3.DeleteObjectOutput{}, nil } if err != nil { return nil, fmt.Errorf("delete object: %w", err) diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go index 12b12116..a00a41ee 100644 --- a/tests/integration/group-tests.go +++ b/tests/integration/group-tests.go @@ -252,6 +252,7 @@ func TestDeleteObject(s *S3Conf) { DeleteObject_directory_object_noslash(s) DeleteObject_non_existing_dir_object(s) DeleteObject_directory_object(s) + DeleteObject_non_empty_dir_obj(s) DeleteObject_success(s) DeleteObject_success_status_code(s) } @@ -913,6 +914,7 @@ func GetIntTests() IntTests { "ListObjectVersions_VD_success": ListObjectVersions_VD_success, "DeleteObject_non_existing_object": DeleteObject_non_existing_object, "DeleteObject_directory_object_noslash": DeleteObject_directory_object_noslash, + "DeleteObject_non_empty_dir_obj": DeleteObject_non_empty_dir_obj, "DeleteObject_name_too_long": DeleteObject_name_too_long, "CopyObject_overwrite_same_dir_object": CopyObject_overwrite_same_dir_object, "CopyObject_overwrite_same_file_object": CopyObject_overwrite_same_file_object, diff --git a/tests/integration/tests.go b/tests/integration/tests.go index 0dcc2eb6..9090fc98 100644 --- a/tests/integration/tests.go +++ b/tests/integration/tests.go @@ -5468,6 +5468,46 @@ func DeleteObject_directory_object_noslash(s *S3Conf) error { }) } +func DeleteObject_non_empty_dir_obj(s *S3Conf) error { + testName := "DeleteObject_non_empty_dir_obj" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + objToDel := "foo/" + nestedObj := objToDel + "bar" + _, err := putObjects(s3client, []string{nestedObj, objToDel}, bucket) + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.DeleteObject(ctx, &s3.DeleteObjectInput{ + Bucket: &bucket, + Key: &objToDel, + }) + cancel() + if err != nil { + return err + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + res, err := s3client.ListObjects(ctx, &s3.ListObjectsInput{ + Bucket: &bucket, + }) + cancel() + if err != nil { + return err + } + + if len(res.Contents) != 1 { + return fmt.Errorf("expected the object list length to be 1, instead got %v", len(res.Contents)) + } + if *res.Contents[0].Key != nestedObj { + return fmt.Errorf("expected the object key to be %v, instead got %v", nestedObj, *res.Contents[0].Key) + } + + return nil + }) +} + func DeleteObject_directory_not_empty(s *S3Conf) error { testName := "DeleteObject_directory_not_empty" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {