diff --git a/weed/s3api/s3api_object_handlers.go b/weed/s3api/s3api_object_handlers.go index 6c1a824b4..a53e2d7ef 100644 --- a/weed/s3api/s3api_object_handlers.go +++ b/weed/s3api/s3api_object_handlers.go @@ -1280,12 +1280,12 @@ func (s3a *S3ApiServer) setResponseHeaders(w http.ResponseWriter, entry *filer_p for k, v := range entry.Extended { // Skip internal SeaweedFS headers if !strings.HasPrefix(k, "xattr-") && !s3_constants.IsSeaweedFSInternalHeader(k) { - // Support backward compatibility: migrate old lowercase format to canonical format - // OLD: "x-amz-meta-foo" → NEW: "X-Amz-Meta-foo" + // Support backward compatibility: migrate old non-canonical format to canonical format + // OLD: "x-amz-meta-foo" → NEW: "X-Amz-Meta-foo" (preserving suffix case) headerKey := k if strings.HasPrefix(strings.ToLower(k), "x-amz-meta-") && !strings.HasPrefix(k, s3_constants.AmzUserMetaPrefix) { - // Old format detected - normalize to canonical prefix - suffix := strings.ToLower(k[len("x-amz-meta-"):]) + // Old format detected - normalize to canonical prefix, preserve suffix case + suffix := k[len("x-amz-meta-"):] headerKey = s3_constants.AmzUserMetaPrefix + suffix glog.V(4).Infof("Migrating old user metadata header %q to %q in response", k, headerKey) } diff --git a/weed/s3api/s3api_object_handlers_copy.go b/weed/s3api/s3api_object_handlers_copy.go index ce65534b4..e52fbc71d 100644 --- a/weed/s3api/s3api_object_handlers_copy.go +++ b/weed/s3api/s3api_object_handlers_copy.go @@ -700,29 +700,22 @@ func processMetadataBytes(reqHeader http.Header, existing map[string][]byte, rep if replaceMeta { for header, values := range reqHeader { if strings.HasPrefix(header, s3_constants.AmzUserMetaPrefix) { - // AWS S3 stores user metadata keys in lowercase // Go's HTTP server canonicalizes headers (e.g., x-amz-meta-foo → X-Amz-Meta-Foo) - // Preserve the canonical prefix "X-Amz-Meta-" but lowercase the user-defined suffix - suffix := strings.ToLower(header[len(s3_constants.AmzUserMetaPrefix):]) - normalizedKey := s3_constants.AmzUserMetaPrefix + suffix + // We store them as they come in (after canonicalization) to preserve the user's intent for _, value := range values { - metadata[normalizedKey] = []byte(value) + metadata[header] = []byte(value) } } } } else { - // Copy existing metadata as-is (already normalized during storage) - // Support both old format (x-amz-meta-*) and new format (X-Amz-Meta-*) for backward compatibility + // Copy existing metadata as-is for k, v := range existing { if strings.HasPrefix(k, s3_constants.AmzUserMetaPrefix) { - // New format (X-Amz-Meta-foo) - copy as-is metadata[k] = v } else if strings.HasPrefix(strings.ToLower(k), "x-amz-meta-") { - // Old format (x-amz-meta-foo) - migrate to new format - suffix := strings.ToLower(k[len("x-amz-meta-"):]) - normalizedKey := s3_constants.AmzUserMetaPrefix + suffix - metadata[normalizedKey] = v - glog.V(4).Infof("Migrating old user metadata key %q to %q", k, normalizedKey) + // Backward compatibility: old format (x-amz-meta-foo) + // Preserve the original key from storage for backward compatibility + metadata[k] = v } } } diff --git a/weed/s3api/s3api_object_handlers_put.go b/weed/s3api/s3api_object_handlers_put.go index 6ef0dcf97..018eb2b1e 100644 --- a/weed/s3api/s3api_object_handlers_put.go +++ b/weed/s3api/s3api_object_handlers_put.go @@ -447,13 +447,9 @@ func (s3a *S3ApiServer) putToFiler(r *http.Request, uploadUrl string, dataReader for k, v := range r.Header { if len(v) > 0 && len(v[0]) > 0 { if strings.HasPrefix(k, s3_constants.AmzUserMetaPrefix) { - // AWS S3 stores user metadata keys in lowercase // Go's HTTP server canonicalizes headers (e.g., x-amz-meta-foo → X-Amz-Meta-Foo) - // Preserve the canonical prefix "X-Amz-Meta-" but lowercase the user-defined suffix - // This ensures the key is still detectable via prefix checks elsewhere - suffix := strings.ToLower(k[len(s3_constants.AmzUserMetaPrefix):]) - normalizedKey := s3_constants.AmzUserMetaPrefix + suffix - entry.Extended[normalizedKey] = []byte(v[0]) + // We store them as they come in (after canonicalization) to preserve the user's intent + entry.Extended[k] = []byte(v[0]) } else if k == "Cache-Control" || k == "Expires" || k == "Content-Disposition" { entry.Extended[k] = []byte(v[0]) } diff --git a/weed/server/filer_server_handlers_write_autochunk.go b/weed/server/filer_server_handlers_write_autochunk.go index 59b83501b..f2449ab52 100644 --- a/weed/server/filer_server_handlers_write_autochunk.go +++ b/weed/server/filer_server_handlers_write_autochunk.go @@ -544,14 +544,10 @@ func SaveAmzMetaData(r *http.Request, existing map[string][]byte, isReplace bool for header, values := range r.Header { if strings.HasPrefix(header, s3_constants.AmzUserMetaPrefix) { - // AWS S3 stores user metadata keys in lowercase // Go's HTTP server canonicalizes headers (e.g., x-amz-meta-foo → X-Amz-Meta-Foo) - // Preserve the canonical prefix "X-Amz-Meta-" but lowercase the user-defined suffix - // This ensures the key is still detectable via prefix checks elsewhere - suffix := strings.ToLower(header[len(s3_constants.AmzUserMetaPrefix):]) - normalizedKey := s3_constants.AmzUserMetaPrefix + suffix + // We store them as they come in (after canonicalization) to preserve the user's intent for _, value := range values { - metadata[normalizedKey] = []byte(value) + metadata[header] = []byte(value) } } }