From 77459720ba2c0a557a929aee24be7a39603399fc Mon Sep 17 00:00:00 2001 From: niksis02 Date: Wed, 5 Nov 2025 19:51:56 +0400 Subject: [PATCH] feat: adds x-amz-tagging-count support for HeadObject Closes #1346 `GetObject` and `HeadObject` return the `x-amz-tagging-count` header in the response, which specifies the number of tags associated with the object. This was already supported for `GetObject`, but missing for `HeadObject`. This implementation adds support for `HeadObject` in `azure` and `posix` and updates the integration tests to cover this functionality for `GetObject`. --- backend/azure/azure.go | 5 +++++ backend/posix/posix.go | 11 +++++++++++ s3api/controllers/object-head.go | 1 + s3api/controllers/object-head_test.go | 1 + tests/integration/GetObject.go | 8 ++++++++ tests/integration/HeadObject.go | 9 +++++++++ 6 files changed, 35 insertions(+) diff --git a/backend/azure/azure.go b/backend/azure/azure.go index 57b9dd4..276cb19 100644 --- a/backend/azure/azure.go +++ b/backend/azure/azure.go @@ -583,6 +583,11 @@ func (az *Azure) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3 } } + if resp.TagCount != nil { + tagcount := int32(*resp.TagCount) + result.TagCount = &tagcount + } + return result, nil } diff --git a/backend/posix/posix.go b/backend/posix/posix.go index 13db839..cbf8971 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -4026,6 +4026,16 @@ func (p *Posix) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3. } } + var tagCount *int32 + tags, err := p.getAttrTags(bucket, object, versionId) + if err != nil && !errors.Is(err, s3err.GetAPIError(s3err.ErrBucketTaggingNotFound)) { + return nil, err + } + if tags != nil { + tc := int32(len(tags)) + tagCount = &tc + } + return &s3.HeadObjectOutput{ ContentLength: &length, AcceptRanges: backend.GetPtrFromString("bytes"), @@ -4050,6 +4060,7 @@ func (p *Posix) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3. ChecksumSHA256: checksums.SHA256, ChecksumCRC64NVME: checksums.CRC64NVME, ChecksumType: cType, + TagCount: tagCount, }, nil } diff --git a/s3api/controllers/object-head.go b/s3api/controllers/object-head.go index a9be6b3..7890368 100644 --- a/s3api/controllers/object-head.go +++ b/s3api/controllers/object-head.go @@ -150,6 +150,7 @@ func (c S3ApiController) HeadObject(ctx *fiber.Ctx) (*Response, error) { "x-amz-storage-class": utils.ConvertToStringPtr(res.StorageClass), "x-amz-checksum-type": utils.ConvertToStringPtr(res.ChecksumType), "x-amz-object-lock-retain-until-date": utils.FormatDatePtrToString(res.ObjectLockRetainUntilDate, time.RFC3339), + "x-amz-tagging-count": utils.ConvertPtrToStringPtr(res.TagCount), }, MetaOpts: &MetaOptions{ BucketOwner: parsedAcl.Owner, diff --git a/s3api/controllers/object-head_test.go b/s3api/controllers/object-head_test.go index 7729656..a854c6b 100644 --- a/s3api/controllers/object-head_test.go +++ b/s3api/controllers/object-head_test.go @@ -147,6 +147,7 @@ func TestS3ApiController_HeadObject(t *testing.T) { "x-amz-checksum-type": nil, "x-amz-object-lock-retain-until-date": nil, "Last-Modified": nil, + "x-amz-tagging-count": nil, "Content-Type": utils.GetStringPtr("application/xml"), "Content-Length": utils.GetStringPtr("100"), }, diff --git a/tests/integration/GetObject.go b/tests/integration/GetObject.go index 8ed5cef..97640ee 100644 --- a/tests/integration/GetObject.go +++ b/tests/integration/GetObject.go @@ -527,6 +527,7 @@ func GetObject_success(s *S3Conf) error { Expires: &expires, CacheControl: &cacheControl, Metadata: meta, + Tagging: getPtr("key=value&key1=val1"), }, s3client) if err != nil { return err @@ -577,6 +578,13 @@ func GetObject_success(s *S3Conf) error { return fmt.Errorf("expected the object metadata to be %v, instead got %v", meta, out.Metadata) } + var tagCount int32 + if out.TagCount != nil { + tagCount = *out.TagCount + } + if tagCount != 2 { + return fmt.Errorf("expected tag count to be 2, instead got %v", tagCount) + } bdy, err := io.ReadAll(out.Body) if err != nil { diff --git a/tests/integration/HeadObject.go b/tests/integration/HeadObject.go index e612298..988c800 100644 --- a/tests/integration/HeadObject.go +++ b/tests/integration/HeadObject.go @@ -566,6 +566,7 @@ func HeadObject_success(s *S3Conf) error { ContentLanguage: &cLang, CacheControl: &cacheControl, Expires: &expires, + Tagging: getPtr("key=value"), }, s3client) if err != nil { return err @@ -620,6 +621,14 @@ func HeadObject_success(s *S3Conf) error { return fmt.Errorf("expected the storage class to be %v, instead got %v", types.StorageClassStandard, out.StorageClass) } + tagCount := int32(0) + if out.TagCount != nil { + tagCount = *out.TagCount + } + + if tagCount != 1 { + return fmt.Errorf("expected the tagcount to be 1, instead got %v", tagCount) + } return nil })