mirror of
https://github.com/versity/versitygw.git
synced 2026-01-03 10:35:15 +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
|
||||
|
||||
Reference in New Issue
Block a user