@Override
    public StorageObject upload(final Path file, final Local local, final BandwidthThrottle throttle, final StreamListener listener,
                                final TransferStatus status, final ConnectionCallback callback) throws BackgroundException {
        final ThreadPool pool = ThreadPoolFactory.get("multipart", concurrency);
        try {
            MultipartUpload multipart = null;
            try {
                if(status.isAppend()) {
                    final List<MultipartUpload> list = multipartService.find(file);
                    if(!list.isEmpty()) {
                        multipart = list.iterator().next();
                    }
                }
            }
            catch(AccessDeniedException | InteroperabilityException e) {
                log.warn(String.format("Ignore failure listing incomplete multipart uploads. %s", e));
            }
            final Path bucket = containerService.getContainer(file);
            final List<MultipartPart> completed = new ArrayList<>();
            // Not found or new upload
            if(null == multipart) {
                if(log.isInfoEnabled()) {
                    log.info("No pending multipart upload found");
                }
                final S3Object object = new S3WriteFeature(session, acl).getDetails(file, status);
                // ID for the initiated multipart upload.
                multipart = session.getClient().multipartStartUpload(bucket.isRoot() ? StringUtils.EMPTY : bucket.getName(), object);
                if(log.isDebugEnabled()) {
                    log.debug(String.format("Multipart upload started for %s with ID %s", multipart.getObjectKey(), multipart.getUploadId()));
                }
            }
            else {
                if(status.isAppend()) {
                    // Add already completed parts
                    completed.addAll(multipartService.list(multipart));
                }
            }
            // Full size of file
            final long size = status.getOffset() + status.getLength();
            final List<Future<MultipartPart>> parts = new ArrayList<>();
            long remaining = status.getLength();
            long offset = 0;
            for(int partNumber = 1; remaining > 0; partNumber++) {
                boolean skip = false;
                if(status.isAppend()) {
                    if(log.isInfoEnabled()) {
                        log.info(String.format("Determine if part number %d can be skipped", partNumber));
                    }
                    for(MultipartPart c : completed) {
                        if(c.getPartNumber().equals(partNumber)) {
                            if(log.isInfoEnabled()) {
                                log.info(String.format("Skip completed part number %d", partNumber));
                            }
                            skip = true;
                            offset += c.getSize();
                            break;
                        }
                    }
                }
                if(!skip) {
                    // Last part can be less than 5 MB. Adjust part size.
                    final long length = Math.min(Math.max((size / (S3DefaultMultipartService.MAXIMUM_UPLOAD_PARTS - 1)), partsize), remaining);
                    // Submit to queue
                    parts.add(this.submit(pool, file, local, throttle, listener, status, multipart, partNumber, offset, length, callback));
                    remaining -= length;
                    offset += length;
                }
            }
            completed.addAll(Interruptibles.awaitAll(parts));
            // 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.
            multipart.setBucketName(bucket.isRoot() ? StringUtils.EMPTY : bucket.getName());
            final MultipartCompleted complete = session.getClient().multipartCompleteUpload(multipart, completed);
            if(log.isInfoEnabled()) {
                log.info(String.format("Completed multipart upload for %s with %d parts and checksum %s",
                        complete.getObjectKey(), completed.size(), complete.getEtag()));
            }
            if(file.getType().contains(Path.Type.encrypted)) {
                log.warn(String.format("Skip checksum verification for %s with client side encryption enabled", file));
            }
            else {
                if(S3Session.isAwsHostname(session.getHost().getHostname())) {
                    completed.sort(new MultipartPart.PartNumberComparator());
                    final StringBuilder concat = new StringBuilder();
                    for(MultipartPart part : completed) {
                        concat.append(part.getEtag());
                    }
                    final String expected = String.format("%s-%d",
                            ChecksumComputeFactory.get(HashAlgorithm.md5).compute(concat.toString()), completed.size());
                    final String reference = StringUtils.remove(complete.getEtag(), "\"");
                    if(!StringUtils.equalsIgnoreCase(expected, reference)) {
                        throw new ChecksumException(MessageFormat.format(LocaleFactory.localizedString("Upload {0} failed", "Error"), file.getName()),
                                MessageFormat.format("Mismatch between MD5 hash {0} of uploaded data and ETag {1} returned by the server",
                                        expected, reference));
                    }
                }
            }
            final StorageObject object = new StorageObject(containerService.getKey(file));
            object.setETag(complete.getEtag());
            object.setContentLength(size);
            object.setStorageClass(multipart.getStorageClass());
            if(multipart.getMetadata() != null) {
                object.addAllMetadata(multipart.getMetadata());
            }
            // Mark parent status as complete
            status.withResponse(new S3AttributesAdapter(session.getHost()).toAttributes(object)).setComplete();
            return object;
        }
        catch(ServiceException e) {
            throw new S3ExceptionMappingService().map("Upload {0} failed", e, file);
        }
        finally {
            // Cancel future tasks
            pool.shutdown(false);
        }
    }
@Test
    public void testUploadBucketInHostname() throws Exception {
        final S3AccessControlListFeature acl = new S3AccessControlListFeature(virtualhost);
        final S3MultipartUploadService service = new S3MultipartUploadService(virtualhost, new S3WriteFeature(virtualhost, acl), acl, 5 * 1024L * 1024L, 2);
        final Path container = new Path("test-eu-central-1-cyberduck", EnumSet.of(Path.Type.directory, Path.Type.volume));
        final String name = String.format(" %s.txt", UUID.randomUUID());
        final Path test = new Path(container, name, EnumSet.of(Path.Type.file));
        final Local local = new Local(System.getProperty("java.io.tmpdir"), name);
        final byte[] random = RandomUtils.nextBytes(1021);
        IOUtils.write(random, local.getOutputStream(false));
        final TransferStatus status = new TransferStatus();
        status.setLength(random.length);
        status.setStorageClass(S3Object.STORAGE_CLASS_REDUCED_REDUNDANCY);
        final BytecountStreamListener count = new BytecountStreamListener();
        service.upload(test, local, new BandwidthThrottle(BandwidthThrottle.UNLIMITED),
                count, status, new DisabledLoginCallback());
        assertEquals(random.length, count.getSent());
        assertSame(Checksum.NONE, status.getResponse().getChecksum());
        assertTrue(status.isComplete());
        assertNotSame(PathAttributes.EMPTY, status.getResponse());
        assertEquals(random.length, status.getResponse().getSize());
        assertTrue(new S3FindFeature(virtualhost, acl).find(test));
        final PathAttributes attr = new S3AttributesFinderFeature(virtualhost, acl).find(test);
        assertEquals(status.getResponse().getETag(), attr.getETag());
        assertEquals(status.getResponse().getChecksum(), attr.getChecksum());
        assertEquals(random.length, attr.getSize());
        assertEquals(Checksum.NONE, attr.getChecksum());
        assertNotNull(attr.getETag());
        // d2b77e21aa68ebdcbfb589124b9f9192-1
        assertEquals(Checksum.NONE, Checksum.parse(attr.getETag()));
        assertEquals(S3Object.STORAGE_CLASS_REDUCED_REDUNDANCY, new S3StorageClassFeature(virtualhost, acl).getClass(test));
        new S3DefaultDeleteFeature(virtualhost).delete(Collections.singletonList(test), new DisabledLoginCallback(), new Delete.DisabledCallback());
        local.delete();
    }