From 3b5c74ac1617d540b2aca76d338cbc0a8ada3b16 Mon Sep 17 00:00:00 2001 From: Ben McClelland Date: Mon, 7 Sep 2026 18:47:09 -0700 Subject: [PATCH] feat: add storage class to put object flows input A new glaicer mode for the archiving backend needs the storage class supplied to the backend for put object, create multipart upload, and browser post object input. --- s3api/controllers/bucket-post.go | 2 ++ s3api/controllers/bucket-post_test.go | 3 ++ s3api/controllers/object-post.go | 2 ++ s3api/controllers/object-post_test.go | 4 +++ s3api/controllers/object-put.go | 2 ++ s3api/controllers/object-put_test.go | 52 +++++++++++++++++++++++++++ s3response/s3response.go | 1 + 7 files changed, 66 insertions(+) diff --git a/s3api/controllers/bucket-post.go b/s3api/controllers/bucket-post.go index 4ddea6a5..721ad7cf 100644 --- a/s3api/controllers/bucket-post.go +++ b/s3api/controllers/bucket-post.go @@ -127,6 +127,7 @@ func (c S3ApiController) POSTObject(ctx fiber.Ctx) (*Response, error) { cacheControl := parsed.Fields["cache-control"] expires := parsed.Fields["expires"] websiteRedirectLocation := parsed.Fields["x-amz-website-redirect-location"] + storageClass := parsed.Fields["x-amz-storage-class"] key := parsed.Fields["key"] @@ -233,6 +234,7 @@ func (c S3ApiController) POSTObject(ctx fiber.Ctx) (*Response, error) { ContentLength: &parsed.ContentLength, Tagging: &tagging, Metadata: metadata, + StorageClass: types.StorageClass(storageClass), ChecksumCRC32: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc32]), ChecksumCRC32C: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc32c]), ChecksumSHA1: utils.GetStringPtr(checksums[types.ChecksumAlgorithmSha1]), diff --git a/s3api/controllers/bucket-post_test.go b/s3api/controllers/bucket-post_test.go index ef1d64d1..eba1683b 100644 --- a/s3api/controllers/bucket-post_test.go +++ b/s3api/controllers/bucket-post_test.go @@ -567,6 +567,7 @@ func TestS3ApiController_POSTObject(t *testing.T) { []any{"eq", "$content-encoding", "gzip"}, []any{"eq", "$content-language", "en-US"}, []any{"eq", "$expires", "Fri, 21 Mar 2026 00:00:00 GMT"}, + []any{"eq", "$x-amz-storage-class", string(types.StorageClassGlacier)}, }), "file": "ignored", "x-amz-signature": "ignored", @@ -580,6 +581,7 @@ func TestS3ApiController_POSTObject(t *testing.T) { "content-encoding": "gzip", "content-language": "en-US", "expires": "Fri, 21 Mar 2026 00:00:00 GMT", + "x-amz-storage-class": string(types.StorageClassGlacier), }, FileRdr: newMockFileReader("payload"), ContentLength: int64(len("payload")), @@ -719,6 +721,7 @@ func TestS3ApiController_POSTObject(t *testing.T) { assert.Equal(t, "en-US", *putObjectInput.ContentLanguage) assert.Equal(t, "max-age=60", *putObjectInput.CacheControl) assert.Equal(t, "Fri, 21 Mar 2026 00:00:00 GMT", *putObjectInput.Expires) + assert.Equal(t, types.StorageClassGlacier, putObjectInput.StorageClass) assert.Equal(t, int64(len("payload")), *putObjectInput.ContentLength) assert.Equal(t, "project=alpha+team", *putObjectInput.Tagging) assert.Equal(t, map[string]string{"owner": "alice"}, putObjectInput.Metadata) diff --git a/s3api/controllers/object-post.go b/s3api/controllers/object-post.go index bc785972..407e8e16 100644 --- a/s3api/controllers/object-post.go +++ b/s3api/controllers/object-post.go @@ -151,6 +151,7 @@ func (c S3ApiController) CreateMultipartUpload(ctx fiber.Ctx) (*Response, error) tagging := ctx.Get("X-Amz-Tagging") expires := ctx.Get("Expires") websiteRedirectLocation := ctx.Get("X-Amz-Website-Redirect-Location") + storageClass := ctx.Get("X-Amz-Storage-Class") legalHoldHdr := ctx.Get("X-Amz-Object-Lock-Legal-Hold") lockModeHdr := ctx.Get("X-Amz-Object-Lock-Mode") objLockDate := ctx.Get("X-Amz-Object-Lock-Retain-Until-Date") @@ -241,6 +242,7 @@ func (c S3ApiController) CreateMultipartUpload(ctx fiber.Ctx) (*Response, error) ObjectLockMode: objLockState.ObjectLockMode, ObjectLockLegalHoldStatus: objLockState.LegalHoldStatus, Metadata: metadata, + StorageClass: types.StorageClass(storageClass), ChecksumAlgorithm: checksumAlgorithm, ChecksumType: checksumType, }) diff --git a/s3api/controllers/object-post_test.go b/s3api/controllers/object-post_test.go index ecf99f72..dd5253d8 100644 --- a/s3api/controllers/object-post_test.go +++ b/s3api/controllers/object-post_test.go @@ -312,6 +312,7 @@ func TestS3ApiController_CreateMultipartUpload(t *testing.T) { headers: map[string]string{ "x-amz-checksum-algorithm": string(types.ChecksumAlgorithmCrc32), "x-amz-checksum-type": string(types.ChecksumTypeComposite), + "X-Amz-Storage-Class": string(types.StorageClassGlacier), }, }, output: testOutput{ @@ -332,6 +333,9 @@ func TestS3ApiController_CreateMultipartUpload(t *testing.T) { t.Run(tt.name, func(t *testing.T) { be := &BackendMock{ CreateMultipartUploadFunc: func(contextMoqParam context.Context, createMultipartUploadInput s3response.CreateMultipartUploadInput) (s3response.InitiateMultipartUploadResult, error) { + if tt.name == "successful response" && createMultipartUploadInput.StorageClass != types.StorageClassGlacier { + t.Fatalf("expected storage class %q, got %q", types.StorageClassGlacier, createMultipartUploadInput.StorageClass) + } return tt.input.beRes.(s3response.InitiateMultipartUploadResult), tt.input.beErr }, GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { diff --git a/s3api/controllers/object-put.go b/s3api/controllers/object-put.go index f88260ce..c5046656 100644 --- a/s3api/controllers/object-put.go +++ b/s3api/controllers/object-put.go @@ -668,6 +668,7 @@ func (c S3ApiController) PutObject(ctx fiber.Ctx) (*Response, error) { expires := ctx.Get("Expires") websiteRedirectLocation := ctx.Get("X-Amz-Website-Redirect-Location") tagging := ctx.Get("x-amz-tagging") + storageClass := ctx.Get("X-Amz-Storage-Class") legalHoldHdr := ctx.Get("X-Amz-Object-Lock-Legal-Hold") lockModeHdr := ctx.Get("X-Amz-Object-Lock-Mode") objLockDate := ctx.Get("X-Amz-Object-Lock-Retain-Until-Date") @@ -803,6 +804,7 @@ func (c S3ApiController) PutObject(ctx fiber.Ctx) (*Response, error) { ObjectLockRetainUntilDate: &objLock.RetainUntilDate, ObjectLockMode: objLock.ObjectLockMode, ObjectLockLegalHoldStatus: objLock.LegalHoldStatus, + StorageClass: types.StorageClass(storageClass), ChecksumAlgorithm: algorithm, ChecksumCRC32: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc32]), ChecksumCRC32C: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc32c]), diff --git a/s3api/controllers/object-put_test.go b/s3api/controllers/object-put_test.go index 4aa7347a..7893a9a9 100644 --- a/s3api/controllers/object-put_test.go +++ b/s3api/controllers/object-put_test.go @@ -1169,6 +1169,58 @@ func TestS3ApiController_PutObject(t *testing.T) { emptyStringPtr := &str objSize := int64(120) + t.Run("forwards storage class", func(t *testing.T) { + be := &BackendMock{ + PutObjectFunc: func(_ context.Context, input s3response.PutObjectInput) (s3response.PutObjectOutput, error) { + if input.StorageClass != types.StorageClassGlacier { + t.Fatalf("expected storage class %q, got %q", types.StorageClassGlacier, input.StorageClass) + } + return s3response.PutObjectOutput{ETag: "etag", VersionID: "version-id"}, nil + }, + GetBucketPolicyFunc: func(_ context.Context, _ string) ([]byte, error) { + return nil, s3err.GetAPIError(s3err.ErrAccessDenied) + }, + GetObjectLockConfigurationFunc: func(_ context.Context, _ string) ([]byte, error) { + return nil, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound) + }, + GetBucketVersioningFunc: func(_ context.Context, _ string) (s3response.GetBucketVersioningOutput, error) { + return s3response.GetBucketVersioningOutput{}, s3err.GetAPIError(s3err.ErrNotImplemented) + }, + } + + ctrl := S3ApiController{be: be} + testController(t, ctrl.PutObject, &Response{ + Headers: map[string]*string{ + "ETag": utils.GetStringPtr("etag"), + "x-amz-checksum-crc32": nil, + "x-amz-checksum-crc32c": nil, + "x-amz-checksum-crc64nvme": nil, + "x-amz-checksum-sha1": nil, + "x-amz-checksum-sha256": nil, + "x-amz-checksum-sha512": nil, + "x-amz-checksum-md5": nil, + "x-amz-checksum-xxhash64": nil, + "x-amz-checksum-xxhash3": nil, + "x-amz-checksum-xxhash128": nil, + "x-amz-checksum-type": nil, + "x-amz-version-id": utils.GetStringPtr("version-id"), + "x-amz-object-size": nil, + }, + MetaOpts: &MetaOptions{ + BucketOwner: "root", + ObjectETag: utils.GetStringPtr("etag"), + ContentLength: 0, + ObjectSize: 0, + EventName: s3event.EventObjectCreatedPut, + }, + }, nil, ctxInputs{ + locals: defaultLocals, + headers: map[string]string{ + "X-Amz-Storage-Class": string(types.StorageClassGlacier), + }, + }) + }) + tests := []struct { name string input testInput diff --git a/s3response/s3response.go b/s3response/s3response.go index 3c67ebdd..357f8bc3 100644 --- a/s3response/s3response.go +++ b/s3response/s3response.go @@ -608,6 +608,7 @@ type PutObjectInput struct { ObjectLockMode types.ObjectLockMode ObjectLockLegalHoldStatus types.ObjectLockLegalHoldStatus ChecksumAlgorithm types.ChecksumAlgorithm + StorageClass types.StorageClass Metadata map[string]string Body io.Reader