Upstream versity/versitygw state of the touched files before 3dc5e993d2

This commit is contained in:
upstream committed 2026-10-07 19:40:28 +02:00
1 parent b3bf48bdcb
commit 5a14104c85
11 files changed
+10142 -7

No files matched your search

+639
View File
@@ -0,0 +1,639 @@
// Copyright 2023 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package controllers
import (
"fmt"
"math"
"net/http"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/gofiber/fiber/v3"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/debuglogger"
"github.com/versity/versitygw/s3api/utils"
"github.com/versity/versitygw/s3err"
"github.com/versity/versitygw/s3response"
)
func (c S3ApiController) GetObjectTagging(ctx fiber.Ctx) (*Response, error) {
bucket := ctx.Params("bucket")
key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
versionId := ctx.Query("versionId")
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
if err := utils.ValidateVersionId(ctx); err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
action := auth.GetObjectTaggingAction
if versionId != "" {
action = auth.GetObjectVersionTaggingAction
}
err := c.verifyAccess(ctx, auth.AccessOptions{
Acl: parsedAcl,
AclPermission: auth.PermissionRead,
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Object: key,
Actions: []auth.Action{action},
IsPublicRequest: isPublicBucket,
})
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
data, err := c.be.GetObjectTagging(ctx.RequestCtx(), bucket, key, versionId)
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
tags := s3response.Tagging{
TagSet: s3response.TagSet{Tags: []s3response.Tag{}},
}
for key, val := range data {
tags.TagSet.Tags = append(tags.TagSet.Tags,
s3response.Tag{Key: key, Value: val})
}
return &Response{
Data: tags,
Headers: map[string]*string{
"x-amz-version-id": &versionId,
},
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, nil
}
func (c S3ApiController) GetObjectRetention(ctx fiber.Ctx) (*Response, error) {
bucket := ctx.Params("bucket")
key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
versionId := ctx.Query("versionId")
// context locals
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
if err := utils.ValidateVersionId(ctx); err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
err := c.verifyAccess(ctx, auth.AccessOptions{
Acl: parsedAcl,
AclPermission: auth.PermissionRead,
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Object: key,
Actions: []auth.Action{auth.GetObjectRetentionAction},
IsPublicRequest: isPublicBucket,
})
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
data, err := c.be.GetObjectRetention(ctx.RequestCtx(), bucket, key, versionId)
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
retention, err := auth.ParseObjectLockRetentionOutput(data)
return &Response{
Data: retention,
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
func (c S3ApiController) GetObjectLegalHold(ctx fiber.Ctx) (*Response, error) {
bucket := ctx.Params("bucket")
key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
versionId := ctx.Query("versionId")
// context locals
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
if err := utils.ValidateVersionId(ctx); err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
err := c.verifyAccess(ctx, auth.AccessOptions{
Acl: parsedAcl,
AclPermission: auth.PermissionRead,
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Object: key,
Actions: []auth.Action{auth.GetObjectLegalHoldAction},
IsPublicRequest: isPublicBucket,
})
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
data, err := c.be.GetObjectLegalHold(ctx.RequestCtx(), bucket, key, versionId)
return &Response{
Data: auth.ParseObjectLegalHoldOutput(data),
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
func (c S3ApiController) GetObjectAcl(ctx fiber.Ctx) (*Response, error) {
bucket := ctx.Params("bucket")
key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
// context locals
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
err := c.verifyAccess(ctx, auth.AccessOptions{
Acl: parsedAcl,
AclPermission: auth.PermissionReadAcp,
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Object: key,
Actions: []auth.Action{auth.GetObjectAclAction},
IsPublicRequest: isPublicBucket,
})
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
res, err := c.be.GetObjectAcl(ctx.RequestCtx(), &s3.GetObjectAclInput{
Bucket: &bucket,
Key: &key,
})
return &Response{
Data: res,
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
func (c S3ApiController) ListParts(ctx fiber.Ctx) (*Response, error) {
bucket := ctx.Params("bucket")
key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
ssec := utils.ExtractSSECHeaders(ctx)
uploadId := ctx.Query("uploadId")
partNumberMarker := ctx.Query("part-number-marker")
maxPartsStr := ctx.Query("max-parts")
// context locals
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
err := c.verifyAccess(ctx, auth.AccessOptions{
Acl: parsedAcl,
AclPermission: auth.PermissionRead,
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Object: key,
Actions: []auth.Action{auth.ListMultipartUploadPartsAction},
IsPublicRequest: isPublicBucket,
})
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
// parse/validate the part number marker
_, err = utils.ParseMaxLimiter(partNumberMarker, utils.LimiterTypePartNumberMarker)
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
// parse the max parts
maxParts, err := utils.ParseMaxLimiter(maxPartsStr, utils.LimiterTypeMaxParts)
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
res, err := c.be.ListParts(ctx.RequestCtx(), &s3.ListPartsInput{
Bucket: &bucket,
Key: &key,
UploadId: &uploadId,
PartNumberMarker: &partNumberMarker,
MaxParts: &maxParts,
SSECustomerAlgorithm: ssec.Algorithm,
SSECustomerKey: ssec.Key,
SSECustomerKeyMD5: ssec.KeyMD5,
})
return &Response{
Data: res,
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
func (c S3ApiController) GetObjectAttributes(ctx fiber.Ctx) (*Response, error) {
bucket := ctx.Params("bucket")
key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
versionId := ctx.Query("versionId")
maxPartsStr := ctx.Get("X-Amz-Max-Parts")
partNumberMarker := ctx.Get("X-Amz-Part-Number-Marker")
// context locals
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
if err := utils.ValidateVersionId(ctx); err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
action := auth.GetObjectAttributesAction
if versionId != "" {
action = auth.GetObjectVersionAttributesAction
}
err := c.verifyAccess(ctx, auth.AccessOptions{
Acl: parsedAcl,
AclPermission: auth.PermissionRead,
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Object: key,
Actions: []auth.Action{action},
IsPublicRequest: isPublicBucket,
})
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
ssec, err := c.parseSSECHeaders(ctx)
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
var maxParts *int32
// parse max parts
parsed, err := utils.ParseMaxLimiter(maxPartsStr, utils.LimiterTypeMaxParts)
if err == nil {
maxParts = &parsed
}
// parse the object attributes
attrs, err := utils.ParseObjectAttributes(ctx)
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
res, err := c.be.GetObjectAttributes(ctx.RequestCtx(),
&s3.GetObjectAttributesInput{
SSECustomerAlgorithm: ssec.Algorithm,
SSECustomerKey: ssec.Key,
SSECustomerKeyMD5: ssec.KeyMD5,
Bucket: &bucket,
Key: &key,
PartNumberMarker: &partNumberMarker,
MaxParts: maxParts,
VersionId: &versionId,
})
if err != nil {
headers := map[string]*string{
"x-amz-version-id": res.VersionId,
}
if res.DeleteMarker != nil && *res.DeleteMarker {
headers["x-amz-delete-marker"] = utils.GetStringPtr("true")
}
return &Response{
Headers: headers,
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
headers := map[string]*string{
"x-amz-version-id": res.VersionId,
"Last-Modified": utils.FormatDatePtrToString(res.LastModified, iso8601TimeFormatExtended),
}
if res.DeleteMarker != nil && *res.DeleteMarker {
headers["x-amz-delete-marker"] = utils.GetStringPtr("true")
}
return &Response{
Headers: headers,
Data: utils.FilterObjectAttributes(attrs, res),
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
func (c S3ApiController) GetObject(ctx fiber.Ctx) (*Response, error) {
bucket := ctx.Params("bucket")
key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
versionId := ctx.Query("versionId")
acceptRange := ctx.Get("Range")
checksumMode := types.ChecksumMode(strings.ToUpper(ctx.Get("x-amz-checksum-mode")))
partNumberQuery := int32(fiber.Query[int](ctx, "partNumber", -1))
// Extract response override query parameters
responseOverrides := map[string]*string{
"Cache-Control": utils.GetQueryParam(ctx, "response-cache-control"),
"Content-Disposition": utils.GetQueryParam(ctx, "response-content-disposition"),
"Content-Encoding": utils.GetQueryParam(ctx, "response-content-encoding"),
"Content-Language": utils.GetQueryParam(ctx, "response-content-language"),
"Content-Type": utils.GetQueryParam(ctx, "response-content-type"),
"Expires": utils.GetQueryParam(ctx, "response-expires"),
}
// Check if any response override parameters are present
hasResponseOverrides := false
for _, override := range responseOverrides {
if override != nil {
hasResponseOverrides = true
break
}
}
// context locals
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
isPublicBucketRequest := utils.ContextKeyPublicBucket.IsSet(ctx)
utils.ContextKeySkipResBodyLog.Set(ctx, true)
// Validate that response override parameters are not used with anonymous requests
if hasResponseOverrides && isPublicBucketRequest {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, s3err.GetAPIError(s3err.ErrAnonymousResponseHeaders)
}
if err := utils.ValidateVersionId(ctx); err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
action := auth.GetObjectAction
if ctx.Request().URI().QueryArgs().Has("versionId") {
action = auth.GetObjectVersionAction
}
err := c.verifyAccess(ctx, auth.AccessOptions{
Acl: parsedAcl,
AclPermission: auth.PermissionRead,
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Object: key,
Actions: []auth.Action{action},
IsPublicRequest: isPublicBucketRequest,
})
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
ssec, err := c.parseSSECHeaders(ctx)
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
var partNumber *int32
if ctx.Request().URI().QueryArgs().Has("partNumber") {
if partNumberQuery < minPartNumber || partNumberQuery > int32(c.effectiveMpMaxParts()) {
debuglogger.Logf("invalid part number: %d", partNumberQuery)
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, s3err.GetInvalidArgumentErr(s3err.InvalidArgPartNumber, ctx.Query("partNumber"))
}
if acceptRange != "" {
debuglogger.Logf("Range and partNumber cannot both be specified")
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, s3err.GetAPIError(s3err.ErrRangeAndPartNumber)
}
partNumber = &partNumberQuery
}
// validate the checksum mode
if checksumMode != "" && checksumMode != types.ChecksumModeEnabled {
debuglogger.Logf("invalid x-amz-checksum-mode header value: %v", checksumMode)
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, s3err.GetInvalidChecksumHeaderErr("x-amz-checksum-mode")
}
conditionalHeaders := utils.ParsePreconditionHeaders(ctx)
res, err := c.be.GetObject(ctx.RequestCtx(), &s3.GetObjectInput{
SSECustomerAlgorithm: ssec.Algorithm,
SSECustomerKey: ssec.Key,
SSECustomerKeyMD5: ssec.KeyMD5,
Bucket: &bucket,
Key: &key,
Range: &acceptRange,
IfMatch: conditionalHeaders.IfMatch,
IfNoneMatch: conditionalHeaders.IfNoneMatch,
IfModifiedSince: conditionalHeaders.IfModSince,
IfUnmodifiedSince: conditionalHeaders.IfUnmodeSince,
VersionId: &versionId,
ChecksumMode: checksumMode,
PartNumber: partNumber,
})
if err != nil {
var headers map[string]*string
if res != nil {
headers = map[string]*string{
"x-amz-delete-marker": utils.GetStringPtr("true"),
"Last-Modified": utils.FormatDatePtrToString(res.LastModified, timefmt),
}
}
return &Response{
Headers: headers,
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
// Set x-amz-meta-... headers
utils.SetMetaHeaders(ctx, res.Metadata)
status := http.StatusOK
if res.ContentRange != nil && *res.ContentRange != "" {
status = http.StatusPartialContent
}
if res.Body != nil {
// -1 will stream response body until EOF if content length not set
contentLen := -1
if res.ContentLength != nil {
if *res.ContentLength > int64(math.MaxInt) {
debuglogger.Logf("content length %v int overflow",
*res.ContentLength)
return &Response{
MetaOpts: &MetaOptions{
ContentLength: utils.GetInt64(res.ContentLength),
BucketOwner: parsedAcl.Owner,
Status: status,
},
}, s3err.GetInvalidRangeErr("", *res.ContentLength)
}
contentLen = int(*res.ContentLength)
}
utils.StreamResponseBody(ctx, res.Body, contentLen)
}
return &Response{
Headers: map[string]*string{
"ETag": res.ETag,
"x-amz-restore": res.Restore,
"accept-ranges": res.AcceptRanges,
"Content-Range": res.ContentRange,
"Content-Disposition": utils.ApplyOverride(res.ContentDisposition, responseOverrides["Content-Disposition"]),
"Content-Encoding": utils.ApplyOverride(res.ContentEncoding, responseOverrides["Content-Encoding"]),
"Content-Language": utils.ApplyOverride(res.ContentLanguage, responseOverrides["Content-Language"]),
"Cache-Control": utils.ApplyOverride(res.CacheControl, responseOverrides["Cache-Control"]),
"Expires": utils.ApplyOverride(res.ExpiresString, responseOverrides["Expires"]),
"x-amz-website-redirect-location": res.WebsiteRedirectLocation,
"x-amz-checksum-crc32": res.ChecksumCRC32,
"x-amz-checksum-crc64nvme": res.ChecksumCRC64NVME,
"x-amz-checksum-crc32c": res.ChecksumCRC32C,
"x-amz-checksum-sha1": res.ChecksumSHA1,
"x-amz-checksum-sha256": res.ChecksumSHA256,
"x-amz-checksum-sha512": res.ChecksumSHA512,
"x-amz-checksum-md5": res.ChecksumMD5,
"x-amz-checksum-xxhash64": res.ChecksumXXHASH64,
"x-amz-checksum-xxhash3": res.ChecksumXXHASH3,
"x-amz-checksum-xxhash128": res.ChecksumXXHASH128,
"Content-Type": utils.ApplyOverride(res.ContentType, responseOverrides["Content-Type"]),
"x-amz-version-id": res.VersionId,
"Content-Length": utils.ConvertPtrToStringPtr(res.ContentLength),
"x-amz-mp-parts-count": utils.ConvertPtrToStringPtr(res.PartsCount),
"x-amz-tagging-count": utils.ConvertPtrToStringPtr(res.TagCount),
"x-amz-object-lock-mode": utils.ConvertToStringPtr(res.ObjectLockMode),
"x-amz-object-lock-legal-hold": utils.ConvertToStringPtr(res.ObjectLockLegalHoldStatus),
"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),
"Last-Modified": utils.FormatDatePtrToString(res.LastModified, timefmt),
"x-amz-server-side-encryption-customer-algorithm": res.SSECustomerAlgorithm,
"x-amz-server-side-encryption-customer-key-MD5": res.SSECustomerKeyMD5,
},
MetaOpts: &MetaOptions{
ContentLength: utils.GetInt64(res.ContentLength),
BucketOwner: parsedAcl.Owner,
Status: status,
},
}, nil
}
File diff suppressed because it is too large. Load diff
+231
View File
@@ -0,0 +1,231 @@
// Copyright 2023 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package controllers
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/gofiber/fiber/v3"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/debuglogger"
"github.com/versity/versitygw/s3api/utils"
"github.com/versity/versitygw/s3err"
)
func (c S3ApiController) HeadObject(ctx fiber.Ctx) (*Response, error) {
// context locals
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
// url values
bucket := ctx.Params("bucket")
partNumberQuery := int32(fiber.Query(ctx, "partNumber", -1))
versionId := ctx.Query("versionId")
objRange := ctx.Get("Range")
key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
// Extract response override query parameters
responseOverrides := map[string]*string{
"Cache-Control": utils.GetQueryParam(ctx, "response-cache-control"),
"Content-Disposition": utils.GetQueryParam(ctx, "response-content-disposition"),
"Content-Encoding": utils.GetQueryParam(ctx, "response-content-encoding"),
"Content-Language": utils.GetQueryParam(ctx, "response-content-language"),
"Content-Type": utils.GetQueryParam(ctx, "response-content-type"),
"Expires": utils.GetQueryParam(ctx, "response-expires"),
}
// Check if any response override parameters are present
hasResponseOverrides := false
for _, override := range responseOverrides {
if override != nil {
hasResponseOverrides = true
break
}
}
// Validate that response override parameters are not used with anonymous requests
if hasResponseOverrides && isPublicBucket {
debuglogger.Logf("anonymous access is denied with response override params")
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, s3err.GetAPIError(s3err.ErrAnonymousResponseHeaders)
}
if err := utils.ValidateVersionId(ctx); err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
action := auth.GetObjectAction
if ctx.Request().URI().QueryArgs().Has("versionId") {
action = auth.GetObjectVersionAction
}
err := c.verifyAccess(ctx,
auth.AccessOptions{
Acl: parsedAcl,
AclPermission: auth.PermissionRead,
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Object: key,
Actions: []auth.Action{action},
IsPublicRequest: isPublicBucket,
})
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
ssec, err := c.parseSSECHeaders(ctx)
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
var partNumber *int32
if ctx.Request().URI().QueryArgs().Has("partNumber") {
if partNumberQuery < minPartNumber || partNumberQuery > int32(c.effectiveMpMaxParts()) {
debuglogger.Logf("invalid part number: %d", partNumberQuery)
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, s3err.GetInvalidArgumentErr(s3err.InvalidArgPartNumber, ctx.Query("partNumber"))
}
if objRange != "" {
debuglogger.Logf("Range and partNumber cannot both be specified")
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, s3err.GetAPIError(s3err.ErrRangeAndPartNumber)
}
partNumber = &partNumberQuery
}
checksumMode := types.ChecksumMode(strings.ToUpper(ctx.Get("x-amz-checksum-mode")))
if checksumMode != "" && checksumMode != types.ChecksumModeEnabled {
debuglogger.Logf("invalid x-amz-checksum-mode header value: %v", checksumMode)
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, s3err.GetInvalidChecksumHeaderErr("x-amz-checksum-mode")
}
conditionalHeaders := utils.ParsePreconditionHeaders(ctx)
res, err := c.be.HeadObject(ctx.RequestCtx(),
&s3.HeadObjectInput{
SSECustomerAlgorithm: ssec.Algorithm,
SSECustomerKey: ssec.Key,
SSECustomerKeyMD5: ssec.KeyMD5,
Bucket: &bucket,
Key: &key,
PartNumber: partNumber,
VersionId: &versionId,
ChecksumMode: checksumMode,
Range: &objRange,
IfMatch: conditionalHeaders.IfMatch,
IfNoneMatch: conditionalHeaders.IfNoneMatch,
IfModifiedSince: conditionalHeaders.IfModSince,
IfUnmodifiedSince: conditionalHeaders.IfUnmodeSince,
})
if err != nil {
var headers map[string]*string
if res != nil {
headers = map[string]*string{
"x-amz-delete-marker": utils.GetStringPtr("true"),
"Last-Modified": utils.GetStringPtr(res.LastModified.UTC().Format(timefmt)),
}
}
return &Response{
Headers: headers,
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
// Set the metadata headers
utils.SetMetaHeaders(ctx, res.Metadata)
status := http.StatusOK
if res.ContentRange != nil && *res.ContentRange != "" {
status = http.StatusPartialContent
}
return &Response{
Headers: map[string]*string{
"Content-Range": res.ContentRange,
"Content-Disposition": utils.ApplyOverride(res.ContentDisposition, responseOverrides["Content-Disposition"]),
"Content-Encoding": utils.ApplyOverride(res.ContentEncoding, responseOverrides["Content-Encoding"]),
"Content-Language": utils.ApplyOverride(res.ContentLanguage, responseOverrides["Content-Language"]),
"Cache-Control": utils.ApplyOverride(res.CacheControl, responseOverrides["Cache-Control"]),
"Content-Length": utils.ConvertPtrToStringPtr(res.ContentLength),
"Content-Type": utils.ApplyOverride(res.ContentType, responseOverrides["Content-Type"]),
"Expires": utils.ApplyOverride(res.ExpiresString, responseOverrides["Expires"]),
"x-amz-website-redirect-location": res.WebsiteRedirectLocation,
"ETag": res.ETag,
"Last-Modified": utils.FormatDatePtrToString(res.LastModified, timefmt),
"x-amz-restore": res.Restore,
"accept-ranges": res.AcceptRanges,
"x-amz-checksum-crc32": res.ChecksumCRC32,
"x-amz-checksum-crc64nvme": res.ChecksumCRC64NVME,
"x-amz-checksum-crc32c": res.ChecksumCRC32C,
"x-amz-checksum-sha1": res.ChecksumSHA1,
"x-amz-checksum-sha256": res.ChecksumSHA256,
"x-amz-checksum-sha512": res.ChecksumSHA512,
"x-amz-checksum-md5": res.ChecksumMD5,
"x-amz-checksum-xxhash64": res.ChecksumXXHASH64,
"x-amz-checksum-xxhash3": res.ChecksumXXHASH3,
"x-amz-checksum-xxhash128": res.ChecksumXXHASH128,
"x-amz-version-id": res.VersionId,
"x-amz-mp-parts-count": utils.ConvertPtrToStringPtr(res.PartsCount),
"x-amz-object-lock-mode": utils.ConvertToStringPtr(res.ObjectLockMode),
"x-amz-object-lock-legal-hold": utils.ConvertToStringPtr(res.ObjectLockLegalHoldStatus),
"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),
"x-amz-server-side-encryption-customer-algorithm": res.SSECustomerAlgorithm,
"x-amz-server-side-encryption-customer-key-MD5": res.SSECustomerKeyMD5,
},
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
Status: status,
},
}, nil
}
+316
View File
@@ -0,0 +1,316 @@
// Copyright 2023 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package controllers
import (
"context"
"net/http"
"testing"
"time"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/s3api/utils"
"github.com/versity/versitygw/s3err"
)
func TestS3ApiController_HeadObject(t *testing.T) {
tm := time.Now()
cLength := int64(100)
failingBeRes := &s3.HeadObjectOutput{
LastModified: &tm,
}
tests := []struct {
name string
input testInput
output testOutput
}{
{
name: "empty versionId query param",
input: testInput{
locals: defaultLocals,
queries: map[string]string{
"versionId": "",
},
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetInvalidArgumentErr(s3err.InvalidArgEmptyVersionId, ""),
},
},
{
name: "verify access fails",
input: testInput{
locals: accessDeniedLocals,
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrAccessDenied),
},
},
{
name: "anonymous access with override params",
input: testInput{
locals: map[utils.ContextKey]any{
utils.ContextKeyIsRoot: true,
utils.ContextKeyParsedAcl: auth.ACL{
Owner: "root",
},
utils.ContextKeyAccount: auth.Account{
Access: "root",
Role: auth.RoleAdmin,
},
utils.ContextKeyRegion: "us-east-1",
utils.ContextKeyPublicBucket: true,
},
queries: map[string]string{
"response-expires": "something",
},
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrAnonymousResponseHeaders),
},
},
{
name: "invalid part number",
input: testInput{
locals: defaultLocals,
queries: map[string]string{
"partNumber": "-4",
"versionId": "01BX5ZZKBKACTAV9WEVGEMMVRZ",
},
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetInvalidArgumentErr(s3err.InvalidArgPartNumber, "-4"),
},
},
{
name: "both partNumber and Range",
input: testInput{
locals: defaultLocals,
queries: map[string]string{
"partNumber": "6",
},
headers: map[string]string{
"Range": "bytes=1-3",
},
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrRangeAndPartNumber),
},
},
{
name: "invalid checksum mode",
input: testInput{
locals: defaultLocals,
headers: map[string]string{
"x-amz-checksum-mode": "invalid_checksum_mode",
},
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetInvalidChecksumHeaderErr("x-amz-checksum-mode"),
},
},
{
name: "backend returns error",
input: testInput{
locals: defaultLocals,
beErr: s3err.GetAPIError(s3err.ErrInvalidAccessKeyID),
beRes: failingBeRes,
},
output: testOutput{
response: &Response{
Headers: map[string]*string{
"x-amz-delete-marker": utils.GetStringPtr("true"),
"Last-Modified": utils.GetStringPtr(tm.UTC().Format(timefmt)),
},
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrInvalidAccessKeyID),
},
},
{
name: "successful response",
input: testInput{
queries: map[string]string{
"partNumber": "4",
},
locals: defaultLocals,
headers: map[string]string{
"x-amz-checksum-mode": "enabled",
},
beRes: &s3.HeadObjectOutput{
ETag: utils.GetStringPtr("ETag"),
ContentType: utils.GetStringPtr("application/xml"),
ContentLength: &cLength,
},
},
output: testOutput{
response: &Response{
Headers: map[string]*string{
"ETag": utils.GetStringPtr("ETag"),
"x-amz-restore": nil,
"accept-ranges": nil,
"Content-Range": nil,
"Content-Disposition": nil,
"Content-Encoding": nil,
"Content-Language": nil,
"Cache-Control": nil,
"Expires": nil,
"x-amz-website-redirect-location": nil,
"x-amz-checksum-crc32": nil,
"x-amz-checksum-crc64nvme": nil,
"x-amz-checksum-crc32c": 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-version-id": nil,
"x-amz-mp-parts-count": nil,
"x-amz-object-lock-mode": nil,
"x-amz-object-lock-legal-hold": nil,
"x-amz-storage-class": nil,
"x-amz-checksum-type": nil,
"x-amz-object-lock-retain-until-date": nil,
"Last-Modified": nil,
"x-amz-tagging-count": nil,
"x-amz-server-side-encryption-customer-algorithm": nil,
"x-amz-server-side-encryption-customer-key-MD5": nil,
"Content-Type": utils.GetStringPtr("application/xml"),
"Content-Length": utils.GetStringPtr("100"),
},
MetaOpts: &MetaOptions{
BucketOwner: "root",
Status: http.StatusOK,
},
},
},
},
{
name: "successful partial response",
input: testInput{
locals: defaultLocals,
beRes: &s3.HeadObjectOutput{
ContentRange: utils.GetStringPtr("bytes 10-20/100"),
},
},
output: testOutput{
response: &Response{
Headers: map[string]*string{
"ETag": nil,
"x-amz-restore": nil,
"accept-ranges": nil,
"Content-Range": utils.GetStringPtr("bytes 10-20/100"),
"Content-Disposition": nil,
"Content-Encoding": nil,
"Content-Language": nil,
"Cache-Control": nil,
"Expires": nil,
"x-amz-website-redirect-location": nil,
"x-amz-checksum-crc32": nil,
"x-amz-checksum-crc64nvme": nil,
"x-amz-checksum-crc32c": 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-version-id": nil,
"x-amz-mp-parts-count": nil,
"x-amz-object-lock-mode": nil,
"x-amz-object-lock-legal-hold": nil,
"x-amz-storage-class": nil,
"x-amz-checksum-type": nil,
"x-amz-object-lock-retain-until-date": nil,
"Last-Modified": nil,
"x-amz-tagging-count": nil,
"x-amz-server-side-encryption-customer-algorithm": nil,
"x-amz-server-side-encryption-customer-key-MD5": nil,
"Content-Type": nil,
"Content-Length": nil,
},
MetaOpts: &MetaOptions{
BucketOwner: "root",
Status: http.StatusPartialContent,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
be := &BackendMock{
HeadObjectFunc: func(contextMoqParam context.Context, headObjectInput *s3.HeadObjectInput) (*s3.HeadObjectOutput, error) {
return tt.input.beRes.(*s3.HeadObjectOutput), tt.input.beErr
},
GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
},
}
ctrl := S3ApiController{
be: be,
}
testController(
t,
ctrl.HeadObject,
tt.output.response,
tt.output.err,
ctxInputs{
locals: tt.input.locals,
queries: tt.input.queries,
headers: tt.input.headers,
})
})
}
}
-5
View File
@@ -26,7 +26,6 @@ type InvalidArgErrorCode int
const ( const (
InvalidArgMaxBuckets InvalidArgErrorCode = iota InvalidArgMaxBuckets InvalidArgErrorCode = iota
InvalidArgNegativeMaxKeys InvalidArgNegativeMaxKeys
InvalidArgStartAfter
InvalidArgObjectAttributes InvalidArgObjectAttributes
InvalidArgPartNumber InvalidArgPartNumber
InvalidArgCompleteMpPartNumber InvalidArgCompleteMpPartNumber
@@ -91,10 +90,6 @@ var invalidArgErrResponses = map[InvalidArgErrorCode]InvalidArgumentError{
Description: "max-keys cannot be negative", Description: "max-keys cannot be negative",
ArgumentName: "maxKeys", ArgumentName: "maxKeys",
}, },
InvalidArgStartAfter: {
Description: "startAfter only supported in REST.GET.BUCKET with list-type=2",
ArgumentName: "start-after",
},
InvalidArgObjectAttributes: { InvalidArgObjectAttributes: {
Description: "Invalid attribute name specified.", Description: "Invalid attribute name specified.",
ArgumentName: "x-amz-object-attributes", ArgumentName: "x-amz-object-attributes",
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
-2
View File
@@ -315,7 +315,6 @@ func TestListObjects(ts *TestState) {
ts.Run(ListObjects_delimiter) ts.Run(ListObjects_delimiter)
ts.Run(ListObjects_max_keys_none) ts.Run(ListObjects_max_keys_none)
ts.Run(ListObjects_marker_not_from_obj_list) ts.Run(ListObjects_marker_not_from_obj_list)
ts.Run(ListObjects_start_after_not_supported)
ts.Run(ListObjects_list_all_objs) ts.Run(ListObjects_list_all_objs)
ts.Run(ListObjects_nested_dir_file_objs) ts.Run(ListObjects_nested_dir_file_objs)
ts.Run(ListObjects_check_owner) ts.Run(ListObjects_check_owner)
@@ -3118,7 +3117,6 @@ func GetIntTests() IntTests {
"ListObjects_delimiter": ListObjects_delimiter, "ListObjects_delimiter": ListObjects_delimiter,
"ListObjects_max_keys_none": ListObjects_max_keys_none, "ListObjects_max_keys_none": ListObjects_max_keys_none,
"ListObjects_marker_not_from_obj_list": ListObjects_marker_not_from_obj_list, "ListObjects_marker_not_from_obj_list": ListObjects_marker_not_from_obj_list,
"ListObjects_start_after_not_supported": ListObjects_start_after_not_supported,
"ListObjects_list_all_objs": ListObjects_list_all_objs, "ListObjects_list_all_objs": ListObjects_list_all_objs,
"ListObjects_nested_dir_file_objs": ListObjects_nested_dir_file_objs, "ListObjects_nested_dir_file_objs": ListObjects_nested_dir_file_objs,
"ListObjects_check_owner": ListObjects_check_owner, "ListObjects_check_owner": ListObjects_check_owner,
File diff suppressed because it is too large. Load diff
+201
View File
@@ -0,0 +1,201 @@
#!/usr/bin/env bats
# Copyright 2026 Versity Software
# This file is licensed under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http:#www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
load ./bats-support/load
load ./bats-assert/load
source ./tests/commands/get_object.sh
source ./tests/drivers/complete_multipart_upload/complete_multipart_upload_rest.sh
source ./tests/drivers/create_bucket/create_bucket_rest.sh
source ./tests/drivers/create_multipart_upload/create_multipart_upload_rest.sh
source ./tests/setup.sh
# tags: curl,GetObject,HeadObject,range
@test "REST - range download and compare" {
run get_file_name
assert_success
test_file="$output"
run get_bucket_name "$BUCKET_ONE_NAME"
assert_success
bucket_name="$output"
run setup_bucket_and_large_file_v2 "$bucket_name" "$test_file"
assert_success
run put_object "rest" "$TEST_FILE_FOLDER/$test_file" "$bucket_name" "$test_file"
assert_success
download_chunk_size="2000000"
run download_and_compare_file "$TEST_FILE_FOLDER/$test_file" "$bucket_name" "$test_file" "$TEST_FILE_FOLDER/$test_file-copy" "$download_chunk_size"
assert_success
}
# tags: curl,PutObject,GetObject,special-chars,encoding
@test "REST - put, get object, encoded name" {
run get_bucket_name "$BUCKET_ONE_NAME"
assert_success
bucket_name="$output"
file_name=" \"<>\\^\`{}|+&?%"
run setup_bucket_and_file_v2 "$bucket_name" "$file_name"
assert_success
run put_object_rest_special_chars "$TEST_FILE_FOLDER/$file_name" "$bucket_name" "$file_name/$file_name"
assert_success
run list_check_single_object "$bucket_name" "$file_name/$file_name"
assert_success
run get_object_rest_special_chars "$bucket_name" "$file_name/$file_name" "$TEST_FILE_FOLDER/${file_name}-copy"
assert_success
run compare_files "$TEST_FILE_FOLDER/$file_name" "$TEST_FILE_FOLDER/${file_name}-copy"
assert_success
run delete_object_rest "$bucket_name" "$file_name/$file_name"
assert_success
}
# tags: curl,GetObject,invalid-header,x-amz-content-sha256
@test "REST - GetObject w/invalid payload type" {
run get_file_name
assert_success
test_file="$output"
run get_bucket_name "$BUCKET_ONE_NAME"
assert_success
bucket_name="$output"
run setup_bucket_and_file_v2 "$bucket_name" "$test_file"
assert_success
run get_object_rest_with_invalid_streaming_type "$bucket_name" "$test_file"
assert_success
}
# tags: curl,GetObject,partNumber,invalid-header
@test "REST - GetObject - part number 2 w/o multipart upload" {
run setup_bucket_and_file_v3 "$BUCKET_ONE_NAME"
assert_success
read -r bucket_name file_name <<< "$output"
run put_object_rest "$TEST_FILE_FOLDER/$file_name" "$bucket_name" "$file_name"
assert_success
run send_rest_go_command_expect_error "416" "InvalidPartNumber" "not satisfiable" "-bucketName" "$bucket_name" "-objectKey" "$file_name" "-query" "partNumber=2"
assert_success
}
# tags: curl,GetObject,partNumber,range,Content-Range
@test "REST - GetObject - part number 1 returns 206, Content-Range header" {
if [ "$DIRECT" != "true" ]; then
skip "https://github.com/versity/versitygw/issues/2074"
fi
run setup_bucket_and_file_v3 "$BUCKET_ONE_NAME"
assert_success
read -r bucket_name file_name <<< "$output"
run put_object_rest "$TEST_FILE_FOLDER/$file_name" "$bucket_name" "$file_name"
assert_success
run send_rest_go_command_callback "206" "check_for_header_key_and_value" "-bucketName" "$bucket_name" "-objectKey" "$file_name" "-query" "partNumber=1" \
"--" "Content-Range" "bytes 0-9/10"
assert_success
}
# tags: curl,GetObject,invalid-header,response-headers
@test "REST - GetObject - response query - invalid response type" {
if [ "$DIRECT" != "true" ]; then
skip "https://github.com/versity/versitygw/issues/2075"
fi
run setup_bucket_and_file_v3 "$BUCKET_ONE_NAME"
assert_success
read -r bucket_name file_name <<< "$output"
run put_object_rest "$TEST_FILE_FOLDER/$file_name" "$bucket_name" "$file_name"
assert_success
local bad_response_query="response-gibberish"
run send_rest_go_command_expect_error "400" "InvalidArgument" "$bad_response_query is not in the set of overridable response headers" \
"-bucketName" "$bucket_name" "-objectKey" "$file_name" "-query" "$bad_response_query=dummy"
assert_success
}
# tags: curl,GetObject,response-cache-control,response-headers
@test "REST - GetObject - response-cache-control" {
run test_get_object_with_custom_content_header "cache-control" "dummy"
assert_success
}
# tags: curl,GetObject,response-content-disposition,response-headers
@test "REST - GetObject - response-content-disposition" {
run test_get_object_with_custom_content_header "content-disposition" "dummy"
assert_success
}
# tags: curl,GetObject,response-content-encoding,response-headers
@test "REST - GetObject - response-content-encoding" {
run test_get_object_with_custom_content_header "content-encoding" "dummy"
assert_success
}
# tags: curl,GetObject,response-content-language,response-headers
@test "REST - GetObject - response-content-language" {
run test_get_object_with_custom_content_header "content-language" "dummy"
assert_success
}
# tags: curl,GetObject,response-content-type,response-headers
@test "REST - GetObject - response-content-type" {
run test_get_object_with_custom_content_header "content-type" "dummy"
assert_success
}
# tags: curl,GetObject,response-expires,response-headers
@test "REST - GetObject - response-expires" {
run test_get_object_with_custom_content_header "expires" "dummy+ one"
assert_success
}
# tags: curl,GetObject,multipart,partNumber
@test "REST - GetObject - partNumber w/multipart" {
run setup_bucket_and_large_file_v3 "$BUCKET_ONE_NAME" 8
assert_success
read -r bucket_name test_file <<< "$output"
run split_file_irregular "$TEST_FILE_FOLDER/$test_file" 5242880
assert_success
read -r part_one part_two <<< "$output"
log 5 "part one: $part_one, part two: $part_two"
run perform_multipart_upload_rest_variable_parts "$bucket_name" "$test_file" "$part_one" "$part_two"
assert_success
run get_file_size "$part_one"
assert_success
part_size="$output"
run get_file_size "$TEST_FILE_FOLDER/$test_file"
assert_success
file_size="$output"
run send_get_object_with_part_number_validate_response "$bucket_name" "$test_file" "1" "$part_one" "$file_size" "$part_size"
assert_success
run send_get_object_with_part_number_validate_response "$bucket_name" "$test_file" "2" "$part_two" "$file_size" "$part_size"
assert_success
}
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env bats
# Copyright 2026 Versity Software
# This file is licensed under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http:#www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
load ./bats-support/load
load ./bats-assert/load
source ./tests/setup.sh
source ./tests/drivers/create_bucket/create_bucket_rest.sh
source ./tests/drivers/create_multipart_upload/create_multipart_upload_rest.sh
source ./tests/drivers/complete_multipart_upload/complete_multipart_upload_rest.sh
source ./tests/drivers/get_object_attributes/get_object_attributes_rest.sh
source ./tests/drivers/string.sh
# tags: curl,HeadObject,GetObjectAttributes,ETag,x-amz-object-attributes
@test "REST - head object" {
run setup_bucket_and_add_file_v3 "$BUCKET_ONE_NAME"
assert_success
read -r bucket_name file_name <<< "$output"
run get_etag_rest "$bucket_name" "$file_name"
assert_success
expected_etag=$output
run check_etag_attribute_rest "$bucket_name" "$file_name" "$expected_etag"
assert_success
}
# tags: curl,HeadObject,Content-Type
@test "REST - HeadObject - default Content-Type is binary/octet-stream" {
run setup_bucket_and_add_file_v3 "$BUCKET_ONE_NAME"
assert_success
read -r bucket_name file_name <<< "$output"
run head_object_check_header_key_and_value "$bucket_name" "$file_name" "Content-Type" "binary/octet-stream"
assert_success
}
# tags: curl,HeadObject,partNumber,invalid-query
@test "REST - HeadObject - letter partNumber" {
run setup_bucket_and_add_file_v3 "$BUCKET_ONE_NAME"
assert_success
read -r bucket_name file_name <<< "$output"
run send_rest_go_command "400" "-method" "HEAD" "-query" "partNumber=abc" "-objectKey" "$file_name" "-bucketName" "$bucket_name"
assert_success
}
# tags: curl,HeadObject,partNumber,invalid-query
@test "REST - HeadObject - invalid partNumber" {
run setup_bucket_and_add_file_v3 "$BUCKET_ONE_NAME"
assert_success
read -r bucket_name file_name <<< "$output"
run send_rest_go_command "416" "-method" "HEAD" "-query" "partNumber=2" "-objectKey" "$file_name" "-bucketName" "$bucket_name"
assert_success
}
# tags: curl,HeadObject,partNumber,multipart
@test "REST - HeadObject - valid partNumbers" {
file_mb=8
run setup_bucket_and_large_file_v3 "$BUCKET_ONE_NAME" "$file_mb"
assert_success
read -r bucket_name test_file <<< "$output"
file_bytes=$((file_mb*1024*1024))
run split_file_irregular "$TEST_FILE_FOLDER/$test_file" 5242880
assert_success
read -r part_one part_two <<< "$output"
log 5 "part one: $part_one, part two: $part_two"
run perform_multipart_upload_rest_variable_parts "$bucket_name" "$test_file" "$part_one" "$part_two"
assert_success
run get_file_size "$part_one"
assert_success
part_size_one="$output"
run get_file_size "$part_one"
assert_success
part_size_two="$output"
run send_rest_go_command_callback "206" "check_header_partial_content_response" "-bucketName" "$bucket_name" "-objectKey" "$test_file" \
"-method" "HEAD" "-query" "partNumber=1" "--" "1" "$file_bytes" "$part_size_one"
assert_success
run send_rest_go_command_callback "206" "check_header_partial_content_response" "-bucketName" "$bucket_name" "-objectKey" "$test_file" \
"-method" "HEAD" "-query" "partNumber=2" "--" "2" "$file_bytes" "$part_size_two"
assert_success
}
# tags: curl,HeadObject,invalid-query
@test "REST - HeadObject - invalid request header type" {
if [ "$DIRECT" != "true" ]; then
skip "https://github.com/versity/versitygw/issues/2075"
fi
run setup_bucket_and_add_file_v3 "$BUCKET_ONE_NAME"
assert_success
read -r bucket_name file_name <<< "$output"
run send_rest_go_command "400" "-bucketName" "$bucket_name" "-objectKey" "$file_name" \
"-method" "HEAD" "-query" "response-invalid=invalid"
assert_success
}
# tags: curl,HeadObject,response-cache-control,response-content-disposition,response-content-encoding,response-content-language,response-content-type,response-expires
@test "REST - HeadObject - response queries" {
run setup_bucket_and_add_file_v3 "$BUCKET_ONE_NAME"
assert_success
read -r bucket_name file_name <<< "$output"
query_suffixes=("cache-control" "content-disposition" "content-encoding" "content-language" "content-type" "expires")
for suffix in "${query_suffixes[@]}"; do
run generate_random_string 20 40
assert_success
value=$output
log 5 "value: $value"
run send_rest_go_command_callback "200" "check_for_header_key_and_value" "-bucketName" "$bucket_name" "-objectKey" "$file_name" \
"-method" "HEAD" "-query" "response-$suffix=$value" "--" "$suffix" "$value"
assert_success
done
}
@test "REST - quick compare" {
run setup_bucket_and_large_file_v3 "$BUCKET_ONE_NAME" 5
assert_success
read -r bucket_name file_name <<< "$output"
run put_object_rest "$TEST_FILE_FOLDER/$file_name" "$bucket_name" "$file_name"
assert_success
export QUICK_COMPARE_SIZE=1000000
run check_quick_compare "$TEST_FILE_FOLDER/$file_name" "$bucket_name" "$file_name"
assert_success
export QUICK_COMPARE_SIZE=10000000
run check_quick_compare "$TEST_FILE_FOLDER/$file_name" "$bucket_name" "$file_name"
assert_failure 2
}