@Override
    protected String copy(final Path source, final S3Object destination, final TransferStatus status, final StreamListener listener) throws BackgroundException {
        try {
            final List<MultipartPart> completed = new ArrayList<>();
            // ID for the initiated multipart upload.
            final MultipartUpload multipart = session.getClient().multipartStartUpload(
                    destination.getBucketName(), destination);
            if(log.isDebugEnabled()) {
                log.debug(String.format("Multipart upload started for %s with ID %s",
                        multipart.getObjectKey(), multipart.getUploadId()));
            }
            final long size = status.getLength();
            long remaining = size;
            long offset = 0;
            final List<Future<MultipartPart>> parts = new ArrayList<>();
            for(int partNumber = 1; remaining > 0; partNumber++) {
                // Last part can be less than 5 MB. Adjust part size.
                final long length = Math.min(Math.max((size / S3DefaultMultipartService.MAXIMUM_UPLOAD_PARTS), partsize), remaining);
                // Submit to queue
                parts.add(this.submit(source, multipart, partNumber, offset, length));
                remaining -= length;
                offset += length;
            }
            for(Future<MultipartPart> f : parts) {
                final MultipartPart part = Interruptibles.await(f);
                completed.add(part);
                listener.sent(part.getSize());
            }
            // Combining all the given parts into the final object. Processing of a Complete Multipart Upload request
            // could take several minutes to complete. Because a request could fail after the initial 200 OK response
            // has been sent, it is important that you check the response body to determine whether the request succeeded.
            final MultipartCompleted complete = session.getClient().multipartCompleteUpload(multipart, completed);
            if(log.isDebugEnabled()) {
                log.debug(String.format("Completed multipart upload for %s with checksum %s",
                        complete.getObjectKey(), complete.getEtag()));
            }
            return complete.getVersionId();
        }
        catch(ServiceException e) {
            throw new S3ExceptionMappingService().map("Cannot copy {0}", e, source);
        }
        finally {
            pool.shutdown(false);
        }
    }
@Test
    public void testCopyAWS4Signature() throws Exception {
        final Path container = new Path("test-eu-central-1-cyberduck", EnumSet.of(Path.Type.directory, Path.Type.volume));
        final Path test = new Path(container, UUID.randomUUID().toString(), EnumSet.of(Path.Type.file));
        final byte[] content = RandomUtils.nextBytes(1023);
        final TransferStatus status = new TransferStatus().withLength(content.length);
        status.setChecksum(new SHA256ChecksumCompute().compute(new ByteArrayInputStream(content), status));
        final OutputStream out = new S3WriteFeature(session, new S3AccessControlListFeature(session)).write(test, status, new DisabledConnectionCallback());
        assertNotNull(out);
        new StreamCopier(new TransferStatus(), new TransferStatus()).transfer(new ByteArrayInputStream(content), out);
        out.close();
        test.attributes().setSize(content.length);
        final Path copy = new Path(container, UUID.randomUUID().toString(), EnumSet.of(Path.Type.file));

        final S3MultipartCopyFeature feature = new S3MultipartCopyFeature(session, new S3AccessControlListFeature(session));
        feature.copy(test, copy, status, new DisabledConnectionCallback(), new DisabledStreamListener());
        assertTrue(new S3FindFeature(session, new S3AccessControlListFeature(session)).find(test));
        assertEquals(content.length, new S3AttributesFinderFeature(session, new S3AccessControlListFeature(session)).find(test).getSize());
        new S3DefaultDeleteFeature(session).delete(Collections.singletonList(test), new DisabledLoginCallback(), new Delete.DisabledCallback());
        assertTrue(new S3FindFeature(session, new S3AccessControlListFeature(session)).find(copy));
        assertEquals(content.length, new S3AttributesFinderFeature(session, new S3AccessControlListFeature(session)).find(copy).getSize());
        new S3DefaultDeleteFeature(session).delete(Collections.singletonList(copy), new DisabledLoginCallback(), new Delete.DisabledCallback());
    }