mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-01 04:05:54 +00:00
* s3api: do not store aws-chunked in an object's Content-Encoding aws-chunked in Content-Encoding names the SigV4 streaming framing of the request body, which the gateway decodes on upload. PutObject and CreateMultipartUpload stored the header as sent, so an object uploaded with "gzip, aws-chunked" was served with that Content-Encoding, and one uploaded with "aws-chunked" alone was served as aws-chunked. S3 drops aws-chunked and keeps the other encodings. Also run ceph/s3-tests' test_object_content_encoding_aws_chunked in CI. * s3api: read every Content-Encoding field, and drop aws-chunked on copy A client can send aws-chunked and the object's own encoding as separate Content-Encoding fields. Only the first was read, so "aws-chunked" followed by "gzip" left the object without its gzip. Combine all the fields before dropping aws-chunked. CopyObject with the REPLACE directive stored the requested Content-Encoding as sent: drop aws-chunked there too.
136 lines
4.4 KiB
Go
136 lines
4.4 KiB
Go
package s3api
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
|
)
|
|
|
|
// ParseS3Metadata extracts S3-specific metadata from HTTP request headers
|
|
// This includes: storage class, tags, user metadata, SSE headers, and ACL headers
|
|
// Used by S3 API handlers to prepare metadata before saving to filer
|
|
// Returns an S3 error code if tag parsing fails
|
|
func ParseS3Metadata(r *http.Request, existing map[string][]byte, isReplace bool) (metadata map[string][]byte, errCode s3err.ErrorCode) {
|
|
metadata = make(map[string][]byte)
|
|
|
|
// Copy existing metadata unless replacing
|
|
if !isReplace {
|
|
for k, v := range existing {
|
|
metadata[k] = v
|
|
}
|
|
}
|
|
|
|
// Storage class
|
|
if sc := r.Header.Get(s3_constants.AmzStorageClass); sc != "" {
|
|
metadata[s3_constants.AmzStorageClass] = []byte(sc)
|
|
}
|
|
|
|
// Content-Encoding (standard HTTP header used by S3)
|
|
if ce := storedContentEncoding(r.Header.Values("Content-Encoding")); ce != "" {
|
|
metadata["Content-Encoding"] = []byte(ce)
|
|
}
|
|
|
|
// Other Standard HTTP headers defined in https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateMultipartUpload.html
|
|
standardHeaders := []string{
|
|
"Cache-Control",
|
|
"Content-Disposition",
|
|
"Content-Language",
|
|
"Expires",
|
|
}
|
|
for _, header := range standardHeaders {
|
|
if value := r.Header.Get(header); value != "" {
|
|
metadata[header] = []byte(value)
|
|
}
|
|
}
|
|
|
|
// Do NOT persist Response-Content-Disposition: it is a GET-only
|
|
// presigned-download override and must not be stored as upload metadata.
|
|
|
|
// Object tagging
|
|
if tags := r.Header.Get(s3_constants.AmzObjectTagging); tags != "" {
|
|
// Use url.ParseQuery for robust parsing and automatic URL decoding
|
|
parsedTags, err := url.ParseQuery(tags)
|
|
if err != nil {
|
|
// Return proper S3 error instead of silently dropping tags
|
|
glog.Warningf("Invalid S3 tag format in header '%s': %v", tags, err)
|
|
return nil, s3err.ErrInvalidTag
|
|
}
|
|
|
|
// Validate: S3 spec does not allow duplicate tag keys
|
|
for key, values := range parsedTags {
|
|
if len(values) > 1 {
|
|
glog.Warningf("Duplicate tag key '%s' in header '%s'", key, tags)
|
|
return nil, s3err.ErrInvalidTag
|
|
}
|
|
// Tag value can be an empty string but not nil
|
|
value := ""
|
|
if len(values) > 0 {
|
|
value = values[0]
|
|
}
|
|
metadata[s3_constants.AmzObjectTagging+"-"+key] = []byte(value)
|
|
}
|
|
}
|
|
|
|
// User-defined metadata (x-amz-meta-* headers)
|
|
for header, values := range r.Header {
|
|
if strings.HasPrefix(header, s3_constants.AmzUserMetaPrefix) {
|
|
// Go's HTTP server canonicalizes headers (e.g., x-amz-meta-foo → X-Amz-Meta-Foo)
|
|
// Per HTTP and S3 spec: multiple header values are concatenated with commas
|
|
// This ensures no metadata is lost when clients send duplicate header names
|
|
metadata[header] = []byte(strings.Join(values, ","))
|
|
}
|
|
}
|
|
|
|
// SSE-C headers
|
|
if algorithm := r.Header.Get(s3_constants.AmzServerSideEncryptionCustomerAlgorithm); algorithm != "" {
|
|
metadata[s3_constants.AmzServerSideEncryptionCustomerAlgorithm] = []byte(algorithm)
|
|
}
|
|
if keyMD5 := r.Header.Get(s3_constants.AmzServerSideEncryptionCustomerKeyMD5); keyMD5 != "" {
|
|
// Store as-is; SSE-C MD5 is base64 and case-sensitive
|
|
metadata[s3_constants.AmzServerSideEncryptionCustomerKeyMD5] = []byte(keyMD5)
|
|
}
|
|
|
|
// ACL owner
|
|
acpOwner := r.Header.Get(s3_constants.ExtAmzOwnerKey)
|
|
if len(acpOwner) > 0 {
|
|
metadata[s3_constants.ExtAmzOwnerKey] = []byte(acpOwner)
|
|
}
|
|
|
|
// ACL grants
|
|
acpGrants := r.Header.Get(s3_constants.ExtAmzAclKey)
|
|
if len(acpGrants) > 0 {
|
|
metadata[s3_constants.ExtAmzAclKey] = []byte(acpGrants)
|
|
}
|
|
|
|
return metadata, s3err.ErrNone
|
|
}
|
|
|
|
// storedContentEncoding returns the Content-Encoding to keep with an object,
|
|
// from the values of the request's Content-Encoding fields, which it combines
|
|
// as one list. aws-chunked names the SigV4 streaming framing of the request
|
|
// body, which is decoded on upload, so S3 does not store it: "gzip,
|
|
// aws-chunked" is kept as "gzip", and "aws-chunked" alone as no
|
|
// Content-Encoding at all.
|
|
func storedContentEncoding(values []string) string {
|
|
value := strings.Join(values, ", ")
|
|
var kept []string
|
|
chunked := false
|
|
for _, encoding := range strings.Split(value, ",") {
|
|
encoding = strings.TrimSpace(encoding)
|
|
switch {
|
|
case strings.EqualFold(encoding, "aws-chunked"):
|
|
chunked = true
|
|
case encoding != "":
|
|
kept = append(kept, encoding)
|
|
}
|
|
}
|
|
if !chunked {
|
|
return value
|
|
}
|
|
return strings.Join(kept, ", ")
|
|
}
|