diff --git a/weed/s3api/filer_multipart.go b/weed/s3api/filer_multipart.go index 22a061852..dc71a6371 100644 --- a/weed/s3api/filer_multipart.go +++ b/weed/s3api/filer_multipart.go @@ -325,6 +325,8 @@ func (s3a *S3ApiServer) completeMultipartUpload(r *http.Request, input *s3.Compl } versionEntry.Extended[s3_constants.ExtVersionIdKey] = []byte(versionId) versionEntry.Extended[s3_constants.SeaweedFSUploadId] = []byte(*input.UploadId) + // Store parts count for x-amz-mp-parts-count header + versionEntry.Extended[s3_constants.SeaweedFSMultipartPartsCount] = []byte(fmt.Sprintf("%d", len(completedPartNumbers))) // Set object owner for versioned multipart objects amzAccountId := r.Header.Get(s3_constants.AmzAccountId) @@ -387,6 +389,8 @@ func (s3a *S3ApiServer) completeMultipartUpload(r *http.Request, input *s3.Compl entry.Extended = make(map[string][]byte) } entry.Extended[s3_constants.ExtVersionIdKey] = []byte("null") + // Store parts count for x-amz-mp-parts-count header + entry.Extended[s3_constants.SeaweedFSMultipartPartsCount] = []byte(fmt.Sprintf("%d", len(completedPartNumbers))) // Set object owner for suspended versioning multipart objects amzAccountId := r.Header.Get(s3_constants.AmzAccountId) @@ -440,6 +444,8 @@ func (s3a *S3ApiServer) completeMultipartUpload(r *http.Request, input *s3.Compl entry.Extended = make(map[string][]byte) } entry.Extended[s3_constants.SeaweedFSUploadId] = []byte(*input.UploadId) + // Store parts count for x-amz-mp-parts-count header + entry.Extended[s3_constants.SeaweedFSMultipartPartsCount] = []byte(fmt.Sprintf("%d", len(completedPartNumbers))) // Set object owner for non-versioned multipart objects amzAccountId := r.Header.Get(s3_constants.AmzAccountId) diff --git a/weed/s3api/s3_constants/header.go b/weed/s3api/s3_constants/header.go index 77ed310d9..019ac63ed 100644 --- a/weed/s3api/s3_constants/header.go +++ b/weed/s3api/s3_constants/header.go @@ -39,10 +39,12 @@ const ( AmzObjectTaggingDirective = "X-Amz-Tagging-Directive" AmzTagCount = "x-amz-tagging-count" - SeaweedFSIsDirectoryKey = "X-Seaweedfs-Is-Directory-Key" - SeaweedFSPartNumber = "X-Seaweedfs-Part-Number" - SeaweedFSUploadId = "X-Seaweedfs-Upload-Id" - SeaweedFSExpiresS3 = "X-Seaweedfs-Expires-S3" + SeaweedFSIsDirectoryKey = "X-Seaweedfs-Is-Directory-Key" + SeaweedFSPartNumber = "X-Seaweedfs-Part-Number" + SeaweedFSUploadId = "X-Seaweedfs-Upload-Id" + SeaweedFSMultipartPartsCount = "X-Seaweedfs-Multipart-Parts-Count" + SeaweedFSExpiresS3 = "X-Seaweedfs-Expires-S3" + AmzMpPartsCount = "x-amz-mp-parts-count" // S3 ACL headers AmzCannedAcl = "X-Amz-Acl" @@ -70,8 +72,6 @@ const ( AmzCopySourceIfModifiedSince = "X-Amz-Copy-Source-If-Modified-Since" AmzCopySourceIfUnmodifiedSince = "X-Amz-Copy-Source-If-Unmodified-Since" - AmzMpPartsCount = "X-Amz-Mp-Parts-Count" - // S3 Server-Side Encryption with Customer-provided Keys (SSE-C) AmzServerSideEncryptionCustomerAlgorithm = "X-Amz-Server-Side-Encryption-Customer-Algorithm" AmzServerSideEncryptionCustomerKey = "X-Amz-Server-Side-Encryption-Customer-Key" diff --git a/weed/s3api/s3api_object_handlers.go b/weed/s3api/s3api_object_handlers.go index 2498f85f2..890e4c088 100644 --- a/weed/s3api/s3api_object_handlers.go +++ b/weed/s3api/s3api_object_handlers.go @@ -1091,6 +1091,16 @@ func (s3a *S3ApiServer) HeadObjectHandler(w http.ResponseWriter, r *http.Request totalSize := int64(filer.FileSize(objectEntryForSSE)) s3a.setResponseHeaders(w, objectEntryForSSE, totalSize) + // Check if PartNumber query parameter is present (for multipart objects) + partNumberStr := r.URL.Query().Get("partNumber") + if partNumberStr != "" && objectEntryForSSE.Extended != nil { + // If this is a multipart object, add the parts count header + if partsCountStr, exists := objectEntryForSSE.Extended[s3_constants.SeaweedFSMultipartPartsCount]; exists { + w.Header().Set(s3_constants.AmzMpPartsCount, string(partsCountStr)) + glog.V(3).Infof("HeadObject: Set PartsCount=%s for multipart object", string(partsCountStr)) + } + } + // Detect and handle SSE sseType := s3a.detectPrimarySSEType(objectEntryForSSE) if sseType != "" && sseType != "None" { diff --git a/weed/s3api/s3api_object_handlers_copy.go b/weed/s3api/s3api_object_handlers_copy.go index 75165e968..1255571ac 100644 --- a/weed/s3api/s3api_object_handlers_copy.go +++ b/weed/s3api/s3api_object_handlers_copy.go @@ -348,9 +348,12 @@ func pathToBucketAndObject(path string) (bucket, object string) { bucket = parts[0] object = "/" + parts[1] return bucket, object + } else if len(parts) == 1 && parts[0] != "" { + // Only bucket provided, no object + return parts[0], "" } - // Only bucket provided, no object - this is invalid in copy operations - return parts[0], "" + // Empty path + return "", "" } func pathToBucketObjectAndVersion(path string) (bucket, object, versionId string) { @@ -393,20 +396,23 @@ func (s3a *S3ApiServer) CopyObjectPartHandler(w http.ResponseWriter, r *http.Req // Copy source path. cpSrcPath := r.Header.Get("X-Amz-Copy-Source") + glog.V(2).Infof("CopyObjectPart: Raw copy source header=%q (len=%d)", cpSrcPath, len(cpSrcPath)) + // Try URL unescaping - AWS SDK sends URL-encoded copy sources unescapedPath, err := url.QueryUnescape(cpSrcPath) if err != nil { // If unescaping fails, log and use original glog.V(2).Infof("CopyObjectPart: Failed to unescape copy source %q: %v, using as-is", cpSrcPath, err) - } else { - cpSrcPath = unescapedPath + unescapedPath = cpSrcPath } + cpSrcPath = unescapedPath - glog.V(2).Infof("CopyObjectPart: Copy source header=%q, after unescape=%q", r.Header.Get("X-Amz-Copy-Source"), cpSrcPath) + glog.V(2).Infof("CopyObjectPart: After unescape=%q (len=%d, bytes=%v)", cpSrcPath, len(cpSrcPath), []byte(cpSrcPath)) srcBucket, srcObject, srcVersionId := pathToBucketObjectAndVersion(cpSrcPath) - glog.V(2).Infof("CopyObjectPart: Parsed srcBucket=%q, srcObject=%q, srcVersionId=%q", srcBucket, srcObject, srcVersionId) + glog.V(2).Infof("CopyObjectPart: Parsed srcBucket=%q (len=%d), srcObject=%q (len=%d), srcVersionId=%q", + srcBucket, len(srcBucket), srcObject, len(srcObject), srcVersionId) // If source object is empty or bucket is empty, reply back invalid copy source. // Note: srcObject can be "/" for root-level objects, but empty string means parsing failed