All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.artipie.asto.s3.MultipartUpload Maven / Gradle / Ivy

There is a newer version: v1.17.16
Show newest version
/*
 * The MIT License (MIT) Copyright (c) 2020-2023 artipie.com
 * https://github.com/artipie/asto/LICENSE.txt
 */
package com.artipie.asto.s3;

import com.artipie.asto.Content;
import com.artipie.asto.Key;
import com.artipie.asto.Splitting;
import hu.akarnokd.rxjava2.interop.SingleInterop;
import io.reactivex.Flowable;
import io.reactivex.Observable;
import java.nio.ByteBuffer;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.reactivestreams.Publisher;
import software.amazon.awssdk.core.async.AsyncRequestBody;
import software.amazon.awssdk.services.s3.model.AbortMultipartUploadRequest;
import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadRequest;
import software.amazon.awssdk.services.s3.model.CompletedMultipartUpload;
import software.amazon.awssdk.services.s3.model.CompletedPart;
import software.amazon.awssdk.services.s3.model.UploadPartRequest;
import software.amazon.awssdk.services.s3.model.UploadPartResponse;

/**
 * Multipart upload of S3 object.
 *
 * @since 0.1
 */
final class MultipartUpload {

    /**
     * Minimum part size.
     * See 
     * Amazon S3 multipart upload limits
     */
    private static final int MIN_PART_SIZE = 5 * 1024 * 1024;

    /**
     * Bucket.
     */
    private final Bucket bucket;

    /**
     * S3 object key.
     */
    private final Key key;

    /**
     * ID of this upload.
     */
    private final String id;

    /**
     * Uploaded parts.
     */
    private final List parts;

    /**
     * Ctor.
     *
     * @param bucket Bucket.
     * @param key S3 object key.
     * @param id ID of this upload.
     */
    MultipartUpload(final Bucket bucket, final Key key, final String id) {
        this.bucket = bucket;
        this.key = key;
        this.id = id;
        this.parts = new CopyOnWriteArrayList<>();
    }

    /**
     * Uploads all content by parts.
     *
     * @param content Object content to be uploaded in parts.
     * @return Completion stage which is completed when responses received from S3 for all parts.
     */
    public CompletionStage upload(final Content content) {
        final AtomicInteger counter = new AtomicInteger();
        return Flowable.fromPublisher(content)
            .concatMap(
                buffer -> Flowable.fromPublisher(
                    new Splitting(buffer, MultipartUpload.MIN_PART_SIZE).publisher()
                )
            ).map(
                chunk -> {
                    final int pnum = counter.incrementAndGet();
                    return this.uploadPart(
                        pnum,
                        Flowable.just(chunk)
                    ).thenAccept(
                        response -> this.parts.add(
                            new UploadedPart(pnum, response.eTag())
                        )
                    );
                }
            ).reduce(
                CompletableFuture.allOf(),
                (acc, stage) -> acc.thenCompose(o -> stage)
            ).to(SingleInterop.get())
            .thenCompose(Function.identity());
    }

    /**
     * Completes the upload.
     *
     * @return Completion stage which is completed when success response received from S3.
     */
    public CompletionStage complete() {
        return this.bucket.completeMultipartUpload(
            CompleteMultipartUploadRequest.builder()
                .key(this.key.string())
                .uploadId(this.id)
                .multipartUpload(
                    CompletedMultipartUpload.builder()
                        .parts(
                            this.parts.stream()
                                .sorted(Comparator.comparingInt(p -> p.pnum))
                                .map(
                                    UploadedPart::completedPart
                                ).collect(Collectors.toList())
                        ).build()
                )
                .build()
        ).thenApply(ignored -> null);
    }

    /**
     * Aborts the upload.
     *
     * @return Completion stage which is completed when success response received from S3.
     */
    public CompletionStage abort() {
        return this.bucket.abortMultipartUpload(
            AbortMultipartUploadRequest.builder()
                .key(this.key.string())
                .uploadId(this.id)
                .build()
        ).thenApply(ignored -> null);
    }

    /**
     * Uploads part.
     *
     * @param part Part number.
     * @param content Part content to be uploaded.
     * @return Completion stage which is completed when success response received from S3.
     */
    private CompletionStage uploadPart(
        final int part,
        final Publisher content) {
        return Observable.fromPublisher(content)
            .reduce(0L, (total, buf) -> total + buf.remaining())
            .to(SingleInterop.get())
            .toCompletableFuture()
            .thenCompose(
                length -> this.bucket.uploadPart(
                    UploadPartRequest.builder()
                        .key(this.key.string())
                        .uploadId(this.id)
                        .partNumber(part)
                        .contentLength(length)
                        .build(),
                    AsyncRequestBody.fromPublisher(content)
                )
            );
    }

    /**
     * Uploaded part.
     * @since 1.12.0
     */
    private static class UploadedPart {
        /**
         * Part's number.
         */
        private final int pnum;

        /**
         * Entity tag for the uploaded object.
         */
        private final String tag;

        /**
         * Ctor.
         *
         * @param pnum Part's number.
         * @param tag Entity tag for the uploaded object..
         */
        UploadedPart(final int pnum, final String tag) {
            this.pnum = pnum;
            this.tag = tag;
        }

        /**
         * Builds {@code CompletedPart}.
         *
         * @return CompletedPart.
         */
        CompletedPart completedPart() {
            return CompletedPart.builder()
                .partNumber(this.pnum)
                .eTag(this.tag)
                .build();
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy