public void download(S3Artifact s3Artifact, Path downloadTo) {
    final long start = System.currentTimeMillis();
    boolean success = false;

    try {
      downloadThrows(s3Artifact, downloadTo);
      success = true;
    } catch (Throwable t) {
      throw Throwables.propagate(t);
    } finally {
      log.info(
          "S3 Download {}/{} finished {} after {}",
          s3Artifact.getS3Bucket(),
          s3Artifact.getS3ObjectKey(),
          success ? "successfully" : "with error",
          JavaUtils.duration(start));
    }
  }
  private boolean handleChunk(
      S3Artifact s3Artifact,
      Future<Path> future,
      Path downloadTo,
      int chunk,
      long start,
      long remainingMillis) {
    if (remainingMillis <= 0) {
      remainingMillis = 1;
    }

    try {
      Path path = future.get(remainingMillis, TimeUnit.MILLISECONDS);

      if (chunk > 0) {
        combineChunk(downloadTo, path);
      }

      return true;
    } catch (TimeoutException te) {
      log.error(
          "Chunk {} for {} timed out after {} - had {} remaining",
          chunk,
          s3Artifact.getFilename(),
          JavaUtils.duration(start),
          JavaUtils.durationFromMillis(remainingMillis));
      future.cancel(true);
      exceptionNotifier.notify(
          te,
          ImmutableMap.of("filename", s3Artifact.getFilename(), "chunk", Integer.toString(chunk)));
    } catch (Throwable t) {
      log.error("Error while handling chunk {} for {}", chunk, s3Artifact.getFilename(), t);
      exceptionNotifier.notify(
          t,
          ImmutableMap.of("filename", s3Artifact.getFilename(), "chunk", Integer.toString(chunk)));
    }

    return false;
  }
  private void downloadThrows(final S3Artifact s3Artifact, final Path downloadTo) throws Exception {
    log.info("Downloading {}", s3Artifact);

    Jets3tProperties jets3tProperties =
        Jets3tProperties.getInstance(Constants.JETS3T_PROPERTIES_FILENAME);
    jets3tProperties.setProperty(
        "httpclient.socket-timeout-ms",
        Long.toString(configuration.getS3ChunkDownloadTimeoutMillis()));

    final S3Service s3 =
        new RestS3Service(
            getCredentialsForBucket(s3Artifact.getS3Bucket()), null, null, jets3tProperties);

    long length = 0;

    if (s3Artifact.getFilesize().isPresent()) {
      length = s3Artifact.getFilesize().get();
    } else {
      StorageObject details =
          s3.getObjectDetails(s3Artifact.getS3Bucket(), s3Artifact.getS3ObjectKey());

      Preconditions.checkNotNull(
          details,
          "Couldn't find object at %s/%s",
          s3Artifact.getS3Bucket(),
          s3Artifact.getS3ObjectKey());

      length = details.getContentLength();
    }

    int numChunks = (int) (length / configuration.getS3ChunkSize());

    if (length % configuration.getS3ChunkSize() > 0) {
      numChunks++;
    }

    final long chunkSize = length / numChunks + (length % numChunks);

    log.info(
        "Downloading {}/{} in {} chunks of {} bytes to {}",
        s3Artifact.getS3Bucket(),
        s3Artifact.getS3ObjectKey(),
        numChunks,
        chunkSize,
        downloadTo);

    final ExecutorService chunkExecutorService =
        Executors.newFixedThreadPool(
            numChunks,
            new ThreadFactoryBuilder()
                .setDaemon(true)
                .setNameFormat("S3ArtifactDownloaderChunkThread-%d")
                .build());
    final List<Future<Path>> futures = Lists.newArrayListWithCapacity(numChunks);

    for (int chunk = 0; chunk < numChunks; chunk++) {
      futures.add(
          chunkExecutorService.submit(
              new S3ArtifactChunkDownloader(
                  configuration,
                  log,
                  s3,
                  s3Artifact,
                  downloadTo,
                  chunk,
                  chunkSize,
                  length,
                  exceptionNotifier)));
    }

    long remainingMillis = configuration.getS3DownloadTimeoutMillis();
    boolean failed = false;

    for (int chunk = 0; chunk < numChunks; chunk++) {
      final Future<Path> future = futures.get(chunk);

      if (failed) {
        future.cancel(true);
        continue;
      }

      final long start = System.currentTimeMillis();

      if (!handleChunk(s3Artifact, future, downloadTo, chunk, start, remainingMillis)) {
        failed = true;
      }

      remainingMillis -= (System.currentTimeMillis() - start);
    }

    chunkExecutorService.shutdownNow();

    Preconditions.checkState(
        !failed, "Downloading %s/%s failed", s3Artifact.getS3Bucket(), s3Artifact.getS3ObjectKey());
  }