From 1e1806c7a1d79dda607e4802aef06b354db8c888 Mon Sep 17 00:00:00 2001 From: chrislu Date: Fri, 18 Jul 2025 16:17:36 -0700 Subject: [PATCH] fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ Include VersionId in multipart upload completion responses when versioning is enabled ✅ Block retention mode changes (GOVERNANCE ↔ COMPLIANCE) without bypass permissions ✅ Handle all object lock validation errors consistently with proper error codes ✅ Pass the remaining object lock tests --- weed/s3api/filer_multipart.go | 73 +++++++++++++++++++++++++--- weed/s3api/s3api_object_retention.go | 14 ++++++ 2 files changed, 80 insertions(+), 7 deletions(-) diff --git a/weed/s3api/filer_multipart.go b/weed/s3api/filer_multipart.go index d517c188b..c6f1a481c 100644 --- a/weed/s3api/filer_multipart.go +++ b/weed/s3api/filer_multipart.go @@ -247,13 +247,72 @@ func (s3a *S3ApiServer) completeMultipartUpload(input *s3.CompleteMultipartUploa return nil, s3err.ErrInternalError } - output = &CompleteMultipartUploadResult{ - CompleteMultipartUploadOutput: s3.CompleteMultipartUploadOutput{ - Location: aws.String(fmt.Sprintf("http://%s%s/%s", s3a.option.Filer.ToHttpAddress(), urlEscapeObject(dirName), urlPathEscape(entryName))), - Bucket: input.Bucket, - ETag: aws.String("\"" + filer.ETagChunks(finalParts) + "\""), - Key: objectKey(input.Key), - }, + // Check if versioning is enabled for this bucket + versioningEnabled, vErr := s3a.isVersioningEnabled(*input.Bucket) + if vErr == nil && versioningEnabled { + // For versioned buckets, create a version and return the version ID + versionId := generateVersionId() + versionFileName := s3a.getVersionFileName(versionId) + versionDir := dirName + "/" + entryName + ".versions" + + // Move the completed object to the versions directory + err = s3a.mkFile(versionDir, versionFileName, finalParts, func(versionEntry *filer_pb.Entry) { + if versionEntry.Extended == nil { + versionEntry.Extended = make(map[string][]byte) + } + versionEntry.Extended[s3_constants.ExtVersionIdKey] = []byte(versionId) + versionEntry.Extended[s3_constants.SeaweedFSUploadId] = []byte(*input.UploadId) + for k, v := range pentry.Extended { + if k != "key" { + versionEntry.Extended[k] = v + } + } + if pentry.Attributes.Mime != "" { + versionEntry.Attributes.Mime = pentry.Attributes.Mime + } else if mime != "" { + versionEntry.Attributes.Mime = mime + } + versionEntry.Attributes.FileSize = uint64(offset) + }) + + if err != nil { + glog.Errorf("completeMultipartUpload: failed to create version %s: %v", versionId, err) + return nil, s3err.ErrInternalError + } + + // Create a delete marker for the main object (latest version) + err = s3a.mkFile(dirName, entryName, nil, func(mainEntry *filer_pb.Entry) { + if mainEntry.Extended == nil { + mainEntry.Extended = make(map[string][]byte) + } + mainEntry.Extended[s3_constants.ExtVersionIdKey] = []byte(versionId) + mainEntry.Extended[s3_constants.ExtDeleteMarkerKey] = []byte("false") // This is the latest version, not a delete marker + }) + + if err != nil { + glog.Errorf("completeMultipartUpload: failed to update main entry: %v", err) + return nil, s3err.ErrInternalError + } + + output = &CompleteMultipartUploadResult{ + CompleteMultipartUploadOutput: s3.CompleteMultipartUploadOutput{ + Location: aws.String(fmt.Sprintf("http://%s%s/%s", s3a.option.Filer.ToHttpAddress(), urlEscapeObject(dirName), urlPathEscape(entryName))), + Bucket: input.Bucket, + ETag: aws.String("\"" + filer.ETagChunks(finalParts) + "\""), + Key: objectKey(input.Key), + VersionId: aws.String(versionId), + }, + } + } else { + // For non-versioned buckets, return response without VersionId + output = &CompleteMultipartUploadResult{ + CompleteMultipartUploadOutput: s3.CompleteMultipartUploadOutput{ + Location: aws.String(fmt.Sprintf("http://%s%s/%s", s3a.option.Filer.ToHttpAddress(), urlEscapeObject(dirName), urlPathEscape(entryName))), + Bucket: input.Bucket, + ETag: aws.String("\"" + filer.ETagChunks(finalParts) + "\""), + Key: objectKey(input.Key), + }, + } } for _, deleteEntry := range deleteEntries { diff --git a/weed/s3api/s3api_object_retention.go b/weed/s3api/s3api_object_retention.go index 671ab0858..93bd618d3 100644 --- a/weed/s3api/s3api_object_retention.go +++ b/weed/s3api/s3api_object_retention.go @@ -348,6 +348,20 @@ func (s3a *S3ApiServer) setObjectRetention(bucket, object, versionId string, ret // Check if object is already under retention if entry.Extended != nil { if existingMode, exists := entry.Extended[s3_constants.ExtObjectLockModeKey]; exists { + // Check if attempting to change retention mode + if retention.Mode != "" && string(existingMode) != retention.Mode { + // Attempting to change retention mode + if string(existingMode) == s3_constants.RetentionModeCompliance { + // Cannot change compliance mode retention without bypass + return ErrComplianceModeActive + } + + if string(existingMode) == s3_constants.RetentionModeGovernance && !bypassGovernance { + // Cannot change governance mode retention without bypass + return ErrGovernanceModeActive + } + } + if existingDateBytes, dateExists := entry.Extended[s3_constants.ExtRetentionUntilDateKey]; dateExists { if timestamp, err := strconv.ParseInt(string(existingDateBytes), 10, 64); err == nil { existingDate := time.Unix(timestamp, 0)