diff --git a/weed/s3api/s3_metadata_util.go b/weed/s3api/s3_metadata_util.go new file mode 100644 index 000000000..a39b8116f --- /dev/null +++ b/weed/s3api/s3_metadata_util.go @@ -0,0 +1,87 @@ +package s3api + +import ( + "net/http" + "net/url" + "strings" + + "github.com/seaweedfs/seaweedfs/weed/glog" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" +) + +// 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 +func ParseS3Metadata(r *http.Request, existing map[string][]byte, isReplace bool) (metadata map[string][]byte) { + 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 { + glog.Errorf("Failed to parse S3 tags '%s': %v", tags, err) + } else { + for key, values := range parsedTags { + // According to S3 spec, if a key is provided multiple times, the last value is used. + // A tag value can be an empty string but not nil. + value := "" + if len(values) > 0 { + value = values[len(values)-1] + } + 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) + // We store them as they come in (after canonicalization) to preserve the user's intent + for _, value := range values { + metadata[header] = []byte(value) + } + } + } + + // 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 +} diff --git a/weed/s3api/s3api_object_handlers_multipart.go b/weed/s3api/s3api_object_handlers_multipart.go index 3ea709b31..6c2ad06a4 100644 --- a/weed/s3api/s3api_object_handlers_multipart.go +++ b/weed/s3api/s3api_object_handlers_multipart.go @@ -20,7 +20,6 @@ import ( "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" "github.com/seaweedfs/seaweedfs/weed/s3api/s3err" - weed_server "github.com/seaweedfs/seaweedfs/weed/server" stats_collect "github.com/seaweedfs/seaweedfs/weed/stats" ) @@ -65,7 +64,8 @@ func (s3a *S3ApiServer) NewMultipartUploadHandler(w http.ResponseWriter, r *http Metadata: make(map[string]*string), } - metadata := weed_server.SaveAmzMetaData(r, nil, false) + // Parse S3 metadata from request headers + metadata := ParseS3Metadata(r, nil, false) for k, v := range metadata { createMultipartUploadInput.Metadata[k] = aws.String(string(v)) } diff --git a/weed/server/filer_server_handlers_read.go b/weed/server/filer_server_handlers_read.go index 21612a161..86670af6b 100644 --- a/weed/server/filer_server_handlers_read.go +++ b/weed/server/filer_server_handlers_read.go @@ -13,12 +13,10 @@ import ( "strings" "time" - "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" - "github.com/seaweedfs/seaweedfs/weed/security" - "github.com/seaweedfs/seaweedfs/weed/filer" "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" + "github.com/seaweedfs/seaweedfs/weed/security" "github.com/seaweedfs/seaweedfs/weed/stats" "github.com/seaweedfs/seaweedfs/weed/util" ) @@ -120,22 +118,12 @@ func (fs *FilerServer) GetOrHeadHandler(w http.ResponseWriter, r *http.Request) writeJsonQuiet(w, r, http.StatusOK, entry) return } - // For S3-created directories (FolderMimeType), return the directory object metadata itself - // rather than listing contents. Regular filer directories show listings. - if entry.Attr.Mime == "" { - // Regular filer directory - show listing if enabled - if !fs.option.ExposeDirectoryData { - writeJsonError(w, r, http.StatusForbidden, errors.New("directory listing is disabled")) - return - } - fs.listDirectoryHandler(w, r) + // Regular directory - show listing if enabled + if !fs.option.ExposeDirectoryData { + writeJsonError(w, r, http.StatusForbidden, errors.New("directory listing is disabled")) return } - // S3-created directory object (FolderMimeType) - fall through to serve metadata - } - - if isForDirectory && entry.Attr.Mime != s3_constants.FolderMimeType { - w.WriteHeader(http.StatusNotFound) + fs.listDirectoryHandler(w, r) return } @@ -176,10 +164,9 @@ func (fs *FilerServer) GetOrHeadHandler(w http.ResponseWriter, r *http.Request) } // print out the header from extended properties + // Filter out xattr-* (filesystem extended attributes) and internal Seaweedfs-* headers for k, v := range entry.Extended { - if !strings.HasPrefix(k, "xattr-") && !s3_constants.IsSeaweedFSInternalHeader(k) { - // "xattr-" prefix is set in filesys.XATTR_PREFIX - // IsSeaweedFSInternalHeader filters internal metadata that should not become HTTP headers + if !strings.HasPrefix(k, "xattr-") && !strings.HasPrefix(strings.ToLower(k), "x-seaweedfs-") { w.Header().Set(k, string(v)) } } @@ -194,17 +181,6 @@ func (fs *FilerServer) GetOrHeadHandler(w http.ResponseWriter, r *http.Request) seaweedHeaders = append(seaweedHeaders, "Content-Disposition") w.Header().Set("Access-Control-Expose-Headers", strings.Join(seaweedHeaders, ",")) - //set tag count - tagCount := 0 - for k := range entry.Extended { - if strings.HasPrefix(k, s3_constants.AmzObjectTagging+"-") { - tagCount++ - } - } - if tagCount > 0 { - w.Header().Set(s3_constants.AmzTagCount, strconv.Itoa(tagCount)) - } - SetEtag(w, etag) filename := entry.Name() diff --git a/weed/server/filer_server_handlers_write_autochunk.go b/weed/server/filer_server_handlers_write_autochunk.go index 8912bb073..fdbc70a64 100644 --- a/weed/server/filer_server_handlers_write_autochunk.go +++ b/weed/server/filer_server_handlers_write_autochunk.go @@ -7,7 +7,6 @@ import ( "fmt" "io" "net/http" - "net/url" "os" "path" "strconv" @@ -18,7 +17,6 @@ import ( "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/operation" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" - "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" "github.com/seaweedfs/seaweedfs/weed/storage/needle" "github.com/seaweedfs/seaweedfs/weed/util" "github.com/seaweedfs/seaweedfs/weed/util/constants" @@ -326,16 +324,13 @@ func (fs *FilerServer) saveMetaData(ctx context.Context, r *http.Request, fileNa Size: int64(entry.FileSize), } - entry.Extended = SaveAmzMetaData(r, entry.Extended, false) - // Note: S3 API now sets SeaweedFSExpiresS3 directly in metadata via gRPC, not HTTP headers + // Save standard HTTP headers as extended attributes + // Note: S3 API now writes directly to volume servers and saves metadata via gRPC + // This handler is for non-S3 clients (WebDAV, SFTP, mount, curl, etc.) for k, v := range r.Header { if len(v) > 0 && len(v[0]) > 0 { if strings.HasPrefix(k, needle.PairNamePrefix) || k == "Cache-Control" || k == "Expires" || k == "Content-Disposition" { entry.Extended[k] = []byte(v[0]) - // Log version ID header specifically for debugging - if k == "Seaweed-X-Amz-Version-Id" { - glog.V(0).Infof("filer: storing version ID header in Extended: %s=%s for path=%s", k, v[0], path) - } } if k == "Response-Content-Disposition" { entry.Extended["Content-Disposition"] = []byte(v[0]) @@ -447,73 +442,3 @@ func (fs *FilerServer) mkdir(ctx context.Context, w http.ResponseWriter, r *http } return filerResult, replyerr } - -func SaveAmzMetaData(r *http.Request, existing map[string][]byte, isReplace bool) (metadata map[string][]byte) { - - metadata = make(map[string][]byte) - if !isReplace { - for k, v := range existing { - metadata[k] = v - } - } - - if sc := r.Header.Get(s3_constants.AmzStorageClass); sc != "" { - metadata[s3_constants.AmzStorageClass] = []byte(sc) - } - - if ce := r.Header.Get("Content-Encoding"); ce != "" { - metadata["Content-Encoding"] = []byte(ce) - } - - 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 { - glog.Errorf("Failed to parse S3 tags '%s': %v", tags, err) - } else { - for key, values := range parsedTags { - // According to S3 spec, if a key is provided multiple times, the last value is used. - // A tag value can be an empty string but not nil. - value := "" - if len(values) > 0 { - value = values[len(values)-1] - } - metadata[s3_constants.AmzObjectTagging+"-"+key] = []byte(value) - } - } - } - - 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) - // We store them as they come in (after canonicalization) to preserve the user's intent - for _, value := range values { - metadata[header] = []byte(value) - } - } - } - - // Handle 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) - } - - //acp-owner - acpOwner := r.Header.Get(s3_constants.ExtAmzOwnerKey) - if len(acpOwner) > 0 { - metadata[s3_constants.ExtAmzOwnerKey] = []byte(acpOwner) - } - - //acp-grants - acpGrants := r.Header.Get(s3_constants.ExtAmzAclKey) - if len(acpGrants) > 0 { - metadata[s3_constants.ExtAmzAclKey] = []byte(acpGrants) - } - - return - -}