From 206e036e2770d23b9215f6ba50bd719c0a91ae26 Mon Sep 17 00:00:00 2001 From: chrislu Date: Sat, 15 Nov 2025 10:52:15 -0800 Subject: [PATCH] set ModifiedTsNs --- weed/s3api/s3api_object_handlers_copy.go | 18 +++++++++--------- weed/s3api/s3api_object_handlers_put.go | 15 +++++++++------ 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/weed/s3api/s3api_object_handlers_copy.go b/weed/s3api/s3api_object_handlers_copy.go index 1255571ac..28f7abcd5 100644 --- a/weed/s3api/s3api_object_handlers_copy.go +++ b/weed/s3api/s3api_object_handlers_copy.go @@ -341,7 +341,7 @@ func (s3a *S3ApiServer) CopyObjectHandler(w http.ResponseWriter, r *http.Request func pathToBucketAndObject(path string) (bucket, object string) { // Remove leading slash if present path = strings.TrimPrefix(path, "/") - + // Split by first slash to separate bucket and object parts := strings.SplitN(path, "/", 2) if len(parts) == 2 { @@ -360,7 +360,7 @@ func pathToBucketObjectAndVersion(path string) (bucket, object, versionId string // Parse versionId from query string if present ONLY at path boundaries // Format: /bucket/object?versionId=version-id // Must ensure we're not matching "?versionId=" that's part of the object name itself - + // Look for ?versionId= that comes after the bucket/object path // The key insight: a real query parameter must either be at the end or followed by & if idx := strings.Index(path, "?versionId="); idx != -1 { @@ -395,9 +395,9 @@ 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 { @@ -406,18 +406,18 @@ func (s3a *S3ApiServer) CopyObjectPartHandler(w http.ResponseWriter, r *http.Req unescapedPath = cpSrcPath } cpSrcPath = unescapedPath - + 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 (len=%d), srcObject=%q (len=%d), srcVersionId=%q", + + 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 if srcObject == "" || srcBucket == "" { - glog.Errorf("CopyObjectPart: Invalid copy source - srcBucket=%q, srcObject=%q (original header: %q)", + glog.Errorf("CopyObjectPart: Invalid copy source - srcBucket=%q, srcObject=%q (original header: %q)", srcBucket, srcObject, r.Header.Get("X-Amz-Copy-Source")) s3err.WriteErrorResponse(w, r, s3err.ErrInvalidCopySource) return diff --git a/weed/s3api/s3api_object_handlers_put.go b/weed/s3api/s3api_object_handlers_put.go index da1458b06..6e59635d5 100644 --- a/weed/s3api/s3api_object_handlers_put.go +++ b/weed/s3api/s3api_object_handlers_put.go @@ -387,13 +387,16 @@ func (s3a *S3ApiServer) putToFiler(r *http.Request, uploadUrl string, dataReader // IMPORTANT: FileChunk.ETag must be base64-encoded (from uploadResult.ContentMd5) // NOT hex-encoded etag! The filer.ETagChunks() function expects base64. + // ModifiedTsNs is critical for multipart race condition handling - when multiple + // uploads for the same part number occur, the one with the latest timestamp wins fileChunk := &filer_pb.FileChunk{ - FileId: assignResult.FileId, - Offset: 0, - Size: uint64(uploadResult.Size), - ETag: uploadResult.ContentMd5, // Base64-encoded MD5 from volume server - Fid: fid, - CipherKey: uploadResult.CipherKey, + FileId: assignResult.FileId, + Offset: 0, + Size: uint64(uploadResult.Size), + ModifiedTsNs: now.UnixNano(), // Set to current time for duplicate part resolution + ETag: uploadResult.ContentMd5, // Base64-encoded MD5 from volume server + Fid: fid, + CipherKey: uploadResult.CipherKey, } glog.V(3).Infof("putToFiler: Created FileChunk - fileId=%s, size=%d, etag(base64)=%s (for multipart ETag calc)",