mirror of
https://github.com/versity/versitygw.git
synced 2026-05-12 15:01:27 +00:00
feat: implements conditional reads for GetObject and HeadObject
Closes #882 Implements conditional reads for `GetObject` and `HeadObject` in the gateway for both POSIX and Azure backends. The behavior is controlled by the `If-Match`, `If-None-Match`, `If-Modified-Since`, and `If-Unmodified-Since` request headers, where the first two perform ETag comparisons and the latter two compare against the object’s `LastModified` date. No validation is performed for invalid ETags or malformed date formats, and precondition date headers are expected to follow RFC1123; otherwise, they are ignored. The Integration tests cover all possible combinations of conditional headers, ensuring the feature is 100% AWS S3–compatible.
This commit is contained in:
@@ -417,6 +417,19 @@ func (az *Azure) GetObject(ctx context.Context, input *s3.GetObjectInput) (*s3.G
|
||||
return nil, azureErrToS3Err(err)
|
||||
}
|
||||
|
||||
if resp.ETag != nil && resp.LastModified != nil {
|
||||
err = backend.EvaluatePreconditions(convertAzureEtag(resp.ETag), *resp.LastModified,
|
||||
backend.PreConditions{
|
||||
IfMatch: input.IfMatch,
|
||||
IfNoneMatch: input.IfNoneMatch,
|
||||
IfModSince: input.IfModifiedSince,
|
||||
IfUnmodeSince: input.IfUnmodifiedSince,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var opts *azblob.DownloadStreamOptions
|
||||
if *input.Range != "" {
|
||||
offset, count, isValid, err := backend.ParseObjectRange(*resp.ContentLength, *input.Range)
|
||||
@@ -478,6 +491,19 @@ func (az *Azure) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3
|
||||
return nil, azureErrToS3Err(err)
|
||||
}
|
||||
|
||||
if res.ETag != nil && res.LastModified != nil {
|
||||
err = backend.EvaluatePreconditions(convertAzureEtag(res.ETag), *res.LastModified,
|
||||
backend.PreConditions{
|
||||
IfMatch: input.IfMatch,
|
||||
IfNoneMatch: input.IfNoneMatch,
|
||||
IfModSince: input.IfModifiedSince,
|
||||
IfUnmodeSince: input.IfUnmodifiedSince,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
partsCount := int32(len(res.UncommittedBlocks))
|
||||
|
||||
for _, block := range res.UncommittedBlocks {
|
||||
@@ -508,6 +534,20 @@ func (az *Azure) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3
|
||||
if err != nil {
|
||||
return nil, azureErrToS3Err(err)
|
||||
}
|
||||
|
||||
if resp.ETag != nil && resp.LastModified != nil {
|
||||
err = backend.EvaluatePreconditions(convertAzureEtag(resp.ETag), *resp.LastModified,
|
||||
backend.PreConditions{
|
||||
IfMatch: input.IfMatch,
|
||||
IfNoneMatch: input.IfNoneMatch,
|
||||
IfModSince: input.IfModifiedSince,
|
||||
IfUnmodeSince: input.IfUnmodifiedSince,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var size int64
|
||||
if resp.ContentLength != nil {
|
||||
size = *resp.ContentLength
|
||||
|
||||
@@ -88,6 +88,8 @@ func TrimEtag(etag *string) *string {
|
||||
var (
|
||||
errInvalidRange = s3err.GetAPIError(s3err.ErrInvalidRange)
|
||||
errInvalidCopySourceRange = s3err.GetAPIError(s3err.ErrInvalidCopySourceRange)
|
||||
errPreconditionFailed = s3err.GetAPIError(s3err.ErrPreconditionFailed)
|
||||
errNotModified = s3err.GetAPIError(s3err.ErrNotModified)
|
||||
)
|
||||
|
||||
// ParseObjectRange parses input range header and returns startoffset, length, isValid
|
||||
@@ -422,3 +424,111 @@ func GenerateEtag(h hash.Hash) string {
|
||||
func AreEtagsSame(e1, e2 string) bool {
|
||||
return strings.Trim(e1, `"`) == strings.Trim(e2, `"`)
|
||||
}
|
||||
|
||||
func getBoolPtr(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
type PreConditions struct {
|
||||
IfMatch *string
|
||||
IfNoneMatch *string
|
||||
IfModSince *time.Time
|
||||
IfUnmodeSince *time.Time
|
||||
}
|
||||
|
||||
// EvaluatePreconditions takes the object ETag, the last modified time and
|
||||
// evaluates the read preconditions:
|
||||
// - if-match,
|
||||
// - if-none-match
|
||||
// - if-modified-since
|
||||
// - if-unmodified-since
|
||||
// if-match and if-none-match are ETag comparisions
|
||||
// if-modified-since and if-unmodified-since are last modifed time comparisons
|
||||
func EvaluatePreconditions(etag string, modTime time.Time, preconditions PreConditions) error {
|
||||
if preconditions.IfMatch == nil && preconditions.IfNoneMatch == nil && preconditions.IfModSince == nil && preconditions.IfUnmodeSince == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// convert all conditions to *bool to evaluate the conditions
|
||||
var ifMatch, ifNoneMatch, ifModSince, ifUnmodeSince *bool
|
||||
if preconditions.IfMatch != nil {
|
||||
ifMatch = getBoolPtr(*preconditions.IfMatch == etag)
|
||||
}
|
||||
if preconditions.IfNoneMatch != nil {
|
||||
ifNoneMatch = getBoolPtr(*preconditions.IfNoneMatch != etag)
|
||||
}
|
||||
if preconditions.IfModSince != nil {
|
||||
ifModSince = getBoolPtr(preconditions.IfModSince.UTC().Before(modTime.UTC()))
|
||||
}
|
||||
if preconditions.IfUnmodeSince != nil {
|
||||
ifUnmodeSince = getBoolPtr(preconditions.IfUnmodeSince.UTC().After(modTime.UTC()))
|
||||
}
|
||||
|
||||
if ifMatch != nil {
|
||||
// if `if-match` doesn't matches, return PreconditionFailed
|
||||
if !*ifMatch {
|
||||
return errPreconditionFailed
|
||||
}
|
||||
|
||||
// if-match matches
|
||||
if *ifMatch {
|
||||
if ifNoneMatch != nil {
|
||||
// if `if-none-match` doesn't match return NotModified
|
||||
if !*ifNoneMatch {
|
||||
return errNotModified
|
||||
}
|
||||
|
||||
// if both `if-match` and `if-none-match` match, return no error
|
||||
return nil
|
||||
}
|
||||
|
||||
// if `if-match` matches but `if-modified-since` is false return NotModified
|
||||
if ifModSince != nil && !*ifModSince {
|
||||
return errNotModified
|
||||
}
|
||||
|
||||
// ignore `if-unmodified-since` as `if-match` is true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if ifNoneMatch != nil {
|
||||
if *ifNoneMatch {
|
||||
// if `if-none-match` is true, but `if-unmodified-since` is false
|
||||
// return PreconditionFailed
|
||||
if ifUnmodeSince != nil && !*ifUnmodeSince {
|
||||
return errPreconditionFailed
|
||||
}
|
||||
|
||||
// ignore `if-modified-since` as `if-none-match` is true
|
||||
return nil
|
||||
} else {
|
||||
// if `if-none-match` is false and `if-unmodified-since` is false
|
||||
// return PreconditionFailed
|
||||
if ifUnmodeSince != nil && !*ifUnmodeSince {
|
||||
return errPreconditionFailed
|
||||
}
|
||||
|
||||
// in all other cases when `if-none-match` is false return NotModified
|
||||
return errNotModified
|
||||
}
|
||||
}
|
||||
|
||||
if ifModSince != nil && !*ifModSince {
|
||||
// if both `if-modified-since` and `if-unmodified-since` are false
|
||||
// return PreconditionFailed
|
||||
if ifUnmodeSince != nil && !*ifUnmodeSince {
|
||||
return errPreconditionFailed
|
||||
}
|
||||
|
||||
// if only `if-modified-since` is false, return NotModified
|
||||
return errNotModified
|
||||
}
|
||||
|
||||
// if `if-unmodified-since` is false return PreconditionFailed
|
||||
if ifUnmodeSince != nil && !*ifUnmodeSince {
|
||||
return errPreconditionFailed
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3484,6 +3484,23 @@ func (p *Posix) GetObject(_ context.Context, input *s3.GetObjectInput) (*s3.GetO
|
||||
}
|
||||
}
|
||||
|
||||
b, err := p.meta.RetrieveAttribute(nil, bucket, object, etagkey)
|
||||
etag := string(b)
|
||||
if err != nil {
|
||||
etag = ""
|
||||
}
|
||||
|
||||
// evaluate preconditions
|
||||
err = backend.EvaluatePreconditions(etag, fid.ModTime(), backend.PreConditions{
|
||||
IfMatch: input.IfMatch,
|
||||
IfNoneMatch: input.IfNoneMatch,
|
||||
IfModSince: input.IfModifiedSince,
|
||||
IfUnmodeSince: input.IfUnmodifiedSince,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if fid.IsDir() {
|
||||
_, _, _, err := backend.ParseObjectRange(0, *input.Range)
|
||||
if err != nil {
|
||||
@@ -3493,11 +3510,6 @@ func (p *Posix) GetObject(_ context.Context, input *s3.GetObjectInput) (*s3.GetO
|
||||
userMetaData := make(map[string]string)
|
||||
|
||||
objMeta := p.loadObjectMetaData(nil, bucket, object, &fid, userMetaData)
|
||||
b, err := p.meta.RetrieveAttribute(nil, bucket, object, etagkey)
|
||||
etag := string(b)
|
||||
if err != nil {
|
||||
etag = ""
|
||||
}
|
||||
|
||||
var tagCount *int32
|
||||
tags, err := p.getAttrTags(bucket, object)
|
||||
@@ -3579,12 +3591,6 @@ func (p *Posix) GetObject(_ context.Context, input *s3.GetObjectInput) (*s3.GetO
|
||||
|
||||
objMeta := p.loadObjectMetaData(f, bucket, object, &fi, userMetaData)
|
||||
|
||||
b, err := p.meta.RetrieveAttribute(f, bucket, object, etagkey)
|
||||
etag := string(b)
|
||||
if err != nil {
|
||||
etag = ""
|
||||
}
|
||||
|
||||
var tagCount *int32
|
||||
tags, err := p.getAttrTags(bucket, object)
|
||||
if err != nil && !errors.Is(err, s3err.GetAPIError(s3err.ErrBucketTaggingNotFound)) {
|
||||
@@ -3689,17 +3695,30 @@ func (p *Posix) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// retreive the part etag
|
||||
b, err := p.meta.RetrieveAttribute(nil, bucket, partPath, etagkey)
|
||||
etag := string(b)
|
||||
if err != nil {
|
||||
etag = ""
|
||||
}
|
||||
|
||||
// evaluate preconditions
|
||||
err = backend.EvaluatePreconditions(etag, part.ModTime(), backend.PreConditions{
|
||||
IfMatch: input.IfMatch,
|
||||
IfNoneMatch: input.IfNoneMatch,
|
||||
IfModSince: input.IfModifiedSince,
|
||||
IfUnmodeSince: input.IfUnmodifiedSince,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var contentRange string
|
||||
if isValid {
|
||||
contentRange = fmt.Sprintf("bytes %v-%v/%v",
|
||||
startOffset, startOffset+length-1, size)
|
||||
}
|
||||
|
||||
b, err := p.meta.RetrieveAttribute(nil, bucket, partPath, etagkey)
|
||||
etag := string(b)
|
||||
if err != nil {
|
||||
etag = ""
|
||||
}
|
||||
partsCount := int32(len(ents))
|
||||
|
||||
return &s3.HeadObjectOutput{
|
||||
@@ -3799,6 +3818,17 @@ func (p *Posix) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3.
|
||||
etag = ""
|
||||
}
|
||||
|
||||
// evaluate preconditions
|
||||
err = backend.EvaluatePreconditions(etag, fi.ModTime(), backend.PreConditions{
|
||||
IfMatch: input.IfMatch,
|
||||
IfNoneMatch: input.IfNoneMatch,
|
||||
IfModSince: input.IfModifiedSince,
|
||||
IfUnmodeSince: input.IfUnmodifiedSince,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
size := fi.Size()
|
||||
if fi.IsDir() {
|
||||
size = 0
|
||||
|
||||
@@ -450,12 +450,18 @@ func (c S3ApiController) GetObject(ctx *fiber.Ctx) (*Response, error) {
|
||||
}, s3err.GetInvalidChecksumHeaderErr("x-amz-checksum-mode")
|
||||
}
|
||||
|
||||
conditionalHeaders := utils.ParsePreconditionHeaders(ctx)
|
||||
|
||||
res, err := c.be.GetObject(ctx.Context(), &s3.GetObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &key,
|
||||
Range: &acceptRange,
|
||||
VersionId: &versionId,
|
||||
ChecksumMode: checksumMode,
|
||||
Bucket: &bucket,
|
||||
Key: &key,
|
||||
Range: &acceptRange,
|
||||
IfMatch: conditionalHeaders.IfMatch,
|
||||
IfNoneMatch: conditionalHeaders.IfNoneMatch,
|
||||
IfModifiedSince: conditionalHeaders.IfModSince,
|
||||
IfUnmodifiedSince: conditionalHeaders.IfUnmodeSince,
|
||||
VersionId: &versionId,
|
||||
ChecksumMode: checksumMode,
|
||||
})
|
||||
if err != nil {
|
||||
var headers map[string]*string
|
||||
|
||||
@@ -90,14 +90,20 @@ func (c S3ApiController) HeadObject(ctx *fiber.Ctx) (*Response, error) {
|
||||
}, s3err.GetInvalidChecksumHeaderErr("x-amz-checksum-mode")
|
||||
}
|
||||
|
||||
conditionalHeaders := utils.ParsePreconditionHeaders(ctx)
|
||||
|
||||
res, err := c.be.HeadObject(ctx.Context(),
|
||||
&s3.HeadObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &key,
|
||||
PartNumber: partNumber,
|
||||
VersionId: &versionId,
|
||||
ChecksumMode: checksumMode,
|
||||
Range: &objRange,
|
||||
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
|
||||
|
||||
@@ -642,6 +642,59 @@ func ParseCreateMpChecksumHeaders(ctx *fiber.Ctx) (types.ChecksumAlgorithm, type
|
||||
return algo, chType, nil
|
||||
}
|
||||
|
||||
// ConditionalHeaders holds the conditional header values
|
||||
type ConditionalHeaders struct {
|
||||
IfMatch *string
|
||||
IfNoneMatch *string
|
||||
IfModSince *time.Time
|
||||
IfUnmodeSince *time.Time
|
||||
}
|
||||
|
||||
// ParsePreconditionHeaders parses the precondition headers:
|
||||
// - If-Match
|
||||
// - If-None-Match
|
||||
// - If-Modified-Since
|
||||
// - If-Unmodified-Since
|
||||
func ParsePreconditionHeaders(ctx *fiber.Ctx) ConditionalHeaders {
|
||||
ifMatch, ifNoneMatch := ParsePreconditionMatchHeaders(ctx)
|
||||
ifModSince, ifUnmodeSince := ParsePreconditionDateHeaders(ctx)
|
||||
|
||||
return ConditionalHeaders{
|
||||
IfMatch: ifMatch,
|
||||
IfNoneMatch: ifNoneMatch,
|
||||
IfModSince: ifModSince,
|
||||
IfUnmodeSince: ifUnmodeSince,
|
||||
}
|
||||
}
|
||||
|
||||
// ParsePreconditionMatchHeaders extracts "If-Match" and "If-None-Match" headers from fiber Ctx
|
||||
func ParsePreconditionMatchHeaders(ctx *fiber.Ctx) (*string, *string) {
|
||||
return GetStringPtr(ctx.Get("If-Match")), GetStringPtr(ctx.Get("If-None-Match"))
|
||||
}
|
||||
|
||||
// ParsePreconditionDateHeaders parses the "If-Modified-Since" and "If-Unmodified-Since"
|
||||
// headers from fiber context to *time.Time
|
||||
func ParsePreconditionDateHeaders(ctx *fiber.Ctx) (*time.Time, *time.Time) {
|
||||
ifModSince := ctx.Get("If-Modified-Since")
|
||||
ifUnmodSince := ctx.Get("If-Unmodified-Since")
|
||||
|
||||
var ifModSinceParsed, ifUnmodSinceParsed *time.Time
|
||||
|
||||
// the time format should be a valid RFC1123
|
||||
// if parsing fails, ignore the error and leave the value as nil
|
||||
modParsed, err := time.Parse(time.RFC1123, ifModSince)
|
||||
if err == nil {
|
||||
ifModSinceParsed = &modParsed
|
||||
}
|
||||
|
||||
unmodParsed, err := time.Parse(time.RFC1123, ifUnmodSince)
|
||||
if err == nil {
|
||||
ifUnmodSinceParsed = &unmodParsed
|
||||
}
|
||||
|
||||
return ifModSinceParsed, ifUnmodSinceParsed
|
||||
}
|
||||
|
||||
// TagLimit specifies the allowed tag count in a tag set
|
||||
type TagLimit int
|
||||
|
||||
|
||||
@@ -177,6 +177,7 @@ const (
|
||||
ErrCORSForbidden
|
||||
ErrMissingCORSOrigin
|
||||
ErrCORSIsNotEnabled
|
||||
ErrNotModified
|
||||
|
||||
// Non-AWS errors
|
||||
ErrExistingObjectIsDirectory
|
||||
@@ -786,6 +787,11 @@ var errorCodeResponse = map[ErrorCode]APIError{
|
||||
Description: "CORSResponse: CORS is not enabled for this bucket.",
|
||||
HTTPStatusCode: http.StatusForbidden,
|
||||
},
|
||||
ErrNotModified: {
|
||||
Code: "NotModified",
|
||||
Description: "Not Modified",
|
||||
HTTPStatusCode: http.StatusNotModified,
|
||||
},
|
||||
|
||||
// non aws errors
|
||||
ErrExistingObjectIsDirectory: {
|
||||
|
||||
@@ -179,6 +179,7 @@ func TestHeadObject(s *S3Conf) {
|
||||
HeadObject_with_range(s)
|
||||
HeadObject_zero_len_with_range(s)
|
||||
HeadObject_dir_with_range(s)
|
||||
HeadObject_conditional_reads(s)
|
||||
//TODO: remove the condition after implementing checksums in azure
|
||||
if !s.azureTests {
|
||||
HeadObject_not_enabled_checksum_mode(s)
|
||||
@@ -209,6 +210,7 @@ func TestGetObject(s *S3Conf) {
|
||||
GetObject_dir_with_range(s)
|
||||
GetObject_invalid_parent(s)
|
||||
GetObject_large_object(s)
|
||||
GetObject_conditional_reads(s)
|
||||
//TODO: remove the condition after implementing checksums in azure
|
||||
if !s.azureTests {
|
||||
GetObject_checksums(s)
|
||||
@@ -1124,6 +1126,7 @@ func GetIntTests() IntTests {
|
||||
"HeadObject_with_range": HeadObject_with_range,
|
||||
"HeadObject_zero_len_with_range": HeadObject_zero_len_with_range,
|
||||
"HeadObject_dir_with_range": HeadObject_dir_with_range,
|
||||
"HeadObject_conditional_reads": HeadObject_conditional_reads,
|
||||
"HeadObject_not_enabled_checksum_mode": HeadObject_not_enabled_checksum_mode,
|
||||
"HeadObject_checksums": HeadObject_checksums,
|
||||
"HeadObject_success": HeadObject_success,
|
||||
@@ -1142,6 +1145,7 @@ func GetIntTests() IntTests {
|
||||
"GetObject_dir_with_range": GetObject_dir_with_range,
|
||||
"GetObject_invalid_parent": GetObject_invalid_parent,
|
||||
"GetObject_large_object": GetObject_large_object,
|
||||
"GetObject_conditional_reads": GetObject_conditional_reads,
|
||||
"GetObject_checksums": GetObject_checksums,
|
||||
"GetObject_success": GetObject_success,
|
||||
"GetObject_directory_success": GetObject_directory_success,
|
||||
|
||||
@@ -3945,6 +3945,151 @@ func HeadObject_dir_with_range(s *S3Conf) error {
|
||||
return headObject_zero_len_with_range_helper(testName, "my-dir/", s)
|
||||
}
|
||||
|
||||
func HeadObject_conditional_reads(s *S3Conf) error {
|
||||
testName := "HeadObject_conditional_reads"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
key := "my-obj"
|
||||
obj, err := putObjectWithData(10, &s3.PutObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &key,
|
||||
}, s3client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
errMod := getPtr("NotModified")
|
||||
errCond := getPtr("PreconditionFailed")
|
||||
|
||||
// sleep one second to get dates before and after
|
||||
// the object creation
|
||||
time.Sleep(time.Second * 1)
|
||||
|
||||
before := time.Now().AddDate(0, 0, -3)
|
||||
after := time.Now()
|
||||
etag := obj.res.ETag
|
||||
|
||||
for i, test := range []struct {
|
||||
ifmatch *string
|
||||
ifnonematch *string
|
||||
ifmodifiedsince *time.Time
|
||||
ifunmodifiedsince *time.Time
|
||||
err *string
|
||||
}{
|
||||
// all the cases when preconditions are either empty, true or false
|
||||
{getPtr("invalid_etag"), getPtr("invalid_etag"), &before, &before, errCond},
|
||||
{getPtr("invalid_etag"), getPtr("invalid_etag"), &before, &after, errCond},
|
||||
{getPtr("invalid_etag"), getPtr("invalid_etag"), &before, nil, errCond},
|
||||
{getPtr("invalid_etag"), getPtr("invalid_etag"), &after, &before, errCond},
|
||||
{getPtr("invalid_etag"), getPtr("invalid_etag"), &after, &after, errCond},
|
||||
{getPtr("invalid_etag"), getPtr("invalid_etag"), &after, nil, errCond},
|
||||
{getPtr("invalid_etag"), getPtr("invalid_etag"), nil, &before, errCond},
|
||||
{getPtr("invalid_etag"), getPtr("invalid_etag"), nil, &after, errCond},
|
||||
{getPtr("invalid_etag"), getPtr("invalid_etag"), nil, nil, errCond},
|
||||
|
||||
{getPtr("invalid_etag"), etag, &before, &before, errCond},
|
||||
{getPtr("invalid_etag"), etag, &before, &after, errCond},
|
||||
{getPtr("invalid_etag"), etag, &before, nil, errCond},
|
||||
{getPtr("invalid_etag"), etag, &after, &before, errCond},
|
||||
{getPtr("invalid_etag"), etag, &after, &after, errCond},
|
||||
{getPtr("invalid_etag"), etag, &after, nil, errCond},
|
||||
{getPtr("invalid_etag"), etag, nil, &before, errCond},
|
||||
{getPtr("invalid_etag"), etag, nil, &after, errCond},
|
||||
{getPtr("invalid_etag"), etag, nil, nil, errCond},
|
||||
|
||||
{getPtr("invalid_etag"), nil, &before, &before, errCond},
|
||||
{getPtr("invalid_etag"), nil, &before, &after, errCond},
|
||||
{getPtr("invalid_etag"), nil, &before, nil, errCond},
|
||||
{getPtr("invalid_etag"), nil, &after, &before, errCond},
|
||||
{getPtr("invalid_etag"), nil, &after, &after, errCond},
|
||||
{getPtr("invalid_etag"), nil, &after, nil, errCond},
|
||||
{getPtr("invalid_etag"), nil, nil, &before, errCond},
|
||||
{getPtr("invalid_etag"), nil, nil, &after, errCond},
|
||||
{getPtr("invalid_etag"), nil, nil, nil, errCond},
|
||||
|
||||
{etag, getPtr("invalid_etag"), &before, &before, nil},
|
||||
{etag, getPtr("invalid_etag"), &before, &after, nil},
|
||||
{etag, getPtr("invalid_etag"), &before, nil, nil},
|
||||
{etag, getPtr("invalid_etag"), &after, &before, nil},
|
||||
{etag, getPtr("invalid_etag"), &after, &after, nil},
|
||||
{etag, getPtr("invalid_etag"), &after, nil, nil},
|
||||
{etag, getPtr("invalid_etag"), nil, &before, nil},
|
||||
{etag, getPtr("invalid_etag"), nil, &after, nil},
|
||||
{etag, getPtr("invalid_etag"), nil, nil, nil},
|
||||
|
||||
{etag, etag, &before, &before, errMod},
|
||||
{etag, etag, &before, &after, errMod},
|
||||
{etag, etag, &before, nil, errMod},
|
||||
{etag, etag, &after, &before, errMod},
|
||||
{etag, etag, &after, &after, errMod},
|
||||
{etag, etag, &after, nil, errMod},
|
||||
{etag, etag, nil, &before, errMod},
|
||||
{etag, etag, nil, &after, errMod},
|
||||
{etag, etag, nil, nil, errMod},
|
||||
|
||||
{etag, nil, &before, &before, nil},
|
||||
{etag, nil, &before, &after, nil},
|
||||
{etag, nil, &before, nil, nil},
|
||||
{etag, nil, &after, &before, errMod},
|
||||
{etag, nil, &after, &after, errMod},
|
||||
{etag, nil, &after, nil, errMod},
|
||||
{etag, nil, nil, &before, nil},
|
||||
{etag, nil, nil, &after, nil},
|
||||
{etag, nil, nil, nil, nil},
|
||||
|
||||
{nil, getPtr("invalid_etag"), &before, &before, errCond},
|
||||
{nil, getPtr("invalid_etag"), &before, &after, nil},
|
||||
{nil, getPtr("invalid_etag"), &before, nil, nil},
|
||||
{nil, getPtr("invalid_etag"), &after, &before, errCond},
|
||||
{nil, getPtr("invalid_etag"), &after, &after, nil},
|
||||
{nil, getPtr("invalid_etag"), &after, nil, nil},
|
||||
{nil, getPtr("invalid_etag"), nil, &before, errCond},
|
||||
{nil, getPtr("invalid_etag"), nil, &after, nil},
|
||||
{nil, getPtr("invalid_etag"), nil, nil, nil},
|
||||
|
||||
{nil, etag, &before, &before, errCond},
|
||||
{nil, etag, &before, &after, errMod},
|
||||
{nil, etag, &before, nil, errMod},
|
||||
{nil, etag, &after, &before, errCond},
|
||||
{nil, etag, &after, &after, errMod},
|
||||
{nil, etag, &after, nil, errMod},
|
||||
{nil, etag, nil, &before, errCond},
|
||||
{nil, etag, nil, &after, errMod},
|
||||
{nil, etag, nil, nil, errMod},
|
||||
|
||||
{nil, nil, &before, &before, errCond},
|
||||
{nil, nil, &before, &after, nil},
|
||||
{nil, nil, &before, nil, nil},
|
||||
{nil, nil, &after, &before, errCond},
|
||||
{nil, nil, &after, &after, errMod},
|
||||
{nil, nil, &after, nil, errMod},
|
||||
{nil, nil, nil, &before, errCond},
|
||||
{nil, nil, nil, &after, nil},
|
||||
{nil, nil, nil, nil, nil},
|
||||
} {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &key,
|
||||
IfMatch: test.ifmatch,
|
||||
IfNoneMatch: test.ifnonematch,
|
||||
IfModifiedSince: test.ifmodifiedsince,
|
||||
IfUnmodifiedSince: test.ifunmodifiedsince,
|
||||
})
|
||||
cancel()
|
||||
if test.err == nil && err != nil {
|
||||
return fmt.Errorf("test case %d failed: expected no error, but got %v", i, err)
|
||||
}
|
||||
if test.err != nil {
|
||||
if err := checkSdkApiErr(err, *test.err); err != nil {
|
||||
return fmt.Errorf("test case %d failed: %w", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func HeadObject_success(s *S3Conf) error {
|
||||
testName := "HeadObject_success"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
@@ -4642,6 +4787,155 @@ func GetObject_large_object(s *S3Conf) error {
|
||||
})
|
||||
}
|
||||
|
||||
func GetObject_conditional_reads(s *S3Conf) error {
|
||||
testName := "GetObject_conditional_reads"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
key := "my-obj"
|
||||
obj, err := putObjectWithData(10, &s3.PutObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &key,
|
||||
}, s3client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
errMod := s3err.GetAPIError(s3err.ErrNotModified)
|
||||
errCond := s3err.GetAPIError(s3err.ErrPreconditionFailed)
|
||||
|
||||
// sleep one second to get dates before and after
|
||||
// the object creation
|
||||
time.Sleep(time.Second * 1)
|
||||
|
||||
before := time.Now().AddDate(0, 0, -3)
|
||||
after := time.Now()
|
||||
etag := obj.res.ETag
|
||||
|
||||
for i, test := range []struct {
|
||||
ifmatch *string
|
||||
ifnonematch *string
|
||||
ifmodifiedsince *time.Time
|
||||
ifunmodifiedsince *time.Time
|
||||
err error
|
||||
}{
|
||||
// all the cases when preconditions are either empty, true or false
|
||||
{getPtr("invalid_etag"), getPtr("invalid_etag"), &before, &before, errCond},
|
||||
{getPtr("invalid_etag"), getPtr("invalid_etag"), &before, &after, errCond},
|
||||
{getPtr("invalid_etag"), getPtr("invalid_etag"), &before, nil, errCond},
|
||||
{getPtr("invalid_etag"), getPtr("invalid_etag"), &after, &before, errCond},
|
||||
{getPtr("invalid_etag"), getPtr("invalid_etag"), &after, &after, errCond},
|
||||
{getPtr("invalid_etag"), getPtr("invalid_etag"), &after, nil, errCond},
|
||||
{getPtr("invalid_etag"), getPtr("invalid_etag"), nil, &before, errCond},
|
||||
{getPtr("invalid_etag"), getPtr("invalid_etag"), nil, &after, errCond},
|
||||
{getPtr("invalid_etag"), getPtr("invalid_etag"), nil, nil, errCond},
|
||||
|
||||
{getPtr("invalid_etag"), etag, &before, &before, errCond},
|
||||
{getPtr("invalid_etag"), etag, &before, &after, errCond},
|
||||
{getPtr("invalid_etag"), etag, &before, nil, errCond},
|
||||
{getPtr("invalid_etag"), etag, &after, &before, errCond},
|
||||
{getPtr("invalid_etag"), etag, &after, &after, errCond},
|
||||
{getPtr("invalid_etag"), etag, &after, nil, errCond},
|
||||
{getPtr("invalid_etag"), etag, nil, &before, errCond},
|
||||
{getPtr("invalid_etag"), etag, nil, &after, errCond},
|
||||
{getPtr("invalid_etag"), etag, nil, nil, errCond},
|
||||
|
||||
{getPtr("invalid_etag"), nil, &before, &before, errCond},
|
||||
{getPtr("invalid_etag"), nil, &before, &after, errCond},
|
||||
{getPtr("invalid_etag"), nil, &before, nil, errCond},
|
||||
{getPtr("invalid_etag"), nil, &after, &before, errCond},
|
||||
{getPtr("invalid_etag"), nil, &after, &after, errCond},
|
||||
{getPtr("invalid_etag"), nil, &after, nil, errCond},
|
||||
{getPtr("invalid_etag"), nil, nil, &before, errCond},
|
||||
{getPtr("invalid_etag"), nil, nil, &after, errCond},
|
||||
{getPtr("invalid_etag"), nil, nil, nil, errCond},
|
||||
|
||||
{etag, getPtr("invalid_etag"), &before, &before, nil},
|
||||
{etag, getPtr("invalid_etag"), &before, &after, nil},
|
||||
{etag, getPtr("invalid_etag"), &before, nil, nil},
|
||||
{etag, getPtr("invalid_etag"), &after, &before, nil},
|
||||
{etag, getPtr("invalid_etag"), &after, &after, nil},
|
||||
{etag, getPtr("invalid_etag"), &after, nil, nil},
|
||||
{etag, getPtr("invalid_etag"), nil, &before, nil},
|
||||
{etag, getPtr("invalid_etag"), nil, &after, nil},
|
||||
{etag, getPtr("invalid_etag"), nil, nil, nil},
|
||||
|
||||
{etag, etag, &before, &before, errMod},
|
||||
{etag, etag, &before, &after, errMod},
|
||||
{etag, etag, &before, nil, errMod},
|
||||
{etag, etag, &after, &before, errMod},
|
||||
{etag, etag, &after, &after, errMod},
|
||||
{etag, etag, &after, nil, errMod},
|
||||
{etag, etag, nil, &before, errMod},
|
||||
{etag, etag, nil, &after, errMod},
|
||||
{etag, etag, nil, nil, errMod},
|
||||
|
||||
{etag, nil, &before, &before, nil},
|
||||
{etag, nil, &before, &after, nil},
|
||||
{etag, nil, &before, nil, nil},
|
||||
{etag, nil, &after, &before, errMod},
|
||||
{etag, nil, &after, &after, errMod},
|
||||
{etag, nil, &after, nil, errMod},
|
||||
{etag, nil, nil, &before, nil},
|
||||
{etag, nil, nil, &after, nil},
|
||||
{etag, nil, nil, nil, nil},
|
||||
|
||||
{nil, getPtr("invalid_etag"), &before, &before, errCond},
|
||||
{nil, getPtr("invalid_etag"), &before, &after, nil},
|
||||
{nil, getPtr("invalid_etag"), &before, nil, nil},
|
||||
{nil, getPtr("invalid_etag"), &after, &before, errCond},
|
||||
{nil, getPtr("invalid_etag"), &after, &after, nil},
|
||||
{nil, getPtr("invalid_etag"), &after, nil, nil},
|
||||
{nil, getPtr("invalid_etag"), nil, &before, errCond},
|
||||
{nil, getPtr("invalid_etag"), nil, &after, nil},
|
||||
{nil, getPtr("invalid_etag"), nil, nil, nil},
|
||||
|
||||
{nil, etag, &before, &before, errCond},
|
||||
{nil, etag, &before, &after, errMod},
|
||||
{nil, etag, &before, nil, errMod},
|
||||
{nil, etag, &after, &before, errCond},
|
||||
{nil, etag, &after, &after, errMod},
|
||||
{nil, etag, &after, nil, errMod},
|
||||
{nil, etag, nil, &before, errCond},
|
||||
{nil, etag, nil, &after, errMod},
|
||||
{nil, etag, nil, nil, errMod},
|
||||
|
||||
{nil, nil, &before, &before, errCond},
|
||||
{nil, nil, &before, &after, nil},
|
||||
{nil, nil, &before, nil, nil},
|
||||
{nil, nil, &after, &before, errCond},
|
||||
{nil, nil, &after, &after, errMod},
|
||||
{nil, nil, &after, nil, errMod},
|
||||
{nil, nil, nil, &before, errCond},
|
||||
{nil, nil, nil, &after, nil},
|
||||
{nil, nil, nil, nil, nil},
|
||||
} {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &key,
|
||||
IfMatch: test.ifmatch,
|
||||
IfNoneMatch: test.ifnonematch,
|
||||
IfModifiedSince: test.ifmodifiedsince,
|
||||
IfUnmodifiedSince: test.ifunmodifiedsince,
|
||||
})
|
||||
cancel()
|
||||
if test.err == nil && err != nil {
|
||||
return fmt.Errorf("test case %d failed: expected no error, but got %v", i, err)
|
||||
}
|
||||
if test.err != nil {
|
||||
apiErr, ok := test.err.(s3err.APIError)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid error type: expected s3err.APIError")
|
||||
}
|
||||
if err := checkApiErr(err, apiErr); err != nil {
|
||||
return fmt.Errorf("test case %d failed: %w", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func GetObject_success(s *S3Conf) error {
|
||||
testName := "GetObject_success"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
|
||||
Reference in New Issue
Block a user