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

io.linguarobot.aws.cdk.maven.FileAssetPublisher Maven / Gradle / Ivy

package io.linguarobot.aws.cdk.maven;

import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.core.async.AsyncRequestBody;
import software.amazon.awssdk.services.s3.S3AsyncClient;
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.CreateMultipartUploadRequest;
import software.amazon.awssdk.services.s3.model.CreateMultipartUploadResponse;
import software.amazon.awssdk.services.s3.model.UploadPartRequest;

import javax.annotation.Nullable;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * Publishes file assets to S3.
 */
public class FileAssetPublisher {

    private static final Logger logger = LoggerFactory.getLogger(FileAssetPublisher.class);

    private final ResolvedEnvironment environment;

    private S3AsyncClient s3Client;

    public FileAssetPublisher(ResolvedEnvironment environment) {
        this.environment = environment;
    }

    /**
     * Uploads a file or a directory (zipping it before uploading) to S3 bucket.
     *
     * @param file the file or directory to be uploaded
     * @param objectName the name of the object in the bucket
     * @param bucketName the name of the bucket
     * @throws IOException if I/O error occurs while uploading a file or directory
     */
    public void publish(Path file, String objectName, String bucketName) throws IOException {
        logger.info("Publishing file asset, file={}, bucketName={}, objectName={}", file, bucketName, objectName);
        if (Files.isDirectory(file)) {
            publishDirectory(file, objectName, bucketName);
        } else {
            publishFile(file, objectName, bucketName);
        }
    }

    /**
     * Zips the directory and uploads it to S3 bucket.
     */
    private void publishDirectory(Path directory, String objectName, String bucketName) throws IOException {
        try (
                OutputStream outputStream = new S3ObjectOutputStream(getS3Client(), bucketName, objectName, "application/zip");
                ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)
        ) {
            Files.walkFileTree(directory, new SimpleFileVisitor() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    ZipEntry zipEntry = new ZipEntry(directory.relativize(file).toString());
                    zipOutputStream.putNextEntry(zipEntry);
                    Files.copy(file, zipOutputStream);
                    zipOutputStream.closeEntry();
                    return FileVisitResult.CONTINUE;
                }
            });
        }
    }

    /**
     * Uploads the file to S3 bucket.
     */
    private void publishFile(Path file, String objectName, String bucketName) throws IOException {
        try (OutputStream outputStream = new S3ObjectOutputStream(getS3Client(), bucketName, objectName)) {
            Files.copy(file, outputStream);
        }
    }

    private S3AsyncClient getS3Client() {
        if (this.s3Client == null) {
            this.s3Client = S3AsyncClient.builder()
                    .region(environment.getRegion())
                    .credentialsProvider(environment.getCredentialsProvider())
                    .build();
        }

        return s3Client;
    }

    private static class S3ObjectOutputStream extends OutputStream {

        private static final int MINIMUM_PART_SIZE = 5 * 1024 * 1024;

        private S3AsyncClient s3Client;
        private CreateMultipartUploadResponse createUploadResponse;
        private List> parts;
        private ByteBuffer buffer;

        S3ObjectOutputStream(S3AsyncClient s3Client, String bucketName, String objectKey) {
            this(s3Client, bucketName, objectKey, null);
        }

        S3ObjectOutputStream(S3AsyncClient s3Client, String bucketName, String objectKey, @Nullable String contentType) {
            this(s3Client, bucketName, objectKey, contentType, MINIMUM_PART_SIZE);
        }

        S3ObjectOutputStream(S3AsyncClient s3Client, String bucketName, String objectKey, String contentType, int partSize) {
            if (partSize <= 0) {
                throw new IllegalArgumentException("The minimum part size is 5 MB (" + MINIMUM_PART_SIZE + " bytes)");
            }
            this.s3Client = s3Client;
            this.buffer = ByteBuffer.allocate(partSize);
            this.parts = new ArrayList<>();
            CreateMultipartUploadRequest uploadRequest = buildUploadRequest(bucketName, objectKey, contentType);
            this.createUploadResponse = s3Client.createMultipartUpload(uploadRequest).join();
        }

        @Override
        public void write(int b) throws IOException {
            if (isClosed()) {
                throw new IOException("The stream is closed");
            }

            if (!buffer.hasRemaining()) {
                flush();
            }

            buffer.put((byte) b);
        }

        @Override
        public void write(@NotNull byte[] bytes, int offset, int length) throws IOException {
            if (isClosed()) {
                throw new IOException("The stream is closed");
            }
            int remaining = buffer.remaining();
            buffer.put(bytes, offset, Math.min(remaining, length));
            if (remaining < length) {
                flush();
                write(bytes, offset + remaining, length - remaining);
            }
        }

        @Override
        public void flush() {
            if (!isClosed()) {
                buffer.flip();
                if (buffer.remaining() > 0) {
                    CompletableFuture part = CompletableFuture.completedFuture(parts.size() + 1)
                            .thenApply(this::buildUploadPartRequest)
                            .thenCompose(uploadPartRequest -> {
                                AsyncRequestBody requestBody = AsyncRequestBody.fromByteBuffer(buffer);
                                return s3Client.uploadPart(uploadPartRequest, requestBody)
                                        .thenApply(r -> completedPart(r.eTag(), uploadPartRequest.partNumber()));
                            });
                    parts.add(part);
                }
                buffer.clear();
            }
        }

        @Override
        public void close() {
            if (!isClosed()) {
                flush();
                join(this.parts)
                        .thenCompose(completedParts -> {
                            CompleteMultipartUploadRequest completeUploadRequest = buildCompleteUploadRequest(completedParts);
                            return s3Client.completeMultipartUpload(completeUploadRequest);
                        })
                        .join();

                s3Client = null;
                this.createUploadResponse = null;
                buffer = null;
                this.parts = null;
            }

        }

        private  CompletableFuture> join(List> futures) {
            return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
                    .thenApply(r -> futures.stream()
                            .map(CompletableFuture::join)
                            .collect(Collectors.toList()));
        }

        private boolean isClosed() {
            return buffer == null;
        }

        private CreateMultipartUploadRequest buildUploadRequest(String bucketName,
                                                                String objectKey,
                                                                @Nullable String contentType) {

            CreateMultipartUploadRequest.Builder requestBuilder = CreateMultipartUploadRequest.builder()
                    .bucket(bucketName)
                    .key(objectKey);
            if (contentType != null) {
                requestBuilder = requestBuilder.contentType(contentType);
            }

            return requestBuilder.build();
        }

        private UploadPartRequest buildUploadPartRequest(int partNumber) {
            return UploadPartRequest.builder()
                    .bucket(createUploadResponse.bucket())
                    .key(createUploadResponse.key())
                    .uploadId(createUploadResponse.uploadId())
                    .partNumber(partNumber)
                    .build();
        }

        private CompletedPart completedPart(String eTag, int partNumber) {
            return CompletedPart.builder()
                    .eTag(eTag)
                    .partNumber(partNumber)
                    .build();
        }

        private CompleteMultipartUploadRequest buildCompleteUploadRequest(Collection parts) {
            CompletedMultipartUpload completedMultipartUpload = CompletedMultipartUpload.builder()
                    .parts(parts)
                    .build();

            return CompleteMultipartUploadRequest.builder()
                    .bucket(createUploadResponse.bucket())
                    .key(createUploadResponse.key())
                    .uploadId(createUploadResponse.uploadId())
                    .multipartUpload(completedMultipartUpload)
                    .build();
        }

    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy