public MultipleFileDownload downloadDirectory(String bucketName, String keyPrefix, File destinationDirectory,
                                                  boolean resumeOnRetry, KeyFilter filter) {
        assertNotObjectLambdaArn(bucketName, "downloadDirectory");

        if ( keyPrefix == null )
            keyPrefix = "";
        if ( filter == null ) {
            filter = KeyFilter.INCLUDE_ALL;
        }
        List<S3ObjectSummary> objectSummaries = new LinkedList<S3ObjectSummary>();
        Stack<String> commonPrefixes = new Stack<String>();
        commonPrefixes.add(keyPrefix);
        long totalSize = 0;
        // Recurse all virtual subdirectories to get a list of object summaries.
        // This is a depth-first search.
        do {
            String prefix = commonPrefixes.pop();
            ObjectListing listObjectsResponse = null;

            do {
                if ( listObjectsResponse == null ) {
                    ListObjectsRequest listObjectsRequest = new ListObjectsRequest().withBucketName(bucketName)
                            .withDelimiter(DEFAULT_DELIMITER).withPrefix(prefix);
                    listObjectsResponse = s3.listObjects(listObjectsRequest);
                } else {
                    listObjectsResponse = s3.listNextBatchOfObjects(listObjectsResponse);
                }

                for ( S3ObjectSummary s : listObjectsResponse.getObjectSummaries() ) {
                    // Skip any files that are also virtual directories, since
                    // we can't save both a directory and a file of the same
                    // name.

                    if ( !filter.shouldInclude(s) ) {
                        log.debug("Skipping " + s.getKey() + " as it does not match filter.");
                        continue;
                    }

                    if ( leavesRoot(destinationDirectory, s.getKey()) ) {
                        throw new RuntimeException("Cannot download key " + s.getKey() +
                            ", its relative path resolves outside the parent directory.");
                    }

                    if ( !s.getKey().equals(prefix)
                            && !listObjectsResponse.getCommonPrefixes().contains(s.getKey() + DEFAULT_DELIMITER) ) {
                        objectSummaries.add(s);
                        totalSize += s.getSize();
                    } else {
                        log.debug("Skipping download for object " + s.getKey()
                                + " since it is also a virtual directory");
                    }
                }

                commonPrefixes.addAll(listObjectsResponse.getCommonPrefixes());
            } while ( listObjectsResponse.isTruncated() );
        } while ( !commonPrefixes.isEmpty() );

        /* This is the hook for adding additional progress listeners */
        ProgressListenerChain additionalListeners = new ProgressListenerChain();

        TransferProgress transferProgress = new TransferProgress();
        transferProgress.setTotalBytesToTransfer(totalSize);
        /*
         * Bind additional progress listeners to this
         * MultipleFileTransferProgressUpdatingListener to receive
         * ByteTransferred events from each single-file download implementation.
         */
        ProgressListener listener = new MultipleFileTransferProgressUpdatingListener(
                transferProgress, additionalListeners);

        List<DownloadImpl> downloads = new ArrayList<DownloadImpl>();

        List<PreparedDownloadContext> preparedDownloadContexts = new ArrayList<PreparedDownloadContext>();

        String description = "Downloading from " + bucketName + "/" + keyPrefix;
        final MultipleFileDownloadImpl multipleFileDownload = new MultipleFileDownloadImpl(description, transferProgress,
                additionalListeners, keyPrefix, bucketName, downloads);
        multipleFileDownload.setMonitor(new MultipleFileTransferMonitor(multipleFileDownload, downloads));

        final CountDownLatch latch = new CountDownLatch(1);
        MultipleFileTransferStateChangeListener transferListener =
                new MultipleFileTransferStateChangeListener(latch, multipleFileDownload);

        if (objectSummaries.isEmpty()) {
            multipleFileDownload.setState(TransferState.Completed);
            return multipleFileDownload;
        }

        for ( S3ObjectSummary summary : objectSummaries ) {
            // TODO: non-standard delimiters
            File f = new File(destinationDirectory, summary.getKey());
            File parentFile = f.getParentFile();

            if ( !parentFile.exists() && !parentFile.mkdirs() ) {
                throw new RuntimeException("Couldn't create parent directories for " + f.getAbsolutePath());
            }

            // All the single-file downloads share the same
            // MultipleFileTransferProgressUpdatingListener and
            // MultipleFileTransferStateChangeListener
            GetObjectRequest req = new GetObjectRequest(summary.getBucketName(), summary.getKey())
                    .<GetObjectRequest>withGeneralProgressListener(
                                            listener);


            PreparedDownloadContext ctx = prepareDownload(req, f, transferListener, null, false, 0, null, 0L, resumeOnRetry, null, null);
            preparedDownloadContexts.add(ctx);
        }

        try {
            for (PreparedDownloadContext ctx : preparedDownloadContexts) {
                downloads.add(submitDownload(ctx));
            }
        } catch (Throwable t) {
            for (DownloadImpl d : downloads) {
                try {
                    d.getMonitor().getFuture().cancel(true);
                } catch (Throwable cancelErr) {
                    log.warn("DownloadImpl could not be aborted", cancelErr);
                }
            }
            throw new SdkClientException(t);
        }

        // Notify all state changes waiting for the downloads to all be queued
        // to wake up and continue.
        latch.countDown();
        return multipleFileDownload;
    }
Downloads all objects in the virtual directory designated by the
keyPrefix given to the destination directory given. All virtual
subdirectories will be downloaded recursively.
<p>
If you are downloading <a href="http://aws.amazon.com/kms/">Amazon Web Services
KMS</a>-encrypted objects, you need to specify the correct region of the
bucket on your client and configure Amazon Web Services Signature Version 4 for added
security. For more information on how to do this, see
http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingAWSSDK.html#
specify-signature-version
</p>

@param bucketName
           The bucket containing the virtual directory
@param keyPrefix
           The key prefix for the virtual directory, or null for the
           entire bucket. All subdirectories will be downloaded
           recursively.
@param destinationDirectory
           The directory to place downloaded files. Subdirectories will
           be created as necessary.
@param resumeOnRetry
           If set to true, upon an immediate retry of a failed object
           download, the <code>TransferManager</code> will resume the
           download from the current end of the file on disk.
@param filter
          If set, applies the filter to determine which keys to include
          in the download request. (default is include all).
aws-java-sdk-s3/src/main/java/com/amazonaws/services/s3/transfer/TransferManager.java