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:
niksis02
2025-09-01 18:33:01 -07:00
committed by Ben McClelland
parent e2fb272711
commit b3ed7639f0
9 changed files with 576 additions and 27 deletions
+11 -5
View File
@@ -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
+12 -6
View File
@@ -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
+53
View File
@@ -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