@Override
    public Node 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 {
            final InputStream in;
            if(new SDSTripleCryptEncryptorFeature(session, nodeid).isEncrypted(containerService.getContainer(file))) {
                in = new SDSTripleCryptEncryptorFeature(session, nodeid).encrypt(file, local.getInputStream(), status);
            }
            else {
                in = local.getInputStream();
            }
            final CreateFileUploadRequest createFileUploadRequest = new CreateFileUploadRequest()
                    .directS3Upload(true)
                    .timestampModification(status.getModified() != null ? new DateTime(status.getModified()) : null)
                    .timestampCreation(status.getCreated() != null ? new DateTime(status.getCreated()) : null)
                    .size(TransferStatus.UNKNOWN_LENGTH == status.getLength() ? null : status.getLength())
                    .parentId(Long.parseLong(nodeid.getVersionId(file.getParent())))
                    .name(file.getName());
            final CreateFileUploadResponse createFileUploadResponse = new NodesApi(session.getClient())
                    .createFileUploadChannel(createFileUploadRequest, StringUtils.EMPTY);
            if(log.isDebugEnabled()) {
                log.debug(String.format("upload started for %s with response %s", file, createFileUploadResponse));
            }
            final Map<Integer, TransferStatus> etags = new HashMap<>();
            final List<PresignedUrl> presignedUrls = this.retrievePresignedUrls(createFileUploadResponse, status);
            final List<Future<TransferStatus>> parts = new ArrayList<>();
            try {
                final String random = new UUIDRandomStringService().random();
                // Full size of file
                final long size = status.getLength() + status.getOffset();
                long offset = 0;
                long remaining = status.getLength();
                for(int partNumber = 1; remaining >= 0; partNumber++) {
                    final long length = Math.min(Math.max((size / (MAXIMUM_UPLOAD_PARTS - 1)), partsize), remaining);
                    final PresignedUrl presignedUrl = presignedUrls.get(partNumber - 1);
                    if(new SDSTripleCryptEncryptorFeature(session, nodeid).isEncrypted(containerService.getContainer(file))) {
                        final Local temporary = temp.create(String.format("%s-%d", random, partNumber));
                        if(log.isDebugEnabled()) {
                            log.debug(String.format("Encrypted contents for part %d to %s", partNumber, temporary));
                        }
                        final FileBuffer buffer = new FileBuffer(temporary);
                        new StreamCopier(status, StreamProgress.noop).withAutoclose(false).withLimit(length)
                                .transfer(in, new BufferOutputStream(buffer));
                        parts.add(this.submit(pool, file, temporary, buffer, throttle, listener, status,
                                presignedUrl.getUrl(), presignedUrl.getPartNumber(), 0L, length, callback));
                    }
                    else {
                        parts.add(this.submit(pool, file, local, Buffer.noop, throttle, listener, status,
                                presignedUrl.getUrl(), presignedUrl.getPartNumber(), offset, length, callback));
                    }
                    remaining -= length;
                    offset += length;
                    if(0L == remaining) {
                        break;
                    }
                }
            }
            finally {
                in.close();
            }
            Interruptibles.awaitAll(parts)
                    .forEach(part -> etags.put(part.getPart(), part));
            final CompleteS3FileUploadRequest completeS3FileUploadRequest = new CompleteS3FileUploadRequest()
                    .keepShareLinks(new HostPreferences(session.getHost()).getBoolean("sds.upload.sharelinks.keep"))
                    .resolutionStrategy(CompleteS3FileUploadRequest.ResolutionStrategyEnum.OVERWRITE);
            if(status.getFilekey() != null) {
                final ObjectReader reader = session.getClient().getJSON().getContext(null).readerFor(FileKey.class);
                final FileKey fileKey = reader.readValue(status.getFilekey().array());
                final EncryptedFileKey encryptFileKey = Crypto.encryptFileKey(
                        TripleCryptConverter.toCryptoPlainFileKey(fileKey),
                        TripleCryptConverter.toCryptoUserPublicKey(session.keyPair().getPublicKeyContainer())
                );
                completeS3FileUploadRequest.setFileKey(TripleCryptConverter.toSwaggerFileKey(encryptFileKey));
            }
            etags.forEach((key, value) -> completeS3FileUploadRequest.addPartsItem(
                    new S3FileUploadPart().partEtag(value.getChecksum().hash).partNumber(key)));
            if(log.isDebugEnabled()) {
                log.debug(String.format("Complete file upload with %s for %s", completeS3FileUploadRequest, file));
            }
            new NodesApi(session.getClient()).completeS3FileUpload(completeS3FileUploadRequest, createFileUploadResponse.getUploadId(), StringUtils.EMPTY);
            // Polling
            return new SDSUploadService(session, nodeid).await(file, status, createFileUploadResponse.getUploadId()).getNode();
        }
        catch(CryptoSystemException | InvalidFileKeyException | InvalidKeyPairException | UnknownVersionException e) {
            throw new TripleCryptExceptionMappingService().map("Upload {0} failed", e, file);
        }
        catch(ApiException e) {
            throw new SDSExceptionMappingService(nodeid).map("Upload {0} failed", e, file);
        }
        catch(IOException e) {
            throw new DefaultIOExceptionMappingService().map("Upload {0} failed", e, file);
        }
        finally {
            temp.shutdown();
            // Cancel future tasks
            pool.shutdown(false);
        }
    }
@Test
    public void testUploadExactMultipartSize() throws Exception {
        final SDSNodeIdProvider nodeid = new SDSNodeIdProvider(session);
        final SDSDirectS3UploadFeature feature = new SDSDirectS3UploadFeature(session, nodeid, new SDSDelegatingWriteFeature(session, nodeid, new SDSDirectS3WriteFeature(session, nodeid)));
        final Path room = new SDSDirectoryFeature(session, nodeid).mkdir(
                new Path(new AlphanumericRandomStringService().random(), EnumSet.of(Path.Type.directory, Path.Type.volume)), new TransferStatus());
        final Path test = new Path(room, new AlphanumericRandomStringService().random(), EnumSet.of(Path.Type.file));
        final Local local = new Local(System.getProperty("java.io.tmpdir"), UUID.randomUUID().toString());
        final byte[] random = RandomUtils.nextBytes(10 * 1024 * 1024);
        final OutputStream out = local.getOutputStream(false);
        IOUtils.write(random, out);
        out.close();
        final TransferStatus status = new TransferStatus();
        status.setLength(random.length);
        final Node node = feature.upload(test, local, new BandwidthThrottle(BandwidthThrottle.UNLIMITED),
                new DisabledStreamListener(), status, new DisabledLoginCallback());
        assertTrue(status.isComplete());
        assertNotSame(PathAttributes.EMPTY, status.getResponse());
        assertTrue(new SDSFindFeature(session, nodeid).find(test));
        final PathAttributes attributes = new SDSAttributesFinderFeature(session, nodeid).find(test);
        assertEquals(random.length, attributes.getSize());
        assertEquals(new SDSAttributesAdapter(session).toAttributes(node), attributes);
        new SDSDeleteFeature(session, nodeid).delete(Collections.singletonList(room), new DisabledLoginCallback(), new Delete.DisabledCallback());
        local.delete();
    }