aboutsummaryrefslogtreecommitdiff
path: root/weed/s3api/s3_metadata_util.go
diff options
context:
space:
mode:
Diffstat (limited to 'weed/s3api/s3_metadata_util.go')
-rw-r--r--weed/s3api/s3_metadata_util.go94
1 files changed, 94 insertions, 0 deletions
diff --git a/weed/s3api/s3_metadata_util.go b/weed/s3api/s3_metadata_util.go
new file mode 100644
index 000000000..37363752a
--- /dev/null
+++ b/weed/s3api/s3_metadata_util.go
@@ -0,0 +1,94 @@
+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 := r.Header.Get("Content-Encoding"); ce != "" {
+ metadata["Content-Encoding"] = []byte(ce)
+ }
+
+ // 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
+}