@@ -155,10 +155,33 @@ public void handle(final HttpExchange exchange) throws IOException {
155155 if (upload == null ) {
156156 exchange .sendResponseHeaders (RestStatus .NOT_FOUND .getStatus (), -1 );
157157 } else {
158- final Tuple <String , BytesReference > blob = parseRequestBody (exchange );
159- upload .addPart (blob .v1 (), blob .v2 ());
160- exchange .getResponseHeaders ().add ("ETag" , blob .v1 ());
161- exchange .sendResponseHeaders (RestStatus .OK .getStatus (), -1 );
158+ // CopyPart is UploadPart with an x-amz-copy-source header
159+ final var sourceBlobName = exchange .getRequestHeaders ().get ("X-amz-copy-source" );
160+ if (sourceBlobName != null ) {
161+ var sourceBlob = blobs .get (sourceBlobName .getFirst ());
162+ if (sourceBlob == null ) {
163+ exchange .sendResponseHeaders (RestStatus .NOT_FOUND .getStatus (), -1 );
164+ } else {
165+ var range = parsePartRange (exchange );
166+ // we'll assume for tests that the source object on the heap is under 2G
167+ var part = sourceBlob .slice (range .v1 ().intValue (), range .v2 ().intValue ());
168+ var etag = UUIDs .randomBase64UUID ();
169+ upload .addPart (etag , part );
170+ byte [] response = ("""
171+ <?xml version="1.0" encoding="UTF-8"?>
172+ <CopyPartResult>
173+ <ETag>%s</ETag>
174+ </CopyPartResult>""" .formatted (etag )).getBytes (StandardCharsets .UTF_8 );
175+ exchange .getResponseHeaders ().add ("Content-Type" , "application/xml" );
176+ exchange .sendResponseHeaders (RestStatus .OK .getStatus (), response .length );
177+ exchange .getResponseBody ().write (response );
178+ }
179+ } else {
180+ final Tuple <String , BytesReference > blob = parseRequestBody (exchange );
181+ upload .addPart (blob .v1 (), blob .v2 ());
182+ exchange .getResponseHeaders ().add ("ETag" , blob .v1 ());
183+ exchange .sendResponseHeaders (RestStatus .OK .getStatus (), -1 );
184+ }
162185 }
163186
164187 } else if (request .isCompleteMultipartUploadRequest ()) {
@@ -205,14 +228,18 @@ public void handle(final HttpExchange exchange) throws IOException {
205228 final var sourceBlobName = exchange .getRequestHeaders ().get ("X-amz-copy-source" );
206229 if (sourceBlobName != null ) {
207230 var sourceBlob = blobs .get (sourceBlobName .getFirst ());
208- blobs .put (request .path (), sourceBlob );
231+ if (sourceBlob == null ) {
232+ exchange .sendResponseHeaders (RestStatus .NOT_FOUND .getStatus (), -1 );
233+ } else {
234+ blobs .put (request .path (), sourceBlob );
209235
210- byte [] response = ("""
211- <?xml version="1.0" encoding="UTF-8"?>
212- <CopyObjectResult></CopyObjectResult>""" ).getBytes (StandardCharsets .UTF_8 );
213- exchange .getResponseHeaders ().add ("Content-Type" , "application/xml" );
214- exchange .sendResponseHeaders (RestStatus .OK .getStatus (), response .length );
215- exchange .getResponseBody ().write (response );
236+ byte [] response = ("""
237+ <?xml version="1.0" encoding="UTF-8"?>
238+ <CopyObjectResult></CopyObjectResult>""" ).getBytes (StandardCharsets .UTF_8 );
239+ exchange .getResponseHeaders ().add ("Content-Type" , "application/xml" );
240+ exchange .sendResponseHeaders (RestStatus .OK .getStatus (), response .length );
241+ exchange .getResponseBody ().write (response );
242+ }
216243 } else {
217244 final Tuple <String , BytesReference > blob = parseRequestBody (exchange );
218245 blobs .put (request .path (), blob .v2 ());
@@ -481,6 +508,27 @@ static List<String> extractPartEtags(BytesReference completeMultipartUploadBody)
481508 }
482509 }
483510
511+ private static final Pattern rangePattern = Pattern .compile ("^bytes=([0-9]+)-([0-9]+)$" );
512+
513+ private static Tuple <Long , Long > parsePartRange (final HttpExchange exchange ) {
514+ final var sourceRangeHeaders = exchange .getRequestHeaders ().get ("X-amz-copy-source-range" );
515+ if (sourceRangeHeaders == null ) {
516+ throw new IllegalStateException ("missing x-amz-copy-source-range header" );
517+ }
518+ if (sourceRangeHeaders .size () != 1 ) {
519+ throw new IllegalStateException ("expected 1 x-amz-copy-source-range header, found " + sourceRangeHeaders .size ());
520+ }
521+ final var sourceRangeHeader = sourceRangeHeaders .getFirst ();
522+ final Matcher matcher = rangePattern .matcher (sourceRangeHeader );
523+ if (matcher .find () == false ) {
524+ throw new IllegalStateException ("invalid x-amz-copy-source-range header [" + sourceRangeHeader + "]" );
525+ }
526+ final var start = Long .parseLong (matcher .group (1 ));
527+ final var end = Long .parseLong (matcher .group (2 ));
528+
529+ return new Tuple <>(start , end - start + 1 );
530+ }
531+
484532 MultipartUpload getUpload (String uploadId ) {
485533 return uploads .get (uploadId );
486534 }
0 commit comments