diff --git a/backend/posix/posix.go b/backend/posix/posix.go index 2f38ae4e..e1400628 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -2326,6 +2326,12 @@ func (p *Posix) DeleteObject(ctx context.Context, input *s3.DeleteObjectInput) ( // AWS returns success if the object does not exist return &s3.DeleteObjectOutput{}, nil } + if !strings.HasSuffix(object, "/") && fi.IsDir() { + // requested object is expecting a file, but the object is a + // directory. treat this as a non-existent object. + // AWS returns success if the object does not exist + return &s3.DeleteObjectOutput{}, nil + } err = os.Remove(objpath) if errors.Is(err, fs.ErrNotExist) { @@ -2491,6 +2497,9 @@ func (p *Posix) GetObject(_ context.Context, input *s3.GetObjectInput) (*s3.GetO if strings.HasSuffix(object, "/") && !fi.IsDir() { return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) } + if !strings.HasSuffix(object, "/") && fi.IsDir() { + return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) + } if *input.VersionId != "" { isDelMarker, err := p.isObjDeleteMarker(bucket, object) @@ -2722,6 +2731,9 @@ func (p *Posix) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3. if strings.HasSuffix(object, "/") && !fi.IsDir() { return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) } + if !strings.HasSuffix(object, "/") && fi.IsDir() { + return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) + } if *input.VersionId != "" { isDelMarker, err := p.isObjDeleteMarker(bucket, object) @@ -2892,6 +2904,9 @@ func (p *Posix) CopyObject(ctx context.Context, input *s3.CopyObjectInput) (*s3. if strings.HasSuffix(srcObject, "/") && !fi.IsDir() { return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) } + if !strings.HasSuffix(srcObject, "/") && fi.IsDir() { + return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) + } mdmap := make(map[string]string) p.loadUserMetaData(srcBucket, srcObject, mdmap) diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go index cc97e993..8d46516d 100644 --- a/tests/integration/group-tests.go +++ b/tests/integration/group-tests.go @@ -142,6 +142,7 @@ func TestHeadObject(s *S3Conf) { HeadObject_invalid_part_number(s) HeadObject_non_existing_mp(s) HeadObject_mp_success(s) + HeadObject_directory_object_noslash(s) HeadObject_non_existing_dir_object(s) HeadObject_with_contenttype(s) HeadObject_success(s) @@ -155,6 +156,7 @@ func TestGetObjectAttributes(s *S3Conf) { func TestGetObject(s *S3Conf) { GetObject_non_existing_key(s) + GetObject_directory_object_noslash(s) GetObject_invalid_ranges(s) GetObject_with_meta(s) GetObject_success(s) @@ -191,6 +193,7 @@ func TestListObjectsV2(s *S3Conf) { func TestDeleteObject(s *S3Conf) { DeleteObject_non_existing_object(s) + DeleteObject_directory_object_noslash(s) DeleteObject_non_existing_dir_object(s) DeleteObject_success(s) DeleteObject_success_status_code(s) @@ -640,6 +643,7 @@ func GetIntTests() IntTests { "HeadObject_invalid_part_number": HeadObject_invalid_part_number, "HeadObject_non_existing_mp": HeadObject_non_existing_mp, "HeadObject_mp_success": HeadObject_mp_success, + "HeadObject_directory_object_noslash": HeadObject_directory_object_noslash, "HeadObject_non_existing_dir_object": HeadObject_non_existing_dir_object, "HeadObject_name_too_long": HeadObject_name_too_long, "HeadObject_with_contenttype": HeadObject_with_contenttype, @@ -648,6 +652,7 @@ func GetIntTests() IntTests { "GetObjectAttributes_non_existing_object": GetObjectAttributes_non_existing_object, "GetObjectAttributes_existing_object": GetObjectAttributes_existing_object, "GetObject_non_existing_key": GetObject_non_existing_key, + "GetObject_directory_object_noslash": GetObject_directory_object_noslash, "GetObject_invalid_ranges": GetObject_invalid_ranges, "GetObject_with_meta": GetObject_with_meta, "GetObject_success": GetObject_success, @@ -675,6 +680,7 @@ func GetIntTests() IntTests { "ListObjectsV2_all_objs_max_keys": ListObjectsV2_all_objs_max_keys, "ListObjectsV2_list_all_objs": ListObjectsV2_list_all_objs, "DeleteObject_non_existing_object": DeleteObject_non_existing_object, + "DeleteObject_directory_object_noslash": DeleteObject_directory_object_noslash, "DeleteObject_name_too_long": DeleteObject_name_too_long, "DeleteObject_non_existing_dir_object": DeleteObject_non_existing_dir_object, "DeleteObject_success": DeleteObject_success, diff --git a/tests/integration/tests.go b/tests/integration/tests.go index 83fd1a4e..7f2c23ef 100644 --- a/tests/integration/tests.go +++ b/tests/integration/tests.go @@ -3009,6 +3009,35 @@ func HeadObject_non_existing_dir_object(s *S3Conf) error { }) } +func HeadObject_directory_object_noslash(s *S3Conf) error { + testName := "HeadObject_directory_object_noslash" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-obj/" + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: &bucket, + Key: &obj, + }) + cancel() + if err != nil { + return err + } + + obj = "my-obj" + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: &bucket, + Key: &obj, + }) + cancel() + if err := checkSdkApiErr(err, "NotFound"); err != nil { + return err + } + + return nil + }) +} + const defaultContentType = "binary/octet-stream" func HeadObject_with_contenttype(s *S3Conf) error { @@ -3229,6 +3258,35 @@ func GetObject_non_existing_key(s *S3Conf) error { }) } +func GetObject_directory_object_noslash(s *S3Conf) error { + testName := "GetObject_directory_object_noslash" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-obj/" + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: &bucket, + Key: &obj, + }) + cancel() + if err != nil { + return err + } + + obj = "my-obj" + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: &bucket, + Key: &obj, + }) + cancel() + var bae *types.NoSuchKey + if !errors.As(err, &bae) { + return err + } + return nil + }) +} + func GetObject_invalid_ranges(s *S3Conf) error { testName := "GetObject_invalid_ranges" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { @@ -4226,6 +4284,45 @@ func DeleteObject_non_existing_object(s *S3Conf) error { }) } +func DeleteObject_directory_object_noslash(s *S3Conf) error { + testName := "DeleteObject_directory_object_noslash" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-obj/" + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: &bucket, + Key: &obj, + }) + cancel() + if err != nil { + return err + } + + obj = "my-obj" + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.DeleteObject(ctx, &s3.DeleteObjectInput{ + Bucket: &bucket, + Key: &obj, + }) + cancel() + if err != nil { + return err + } + + // the delete above should succeed, but the object should not be deleted + // since it should not correctly match the directory name + // so the below head object should also succeed + obj = "my-obj/" + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: &bucket, + Key: &obj, + }) + cancel() + return err + }) +} + func DeleteObject_non_existing_dir_object(s *S3Conf) error { testName := "DeleteObject_non_existing_dir_object" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {