@Override
    public Path move(final Path source, final Path renamed, final TransferStatus status, final Delete.Callback callback, final ConnectionCallback connectionCallback) throws BackgroundException {
        Path target;
        if(source.attributes().getCustom().containsKey(KEY_DELETE_MARKER)) {
            // Delete marker, copy not supported but we have to retain the delete marker at the target
            target = new Path(renamed);
            target.attributes().setVersionId(null);
            delete.delete(Collections.singletonMap(target, status), connectionCallback, callback);
            try {
                // Find version id of moved delete marker
                final Path bucket = containerService.getContainer(renamed);
                final VersionOrDeleteMarkersChunk marker = session.getClient().listVersionedObjectsChunked(
                        bucket.isRoot() ? StringUtils.EMPTY : bucket.getName(), containerService.getKey(renamed),
                        String.valueOf(Path.DELIMITER), 1, null, null, false);
                if(marker.getItems().length == 1) {
                    final BaseVersionOrDeleteMarker markerObject = marker.getItems()[0];
                    target.attributes().withVersionId(markerObject.getVersionId()).setCustom(Collections.singletonMap(KEY_DELETE_MARKER, Boolean.TRUE.toString()));
                    delete.delete(Collections.singletonMap(source, status), connectionCallback, callback);
                }
                else {
                    throw new NotfoundException(String.format("Unable to find delete marker %s", renamed.getName()));
                }
            }
            catch(ServiceException e) {
                throw new S3ExceptionMappingService().map("Failure to read attributes of {0}", e, renamed);
            }
        }
        else {
            try {
                target = proxy.copy(source, renamed, status.withLength(source.attributes().getSize()), connectionCallback, new DisabledStreamListener());
                // Copy source path and nullify version id to add a delete marker
                delete.delete(Collections.singletonMap(new Path(source).withAttributes(new PathAttributes(source.attributes()).withVersionId(null)), status),
                        connectionCallback, callback);
            }
            catch(NotfoundException e) {
                if(source.getType().contains(Path.Type.placeholder)) {
                    // No placeholder object to copy, create a new one at the target
                    target = session.getFeature(Directory.class).mkdir(renamed, new TransferStatus().withRegion(source.attributes().getRegion()));
                }
                else {
                    throw e;
                }
            }
        }
        return target;
    }
@Test
    public void testMoveVersioned() throws Exception {
        final Path container = new Path("versioning-test-eu-central-1-cyberduck", EnumSet.of(Path.Type.directory, Path.Type.volume));
        Path test = new Path(container, new AsciiRandomStringService().random(), EnumSet.of(Path.Type.file));
        final S3AccessControlListFeature acl = new S3AccessControlListFeature(session);
        assertNotNull(new S3TouchFeature(session, acl).touch(test, new TransferStatus()).attributes().getVersionId());
        assertTrue(new S3FindFeature(session, acl).find(test));
        // Write some data to add a new version
        final S3WriteFeature feature = new S3WriteFeature(session, acl);
        final byte[] content = RandomUtils.nextBytes(10);
        final TransferStatus status = new TransferStatus();
        status.setLength(content.length);
        status.setChecksum(new SHA256ChecksumCompute().compute(new ByteArrayInputStream(content), status));
        final HttpResponseOutputStream<StorageObject> out = feature.write(test, status, new DisabledConnectionCallback());
        new StreamCopier(status, status).transfer(new ByteArrayInputStream(content), out);
        out.close();
        // Get new path with updated version id
        final AttributedList<Path> list = new S3ListService(session, acl).list(container, new DisabledListProgressListener());
        for(Path path : list) {
            if(new SimplePathPredicate(test).test(path)) {
                test = path;
                break;
            }
        }
        final Path renamed = new Path(container, String.format("%s-renamed", test.getName()), EnumSet.of(Path.Type.file));
        new S3MoveFeature(session, acl).move(test, renamed, new TransferStatus(), new Delete.DisabledCallback(), new DisabledConnectionCallback());
        assertTrue(new S3FindFeature(session, acl).find(test));
        assertTrue(new S3FindFeature(session, acl).find(renamed));
        // Ensure that the latest version of the source file is a delete marker
        for(Path path : new S3ListService(session, acl).list(container, new DisabledListProgressListener())) {
            if(new SimplePathPredicate(test).test(path)) {
                assertTrue(path.attributes().isDuplicate());
                assertTrue(new S3AttributesFinderFeature(session, acl).find(path).isDuplicate());
                assertTrue(new S3AttributesFinderFeature(session, acl).find(path).isDuplicate());
                break;
            }
        }
        new S3DefaultDeleteFeature(session).delete(Collections.singletonList(renamed), new DisabledLoginCallback(), new Delete.DisabledCallback());
    }