io.linguarobot.aws.cdk.maven.FileAssetPublisher Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of aws-cdk-maven-plugin Show documentation
Show all versions of aws-cdk-maven-plugin Show documentation
The AWS CDK Maven plugin produces and deploys CloudFormation templates based on the cloud infrastructure defined
by means of CDK. The goal of the project is to improve the experience of Java developers while working with
CDK by eliminating the need for installing Node.js and interacting with the CDK application by means of CDK
Toolkit.
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