From 7ea50effbf823ead8bf580668d3accd7992677ae Mon Sep 17 00:00:00 2001 From: David Turner Date: Sun, 27 Apr 2025 16:00:30 +0100 Subject: [PATCH] Fix `UploadPartCopy` source handling 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 --- muted-tests.yml | 3 -- .../main/java/fixture/s3/S3HttpHandler.java | 31 +++++++++++++------ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/muted-tests.yml b/muted-tests.yml index a7f378a0fa5e1..95d68918ed074 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -447,9 +447,6 @@ tests: - class: org.elasticsearch.xpack.esql.qa.single_node.PushQueriesIT method: testPushCaseInsensitiveEqualityOnDefaults issue: https://github.com/elastic/elasticsearch/issues/127431 -- class: org.elasticsearch.repositories.blobstore.testkit.analyze.S3RepositoryAnalysisRestIT - method: testRepositoryAnalysis - issue: https://github.com/elastic/elasticsearch/issues/127436 # Examples: # diff --git a/test/fixtures/s3-fixture/src/main/java/fixture/s3/S3HttpHandler.java b/test/fixtures/s3-fixture/src/main/java/fixture/s3/S3HttpHandler.java index 30d890dd68db9..9ca9441e6db9f 100644 --- a/test/fixtures/s3-fixture/src/main/java/fixture/s3/S3HttpHandler.java +++ b/test/fixtures/s3-fixture/src/main/java/fixture/s3/S3HttpHandler.java @@ -158,9 +158,9 @@ public void handle(final HttpExchange exchange) throws IOException { exchange.sendResponseHeaders(RestStatus.NOT_FOUND.getStatus(), -1); } else { // CopyPart is UploadPart with an x-amz-copy-source header - final var sourceBlobName = exchange.getRequestHeaders().get("X-amz-copy-source"); - if (sourceBlobName != null) { - var sourceBlob = blobs.get(sourceBlobName.getFirst()); + final var copySource = copySourceName(exchange); + if (copySource != null) { + var sourceBlob = blobs.get(copySource); if (sourceBlob == null) { exchange.sendResponseHeaders(RestStatus.NOT_FOUND.getStatus(), -1); } else { @@ -230,12 +230,10 @@ public void handle(final HttpExchange exchange) throws IOException { exchange.sendResponseHeaders((upload == null ? RestStatus.NOT_FOUND : RestStatus.NO_CONTENT).getStatus(), -1); } else if (request.isPutObjectRequest()) { - // a copy request is a put request with a copy source header - final var copySources = exchange.getRequestHeaders().get("X-amz-copy-source"); - if (copySources != null) { - final var copySource = copySources.getFirst(); - final var prefix = copySource.length() > 0 && copySource.charAt(0) == '/' ? "" : "/"; - var sourceBlob = blobs.get(prefix + copySource); + // a copy request is a put request with an X-amz-copy-source header + final var copySource = copySourceName(exchange); + if (copySource != null) { + var sourceBlob = blobs.get(copySource); if (sourceBlob == null) { exchange.sendResponseHeaders(RestStatus.NOT_FOUND.getStatus(), -1); } else { @@ -516,6 +514,21 @@ static List extractPartEtags(BytesReference completeMultipartUploadBody) } } + @Nullable // if no X-amz-copy-source header present + private static String copySourceName(final HttpExchange exchange) { + final var copySources = exchange.getRequestHeaders().get("X-amz-copy-source"); + if (copySources != null) { + if (copySources.size() != 1) { + throw new AssertionError("multiple X-amz-copy-source headers found: " + copySources); + } + final var copySource = copySources.get(0); + // SDKv1 uses format /bucket/path/blob whereas SDKv2 omits the leading / so we must add it back in + return copySource.length() > 0 && copySource.charAt(0) == '/' ? copySource : ("/" + copySource); + } else { + return null; + } + } + private static HttpHeaderParser.Range parsePartRange(final HttpExchange exchange) { final var sourceRangeHeaders = exchange.getRequestHeaders().get("X-amz-copy-source-range"); if (sourceRangeHeaders == null) {