diff options
| author | Chris Lu <chrislusf@users.noreply.github.com> | 2025-12-04 12:18:57 -0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-12-04 12:18:57 -0800 |
| commit | a5ab05ec03534a55e42116057be8bceed015cac0 (patch) | |
| tree | 9407b1181dd422d92f81b779b4490cafd1710efd /weed/s3api/s3api_object_handlers.go | |
| parent | 8d110b29ddfd9b9cdb504a4380106b2b287155ca (diff) | |
| download | seaweedfs-a5ab05ec03534a55e42116057be8bceed015cac0.tar.xz seaweedfs-a5ab05ec03534a55e42116057be8bceed015cac0.zip | |
fix: S3 GetObject/HeadObject with PartNumber should return object ETag, not part ETag (#7622)
AWS S3 behavior: when calling GetObject or HeadObject with the PartNumber
query parameter, the ETag header should still return the complete object's
ETag (e.g., 'abc123-4' for a 4-part multipart upload), not the individual
part's ETag.
The previous implementation incorrectly overrode the ETag with the part's
ETag, causing test_multipart_get_part to fail.
This fix removes the ETag override logic while keeping:
- x-amz-mp-parts-count header (correct)
- Content-Length adjusted to part size (correct)
- Range calculation for part boundaries (correct)
Diffstat (limited to 'weed/s3api/s3api_object_handlers.go')
| -rw-r--r-- | weed/s3api/s3api_object_handlers.go | 42 |
1 files changed, 5 insertions, 37 deletions
diff --git a/weed/s3api/s3api_object_handlers.go b/weed/s3api/s3api_object_handlers.go index 1406bbf42..43cc4e5fc 100644 --- a/weed/s3api/s3api_object_handlers.go +++ b/weed/s3api/s3api_object_handlers.go @@ -659,16 +659,14 @@ func (s3a *S3ApiServer) GetObjectHandler(w http.ResponseWriter, r *http.Request) glog.V(3).Infof("GetObject: Set PartsCount=%d for multipart GET with PartNumber=%d", partsCount, partNumber) // Calculate the byte range for this part + // Note: ETag is NOT overridden - AWS S3 returns the complete object's ETag + // even when requesting a specific part via PartNumber var startOffset, endOffset int64 if partInfo != nil { // Use part boundaries from metadata (accurate for multi-chunk parts) startOffset = objectEntryForSSE.Chunks[partInfo.StartChunk].Offset lastChunk := objectEntryForSSE.Chunks[partInfo.EndChunk-1] endOffset = lastChunk.Offset + int64(lastChunk.Size) - 1 - - // Override ETag with the part's ETag from metadata - w.Header().Set("ETag", "\""+partInfo.ETag+"\"") - glog.V(3).Infof("GetObject: Override ETag with part %d ETag: %s (from metadata)", partNumber, partInfo.ETag) } else { // Fallback: assume 1:1 part-to-chunk mapping (backward compatibility) chunkIndex := partNumber - 1 @@ -680,15 +678,6 @@ func (s3a *S3ApiServer) GetObjectHandler(w http.ResponseWriter, r *http.Request) partChunk := objectEntryForSSE.Chunks[chunkIndex] startOffset = partChunk.Offset endOffset = partChunk.Offset + int64(partChunk.Size) - 1 - - // Override ETag with chunk's ETag (fallback) - if partChunk.ETag != "" { - if md5Bytes, decodeErr := base64.StdEncoding.DecodeString(partChunk.ETag); decodeErr == nil { - partETag := fmt.Sprintf("%x", md5Bytes) - w.Header().Set("ETag", "\""+partETag+"\"") - glog.V(3).Infof("GetObject: Override ETag with part %d ETag: %s (fallback from chunk)", partNumber, partETag) - } - } } // Check if client supplied a Range header - if so, apply it within the part's boundaries @@ -2266,7 +2255,7 @@ func (s3a *S3ApiServer) HeadObjectHandler(w http.ResponseWriter, r *http.Request if partNumberStr != "" { if partNumber, parseErr := strconv.Atoi(partNumberStr); parseErr == nil && partNumber > 0 { // Get actual parts count from metadata (not chunk count) - partsCount, partInfo := s3a.getMultipartInfo(objectEntryForSSE, partNumber) + partsCount, _ := s3a.getMultipartInfo(objectEntryForSSE, partNumber) // Validate part number if partNumber > partsCount { @@ -2276,31 +2265,10 @@ func (s3a *S3ApiServer) HeadObjectHandler(w http.ResponseWriter, r *http.Request } // Set parts count header + // Note: ETag is NOT overridden - AWS S3 returns the complete object's ETag + // even when requesting a specific part via PartNumber w.Header().Set(s3_constants.AmzMpPartsCount, strconv.Itoa(partsCount)) glog.V(3).Infof("HeadObject: Set PartsCount=%d for part %d", partsCount, partNumber) - - // Override ETag with the part's ETag - if partInfo != nil { - // Use part ETag from metadata (accurate for multi-chunk parts) - w.Header().Set("ETag", "\""+partInfo.ETag+"\"") - glog.V(3).Infof("HeadObject: Override ETag with part %d ETag: %s (from metadata)", partNumber, partInfo.ETag) - } else { - // Fallback: use chunk's ETag (backward compatibility) - chunkIndex := partNumber - 1 - if chunkIndex >= len(objectEntryForSSE.Chunks) { - glog.Warningf("HeadObject: Part %d chunk index %d out of range (chunks: %d)", partNumber, chunkIndex, len(objectEntryForSSE.Chunks)) - s3err.WriteErrorResponse(w, r, s3err.ErrInvalidPart) - return - } - partChunk := objectEntryForSSE.Chunks[chunkIndex] - if partChunk.ETag != "" { - if md5Bytes, decodeErr := base64.StdEncoding.DecodeString(partChunk.ETag); decodeErr == nil { - partETag := fmt.Sprintf("%x", md5Bytes) - w.Header().Set("ETag", "\""+partETag+"\"") - glog.V(3).Infof("HeadObject: Override ETag with part %d ETag: %s (fallback from chunk)", partNumber, partETag) - } - } - } } } |
