Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
repositories {
mavenCentral()
}

configurations.all {
resolutionStrategy.preferProjectModules()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package example

import io.micronaut.http.HttpResponse
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import io.micronaut.objectstorage.ObjectStorageOperations
import io.micronaut.objectstorage.request.CreatePresignedUploadRequest
import io.micronaut.objectstorage.response.PresignedUpload

import java.time.Duration
import java.time.Instant

//tag::beginclass[]
@Controller('/uploads')
class PresignedUploadController {

final ObjectStorageOperations<?, ?, ?> objectStorage

PresignedUploadController(ObjectStorageOperations<?, ?, ?> objectStorage) {
this.objectStorage = objectStorage
}
//end::beginclass[]

//tag::presigned-upload[]
@Post(uri = '/signed', consumes = MediaType.APPLICATION_JSON, produces = MediaType.APPLICATION_JSON)
HttpResponse<PresignedUploadResponse> create(@Body CreateUploadCommand command) {
CreatePresignedUploadRequest request =
new CreatePresignedUploadRequest("profiles/${command.userId}.png", Duration.ofMinutes(5)) // <1>
request.contentType = MediaType.IMAGE_PNG
request.metadata = [owner: command.userId]

PresignedUpload signedUpload = objectStorage.createPresignedUpload(request)
.orElseThrow(() -> new IllegalStateException('This provider does not support pre-signed uploads'))

HttpResponse.ok(new PresignedUploadResponse( // <2>
signedUpload.uri.toString(),
signedUpload.method,
signedUpload.headers,
signedUpload.expiration
))
}
//end::presigned-upload[]

static class CreateUploadCommand {
String userId
}

static class PresignedUploadResponse {
final String url
final String method
final Map<String, List<String>> headers
final Instant expiresAt

PresignedUploadResponse(String url,
String method,
Map<String, List<String>> headers,
Instant expiresAt) {
this.url = url
this.method = method
this.headers = headers
this.expiresAt = expiresAt
}
}
}
//tag::endclass[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package example;

import io.micronaut.http.HttpResponse;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import io.micronaut.objectstorage.ObjectStorageOperations;
import io.micronaut.objectstorage.request.CreatePresignedUploadRequest;
import io.micronaut.objectstorage.response.PresignedUpload;

import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Map;

//tag::beginclass[]
@Controller("/uploads")
public class PresignedUploadController {

private final ObjectStorageOperations<?, ?, ?> objectStorage;

public PresignedUploadController(ObjectStorageOperations<?, ?, ?> objectStorage) {
this.objectStorage = objectStorage;
}
//end::beginclass[]

//tag::presigned-upload[]
@Post(uri = "/signed", consumes = MediaType.APPLICATION_JSON, produces = MediaType.APPLICATION_JSON)
public HttpResponse<PresignedUploadResponse> create(@Body CreateUploadCommand command) {
CreatePresignedUploadRequest request =
new CreatePresignedUploadRequest("profiles/" + command.userId() + ".png", Duration.ofMinutes(5)); // <1>
request.setContentType(MediaType.IMAGE_PNG);
request.setMetadata(Map.of("owner", command.userId()));

PresignedUpload signedUpload = objectStorage.createPresignedUpload(request)
.orElseThrow(() -> new IllegalStateException("This provider does not support pre-signed uploads"));

return HttpResponse.ok(new PresignedUploadResponse( // <2>
signedUpload.getUri().toString(),
signedUpload.getMethod(),
signedUpload.getHeaders(),
signedUpload.getExpiration()
));
}
//end::presigned-upload[]

public record CreateUploadCommand(String userId) {
}

public record PresignedUploadResponse(String url,
String method,
Map<String, List<String>> headers,
Instant expiresAt) {
}
}
//tag::endclass[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package example

import io.micronaut.http.HttpResponse
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import io.micronaut.objectstorage.ObjectStorageOperations
import io.micronaut.objectstorage.request.CreatePresignedUploadRequest
import java.time.Duration
import java.time.Instant

//tag::beginclass[]
@Controller("/uploads")
open class PresignedUploadController(private val objectStorage: ObjectStorageOperations<*, *, *>) {
//end::beginclass[]

//tag::presigned-upload[]
@Post(uri = "/signed", consumes = [MediaType.APPLICATION_JSON], produces = [MediaType.APPLICATION_JSON])
open fun create(@Body command: CreateUploadCommand): HttpResponse<PresignedUploadResponse> {
val request = CreatePresignedUploadRequest("profiles/${command.userId}.png", Duration.ofMinutes(5)) // <1>
request.setContentType(MediaType.IMAGE_PNG)
request.setMetadata(mapOf("owner" to command.userId))

val signedUpload = objectStorage.createPresignedUpload(request)
.orElseThrow { IllegalStateException("This provider does not support pre-signed uploads") }

return HttpResponse.ok(
PresignedUploadResponse( // <2>
signedUpload.uri.toString(),
signedUpload.method,
signedUpload.headers,
signedUpload.expiration
)
)
}
//end::presigned-upload[]

data class CreateUploadCommand(val userId: String)

data class PresignedUploadResponse(
val url: String,
val method: String,
val headers: Map<String, List<String>>,
val expiresAt: Instant
)
}
//tag::endclass[]
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@
import io.micronaut.objectstorage.ObjectStorageException;
import io.micronaut.objectstorage.ObjectStorageOperations;
import io.micronaut.objectstorage.configuration.ToggeableCondition;
import io.micronaut.objectstorage.request.CreatePresignedUploadRequest;
import io.micronaut.objectstorage.request.BytesUploadRequest;
import io.micronaut.objectstorage.request.FileUploadRequest;
import io.micronaut.objectstorage.request.ListObjectsRequest;
import io.micronaut.objectstorage.request.UploadRequest;
import io.micronaut.objectstorage.response.ListObjectsResponse;
import io.micronaut.objectstorage.response.PresignedUpload;
import io.micronaut.objectstorage.response.UploadResponse;
import org.jspecify.annotations.NonNull;
import software.amazon.awssdk.awscore.exception.AwsServiceException;
Expand All @@ -48,14 +50,18 @@
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
import software.amazon.awssdk.services.s3.model.S3Object;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.StringJoiner;
Expand Down Expand Up @@ -240,6 +246,40 @@
}
}

@Override
@NonNull
public Optional<PresignedUpload> createPresignedUpload(@NonNull CreatePresignedUploadRequest request) {
try {
PresignedPutObjectRequest presignedRequest;
try (S3Presigner s3Presigner = createS3Presigner()) {
presignedRequest = s3Presigner.presignPutObject(builder -> builder
.signatureDuration(request.getExpiresIn())
.putObjectRequest(getPresignedUploadRequestBuilder(request).build()));

Check warning on line 257 in object-storage-aws/src/main/java/io/micronaut/objectstorage/aws/AwsS3Operations.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consider using the Consumer Builder method instead of creating this nested builder.

See more on https://sonarcloud.io/project/issues?id=micronaut-projects_micronaut-object-storage&issues=AZ1ODgjkY_7EyS8rghiu&open=AZ1ODgjkY_7EyS8rghiu&pullRequest=742
}
return Optional.of(new PresignedUpload(
java.net.URI.create(presignedRequest.url().toString()),
presignedRequest.httpRequest().method().name(),
toPortableHeaders(presignedRequest.signedHeaders()),
presignedRequest.expiration()
));
} catch (RuntimeException e) {
String msg = String.format(
"Error when trying to create a pre-signed upload request with key [%s] in Amazon S3",
request.getKey()
);
throw new ObjectStorageException(msg, e);
}
}

/**
* @return the presigner used to create pre-signed upload requests.
* @since 3.0.0
*/
@NonNull
protected S3Presigner createS3Presigner() {
return S3Presigner.builder().s3Client(s3Client).build();
}

/**
* @param request the upload request
* @return An AWS' {@link PutObjectRequest.Builder} from a Micronaut's {@link UploadRequest}.
Expand All @@ -257,6 +297,24 @@
return builder;
}

/**
* @param request the pre-signed upload request
* @return An AWS' {@link PutObjectRequest.Builder} from a Micronaut pre-signed upload request.
* @since 3.0.0
*/
protected PutObjectRequest.@NonNull Builder getPresignedUploadRequestBuilder(@NonNull CreatePresignedUploadRequest request) {
PutObjectRequest.Builder builder = PutObjectRequest.builder()
.bucket(configuration.getBucket())
.key(request.getKey());

request.getContentType().ifPresent(builder::contentType);
request.getContentLength().ifPresent(builder::contentLength);
if (CollectionUtils.isNotEmpty(request.getMetadata())) {
builder.metadata(request.getMetadata());
}
return builder;
}

/**
* @param uploadRequest the upload request
* @return An AWS' {@link RequestBody} from a Micronaut's {@link UploadRequest}.
Expand Down Expand Up @@ -345,6 +403,13 @@
return new String(Base64.getUrlDecoder().decode(value), StandardCharsets.UTF_8);
}

@NonNull
private Map<String, List<String>> toPortableHeaders(Map<String, List<String>> signedHeaders) {
Map<String, List<String>> headers = new LinkedHashMap<>(signedHeaders.size());

Check warning on line 408 in object-storage-aws/src/main/java/io/micronaut/objectstorage/aws/AwsS3Operations.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this call to the constructor with the better suited static method LinkedHashMap.newLinkedHashMap(int numMappings)

See more on https://sonarcloud.io/project/issues?id=micronaut-projects_micronaut-object-storage&issues=AZ1ODgjkY_7EyS8rghiv&open=AZ1ODgjkY_7EyS8rghiv&pullRequest=742
signedHeaders.forEach((name, values) -> headers.put(name, List.copyOf(values)));
return headers;
}

private record DecodedContinuationToken(Optional<String> rawContinuationToken,
Optional<String> startAfter) {
}
Expand Down
Loading
Loading