|
|
@ -36,13 +36,14 @@ func (s3a *S3ApiServer) CopyObjectHandler(w http.ResponseWriter, r *http.Request |
|
|
dstBucket, dstObject := s3_constants.GetBucketAndObject(r) |
|
|
dstBucket, dstObject := s3_constants.GetBucketAndObject(r) |
|
|
|
|
|
|
|
|
// Copy source path.
|
|
|
// 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 { |
|
|
if err != nil { |
|
|
// Save unescaped string as is.
|
|
|
// 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) |
|
|
glog.V(3).Infof("CopyObjectHandler %s %s (version: %s) => %s %s", srcBucket, srcObject, srcVersionId, dstBucket, dstObject) |
|
|
|
|
|
|
|
|
@ -356,16 +357,29 @@ func pathToBucketAndObject(path string) (bucket, object string) { |
|
|
return "", "" |
|
|
return "", "" |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func pathToBucketObjectAndVersion(path string) (bucket, object, versionId string) { |
|
|
|
|
|
// Parse versionId from query string if present
|
|
|
|
|
|
// Format: /bucket/object?versionId=version-id
|
|
|
|
|
|
// Use net/url.Parse to properly handle all query parameters
|
|
|
|
|
|
if u, err := url.Parse(path); err == nil && u.Query().Has("versionId") { |
|
|
|
|
|
versionId = u.Query().Get("versionId") |
|
|
|
|
|
path = u.Path |
|
|
|
|
|
|
|
|
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 |
|
|
return bucket, object, versionId |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -380,22 +394,21 @@ func (s3a *S3ApiServer) CopyObjectPartHandler(w http.ResponseWriter, r *http.Req |
|
|
dstBucket, dstObject := s3_constants.GetBucketAndObject(r) |
|
|
dstBucket, dstObject := s3_constants.GetBucketAndObject(r) |
|
|
|
|
|
|
|
|
// Copy source path.
|
|
|
// Copy source path.
|
|
|
cpSrcPath := r.Header.Get("X-Amz-Copy-Source") |
|
|
|
|
|
|
|
|
rawCopySource := r.Header.Get("X-Amz-Copy-Source") |
|
|
|
|
|
|
|
|
glog.V(2).Infof("CopyObjectPart: Raw copy source header=%q (len=%d)", cpSrcPath, len(cpSrcPath)) |
|
|
|
|
|
|
|
|
glog.V(2).Infof("CopyObjectPart: Raw copy source header=%q (len=%d)", rawCopySource, len(rawCopySource)) |
|
|
|
|
|
|
|
|
// Try URL unescaping - AWS SDK sends URL-encoded copy sources
|
|
|
// Try URL unescaping - AWS SDK sends URL-encoded copy sources
|
|
|
unescapedPath, err := url.QueryUnescape(cpSrcPath) |
|
|
|
|
|
|
|
|
cpSrcPath, err := url.QueryUnescape(rawCopySource) |
|
|
if err != nil { |
|
|
if err != nil { |
|
|
// If unescaping fails, log and use original
|
|
|
// If unescaping fails, log and use original
|
|
|
glog.V(2).Infof("CopyObjectPart: Failed to unescape copy source %q: %v, using as-is", cpSrcPath, err) |
|
|
|
|
|
unescapedPath = cpSrcPath |
|
|
|
|
|
|
|
|
glog.V(2).Infof("CopyObjectPart: Failed to unescape copy source %q: %v, using as-is", rawCopySource, err) |
|
|
|
|
|
cpSrcPath = rawCopySource |
|
|
} |
|
|
} |
|
|
cpSrcPath = unescapedPath |
|
|
|
|
|
|
|
|
|
|
|
glog.V(2).Infof("CopyObjectPart: After unescape=%q (len=%d, bytes=%v)", cpSrcPath, len(cpSrcPath), []byte(cpSrcPath)) |
|
|
glog.V(2).Infof("CopyObjectPart: After unescape=%q (len=%d, bytes=%v)", cpSrcPath, len(cpSrcPath), []byte(cpSrcPath)) |
|
|
|
|
|
|
|
|
srcBucket, srcObject, srcVersionId := pathToBucketObjectAndVersion(cpSrcPath) |
|
|
|
|
|
|
|
|
srcBucket, srcObject, srcVersionId := pathToBucketObjectAndVersion(rawCopySource, 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) |
|
|
srcBucket, len(srcBucket), srcObject, len(srcObject), srcVersionId) |
|
|
|