Skip to content

Commit 9772b5e

Browse files
authored
Fix UploadPartCopy source handling (#127442)
The leading `/` on the `x-amz-copy-source` header is optional; SDKv1 includes it but SDKv2 does not. Today we handle its omission when handling a `CopyObject` request but we must do the same for the `UploadPartCopy` path. Closes #127436
1 parent 3057168 commit 9772b5e

File tree

2 files changed

+22
-12
lines changed

2 files changed

+22
-12
lines changed

muted-tests.yml

-3
Original file line numberDiff line numberDiff line change
@@ -447,9 +447,6 @@ tests:
447447
- class: org.elasticsearch.xpack.esql.qa.single_node.PushQueriesIT
448448
method: testPushCaseInsensitiveEqualityOnDefaults
449449
issue: https://github.com/elastic/elasticsearch/issues/127431
450-
- class: org.elasticsearch.repositories.blobstore.testkit.analyze.S3RepositoryAnalysisRestIT
451-
method: testRepositoryAnalysis
452-
issue: https://github.com/elastic/elasticsearch/issues/127436
453450

454451
# Examples:
455452
#

test/fixtures/s3-fixture/src/main/java/fixture/s3/S3HttpHandler.java

+22-9
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,9 @@ public void handle(final HttpExchange exchange) throws IOException {
158158
exchange.sendResponseHeaders(RestStatus.NOT_FOUND.getStatus(), -1);
159159
} else {
160160
// CopyPart is UploadPart with an x-amz-copy-source header
161-
final var sourceBlobName = exchange.getRequestHeaders().get("X-amz-copy-source");
162-
if (sourceBlobName != null) {
163-
var sourceBlob = blobs.get(sourceBlobName.getFirst());
161+
final var copySource = copySourceName(exchange);
162+
if (copySource != null) {
163+
var sourceBlob = blobs.get(copySource);
164164
if (sourceBlob == null) {
165165
exchange.sendResponseHeaders(RestStatus.NOT_FOUND.getStatus(), -1);
166166
} else {
@@ -230,12 +230,10 @@ public void handle(final HttpExchange exchange) throws IOException {
230230
exchange.sendResponseHeaders((upload == null ? RestStatus.NOT_FOUND : RestStatus.NO_CONTENT).getStatus(), -1);
231231

232232
} else if (request.isPutObjectRequest()) {
233-
// a copy request is a put request with a copy source header
234-
final var copySources = exchange.getRequestHeaders().get("X-amz-copy-source");
235-
if (copySources != null) {
236-
final var copySource = copySources.getFirst();
237-
final var prefix = copySource.length() > 0 && copySource.charAt(0) == '/' ? "" : "/";
238-
var sourceBlob = blobs.get(prefix + copySource);
233+
// a copy request is a put request with an X-amz-copy-source header
234+
final var copySource = copySourceName(exchange);
235+
if (copySource != null) {
236+
var sourceBlob = blobs.get(copySource);
239237
if (sourceBlob == null) {
240238
exchange.sendResponseHeaders(RestStatus.NOT_FOUND.getStatus(), -1);
241239
} else {
@@ -516,6 +514,21 @@ static List<String> extractPartEtags(BytesReference completeMultipartUploadBody)
516514
}
517515
}
518516

517+
@Nullable // if no X-amz-copy-source header present
518+
private static String copySourceName(final HttpExchange exchange) {
519+
final var copySources = exchange.getRequestHeaders().get("X-amz-copy-source");
520+
if (copySources != null) {
521+
if (copySources.size() != 1) {
522+
throw new AssertionError("multiple X-amz-copy-source headers found: " + copySources);
523+
}
524+
final var copySource = copySources.get(0);
525+
// SDKv1 uses format /bucket/path/blob whereas SDKv2 omits the leading / so we must add it back in
526+
return copySource.length() > 0 && copySource.charAt(0) == '/' ? copySource : ("/" + copySource);
527+
} else {
528+
return null;
529+
}
530+
}
531+
519532
private static HttpHeaderParser.Range parsePartRange(final HttpExchange exchange) {
520533
final var sourceRangeHeaders = exchange.getRequestHeaders().get("X-amz-copy-source-range");
521534
if (sourceRangeHeaders == null) {

0 commit comments

Comments
 (0)