aboutsummaryrefslogtreecommitdiff
path: root/weed/s3api/s3api_object_handlers_copy.go
diff options
context:
space:
mode:
Diffstat (limited to 'weed/s3api/s3api_object_handlers_copy.go')
-rw-r--r--weed/s3api/s3api_object_handlers_copy.go136
1 files changed, 112 insertions, 24 deletions
diff --git a/weed/s3api/s3api_object_handlers_copy.go b/weed/s3api/s3api_object_handlers_copy.go
index f04522ca6..86a7bc74b 100644
--- a/weed/s3api/s3api_object_handlers_copy.go
+++ b/weed/s3api/s3api_object_handlers_copy.go
@@ -36,13 +36,14 @@ func (s3a *S3ApiServer) CopyObjectHandler(w http.ResponseWriter, r *http.Request
dstBucket, dstObject := s3_constants.GetBucketAndObject(r)
// Copy source path.
- cpSrcPath, err := url.QueryUnescape(r.Header.Get("X-Amz-Copy-Source"))
+ rawCopySource := r.Header.Get("X-Amz-Copy-Source")
+ cpSrcPath, err := url.QueryUnescape(rawCopySource)
if err != nil {
// Save unescaped string as is.
- cpSrcPath = r.Header.Get("X-Amz-Copy-Source")
+ cpSrcPath = rawCopySource
}
- srcBucket, srcObject, srcVersionId := pathToBucketObjectAndVersion(cpSrcPath)
+ srcBucket, srcObject, srcVersionId := pathToBucketObjectAndVersion(rawCopySource, cpSrcPath)
glog.V(3).Infof("CopyObjectHandler %s %s (version: %s) => %s %s", srcBucket, srcObject, srcVersionId, dstBucket, dstObject)
@@ -84,7 +85,7 @@ func (s3a *S3ApiServer) CopyObjectHandler(w http.ResponseWriter, r *http.Request
return
}
writeSuccessResponseXML(w, r, CopyObjectResult{
- ETag: fmt.Sprintf("%x", entry.Attributes.Md5),
+ ETag: filer.ETag(entry),
LastModified: time.Now().UTC(),
})
return
@@ -339,23 +340,46 @@ 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 {
- return parts[0], "/" + parts[1]
- }
- return parts[0], "/"
+ bucket = parts[0]
+ object = "/" + parts[1]
+ return bucket, object
+ } else if len(parts) == 1 && parts[0] != "" {
+ // Only bucket provided, no object
+ return parts[0], ""
+ }
+ // Empty path
+ return "", ""
}
-func pathToBucketObjectAndVersion(path string) (bucket, object, versionId string) {
- // Parse versionId from query string if present
- // Format: /bucket/object?versionId=version-id
- if idx := strings.Index(path, "?versionId="); idx != -1 {
- versionId = path[idx+len("?versionId="):] // dynamically calculate length
- path = path[:idx]
+func pathToBucketObjectAndVersion(rawPath, decodedPath string) (bucket, object, versionId string) {
+ pathForBucket := decodedPath
+
+ if rawPath != "" {
+ if idx := strings.Index(rawPath, "?"); idx != -1 {
+ queryPart := rawPath[idx+1:]
+ if values, err := url.ParseQuery(queryPart); err == nil && values.Has("versionId") {
+ versionId = values.Get("versionId")
+
+ rawPathNoQuery := rawPath[:idx]
+ if unescaped, err := url.QueryUnescape(rawPathNoQuery); err == nil {
+ pathForBucket = unescaped
+ } else {
+ pathForBucket = rawPathNoQuery
+ }
+
+ bucket, object = pathToBucketAndObject(pathForBucket)
+ return bucket, object, versionId
+ }
+ }
}
- bucket, object = pathToBucketAndObject(path)
+ bucket, object = pathToBucketAndObject(pathForBucket)
return bucket, object, versionId
}
@@ -370,15 +394,28 @@ func (s3a *S3ApiServer) CopyObjectPartHandler(w http.ResponseWriter, r *http.Req
dstBucket, dstObject := s3_constants.GetBucketAndObject(r)
// Copy source path.
- cpSrcPath, err := url.QueryUnescape(r.Header.Get("X-Amz-Copy-Source"))
+ rawCopySource := r.Header.Get("X-Amz-Copy-Source")
+
+ glog.V(4).Infof("CopyObjectPart: Raw copy source header=%q", rawCopySource)
+
+ // Try URL unescaping - AWS SDK sends URL-encoded copy sources
+ cpSrcPath, err := url.QueryUnescape(rawCopySource)
if err != nil {
- // Save unescaped string as is.
- cpSrcPath = r.Header.Get("X-Amz-Copy-Source")
+ // If unescaping fails, log and use original
+ glog.V(4).Infof("CopyObjectPart: Failed to unescape copy source %q: %v, using as-is", rawCopySource, err)
+ cpSrcPath = rawCopySource
}
- srcBucket, srcObject, srcVersionId := pathToBucketObjectAndVersion(cpSrcPath)
+ srcBucket, srcObject, srcVersionId := pathToBucketObjectAndVersion(rawCopySource, cpSrcPath)
+
+ glog.V(4).Infof("CopyObjectPart: Parsed srcBucket=%q, srcObject=%q, srcVersionId=%q",
+ srcBucket, 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)",
+ srcBucket, srcObject, r.Header.Get("X-Amz-Copy-Source"))
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidCopySource)
return
}
@@ -471,9 +508,15 @@ func (s3a *S3ApiServer) CopyObjectPartHandler(w http.ResponseWriter, r *http.Req
}
// Create new entry for the part
+ // Calculate part size, avoiding underflow for invalid ranges
+ partSize := uint64(0)
+ if endOffset >= startOffset {
+ partSize = uint64(endOffset - startOffset + 1)
+ }
+
dstEntry := &filer_pb.Entry{
Attributes: &filer_pb.FuseAttributes{
- FileSize: uint64(endOffset - startOffset + 1),
+ FileSize: partSize,
Mtime: time.Now().Unix(),
Crtime: time.Now().Unix(),
Mime: entry.Attributes.Mime,
@@ -483,7 +526,8 @@ func (s3a *S3ApiServer) CopyObjectPartHandler(w http.ResponseWriter, r *http.Req
// Handle zero-size files or empty ranges
if entry.Attributes.FileSize == 0 || endOffset < startOffset {
- // For zero-size files or invalid ranges, create an empty part
+ // For zero-size files or invalid ranges, create an empty part with size 0
+ dstEntry.Attributes.FileSize = 0
dstEntry.Chunks = nil
} else {
// Copy chunks that overlap with the range
@@ -660,15 +704,37 @@ func processMetadataBytes(reqHeader http.Header, existing map[string][]byte, rep
if replaceMeta {
for header, values := range reqHeader {
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)
}
}
}
} else {
+ // Copy existing metadata as-is
+ // Note: Metadata should already be normalized during storage (X-Amz-Meta-*),
+ // but we handle legacy non-canonical formats for backward compatibility
for k, v := range existing {
if strings.HasPrefix(k, s3_constants.AmzUserMetaPrefix) {
+ // Already in canonical format
metadata[k] = v
+ } else if len(k) >= 11 && strings.EqualFold(k[:11], "x-amz-meta-") {
+ // Backward compatibility: migrate old non-canonical format to canonical format
+ // This ensures gradual migration of metadata to consistent format
+ suffix := k[11:] // Extract suffix after "x-amz-meta-"
+ canonicalKey := s3_constants.AmzUserMetaPrefix + suffix
+
+ if glog.V(3) {
+ glog.Infof("Migrating legacy user metadata key %q to canonical format %q during copy", k, canonicalKey)
+ }
+
+ // Check for collision with canonical key
+ if _, exists := metadata[canonicalKey]; exists {
+ glog.Warningf("User metadata key collision during copy migration: canonical key %q already exists, skipping legacy key %q", canonicalKey, k)
+ } else {
+ metadata[canonicalKey] = v
+ }
}
}
}
@@ -1272,6 +1338,7 @@ func (s3a *S3ApiServer) copyMultipartSSEKMSChunk(chunk *filer_pb.FileChunk, dest
}
// Encrypt with destination key
+ originalSize := len(finalData)
encryptedReader, destSSEKey, encErr := CreateSSEKMSEncryptedReaderWithBucketKey(bytes.NewReader(finalData), destKeyID, encryptionContext, bucketKeyEnabled)
if encErr != nil {
return nil, fmt.Errorf("create SSE-KMS encrypted reader: %w", encErr)
@@ -1296,7 +1363,7 @@ func (s3a *S3ApiServer) copyMultipartSSEKMSChunk(chunk *filer_pb.FileChunk, dest
dstChunk.SseType = filer_pb.SSEType_SSE_KMS
dstChunk.SseMetadata = kmsMetadata
- glog.V(4).Infof("Re-encrypted multipart SSE-KMS chunk: %d bytes → %d bytes", len(finalData)-len(reencryptedData)+len(finalData), len(finalData))
+ glog.V(4).Infof("Re-encrypted multipart SSE-KMS chunk: %d bytes → %d bytes", originalSize, len(finalData))
}
// Upload the final data
@@ -1360,10 +1427,12 @@ func (s3a *S3ApiServer) copyMultipartSSECChunk(chunk *filer_pb.FileChunk, copySo
// Calculate the correct IV for this chunk using within-part offset
var chunkIV []byte
+ var ivSkip int
if ssecMetadata.PartOffset > 0 {
- chunkIV = calculateIVWithOffset(chunkBaseIV, ssecMetadata.PartOffset)
+ chunkIV, ivSkip = calculateIVWithOffset(chunkBaseIV, ssecMetadata.PartOffset)
} else {
chunkIV = chunkBaseIV
+ ivSkip = 0
}
// Decrypt the chunk data
@@ -1372,6 +1441,14 @@ func (s3a *S3ApiServer) copyMultipartSSECChunk(chunk *filer_pb.FileChunk, copySo
return nil, nil, fmt.Errorf("create decrypted reader: %w", decErr)
}
+ // CRITICAL: Skip intra-block bytes from CTR decryption (non-block-aligned offset handling)
+ if ivSkip > 0 {
+ _, skipErr := io.CopyN(io.Discard, decryptedReader, int64(ivSkip))
+ if skipErr != nil {
+ return nil, nil, fmt.Errorf("failed to skip intra-block bytes (%d): %w", ivSkip, skipErr)
+ }
+ }
+
decryptedData, readErr := io.ReadAll(decryptedReader)
if readErr != nil {
return nil, nil, fmt.Errorf("decrypt chunk data: %w", readErr)
@@ -1393,6 +1470,7 @@ func (s3a *S3ApiServer) copyMultipartSSECChunk(chunk *filer_pb.FileChunk, copySo
destIV = newIV
// Encrypt with new key and IV
+ originalSize := len(finalData)
encryptedReader, iv, encErr := CreateSSECEncryptedReader(bytes.NewReader(finalData), destKey)
if encErr != nil {
return nil, nil, fmt.Errorf("create encrypted reader: %w", encErr)
@@ -1415,7 +1493,7 @@ func (s3a *S3ApiServer) copyMultipartSSECChunk(chunk *filer_pb.FileChunk, copySo
dstChunk.SseType = filer_pb.SSEType_SSE_C
dstChunk.SseMetadata = ssecMetadata // Use unified metadata field
- glog.V(4).Infof("Re-encrypted multipart SSE-C chunk: %d bytes → %d bytes", len(finalData)-len(reencryptedData)+len(finalData), len(finalData))
+ glog.V(4).Infof("Re-encrypted multipart SSE-C chunk: %d bytes → %d bytes", originalSize, len(finalData))
}
// Upload the final data
@@ -1580,10 +1658,12 @@ func (s3a *S3ApiServer) copyCrossEncryptionChunk(chunk *filer_pb.FileChunk, sour
// Calculate the correct IV for this chunk using within-part offset
var chunkIV []byte
+ var ivSkip int
if ssecMetadata.PartOffset > 0 {
- chunkIV = calculateIVWithOffset(chunkBaseIV, ssecMetadata.PartOffset)
+ chunkIV, ivSkip = calculateIVWithOffset(chunkBaseIV, ssecMetadata.PartOffset)
} else {
chunkIV = chunkBaseIV
+ ivSkip = 0
}
decryptedReader, decErr := CreateSSECDecryptedReader(bytes.NewReader(encryptedData), sourceSSECKey, chunkIV)
@@ -1591,6 +1671,14 @@ func (s3a *S3ApiServer) copyCrossEncryptionChunk(chunk *filer_pb.FileChunk, sour
return nil, fmt.Errorf("create SSE-C decrypted reader: %w", decErr)
}
+ // CRITICAL: Skip intra-block bytes from CTR decryption (non-block-aligned offset handling)
+ if ivSkip > 0 {
+ _, skipErr := io.CopyN(io.Discard, decryptedReader, int64(ivSkip))
+ if skipErr != nil {
+ return nil, fmt.Errorf("failed to skip intra-block bytes (%d): %w", ivSkip, skipErr)
+ }
+ }
+
decryptedData, readErr := io.ReadAll(decryptedReader)
if readErr != nil {
return nil, fmt.Errorf("decrypt SSE-C chunk data: %w", readErr)