diff --git a/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/ObjectMetadata.java b/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/ObjectMetadata.java index 10131a86e..ab49feda9 100644 --- a/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/ObjectMetadata.java +++ b/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/ObjectMetadata.java @@ -260,6 +260,95 @@ void apply(PutObjectRequest.Builder builder) { } } + void apply(CopyObjectRequest.Builder builder) { + builder.metadataDirective(MetadataDirective.REPLACE); + if (acl != null) { + builder.acl(acl); + } + if (cacheControl != null) { + builder.cacheControl(cacheControl); + } + if (contentDisposition != null) { + builder.contentDisposition(contentDisposition); + } + if (contentEncoding != null) { + builder.contentEncoding(contentEncoding); + } + if (contentLanguage != null) { + builder.contentLanguage(contentLanguage); + } + if (contentType != null) { + builder.contentType(contentType); + } + if (expires != null) { + builder.expires(expires); + } + if (grantFullControl != null) { + builder.grantFullControl(grantFullControl); + } + if (grantRead != null) { + builder.grantRead(grantRead); + } + if (grantReadACP != null) { + builder.grantReadACP(grantReadACP); + } + if (grantWriteACP != null) { + builder.grantWriteACP(grantWriteACP); + } + if (metadata != null) { + builder.metadata(metadata); + } + if (serverSideEncryption != null) { + builder.serverSideEncryption(serverSideEncryption); + } + if (storageClass != null) { + builder.storageClass(storageClass); + } + if (websiteRedirectLocation != null) { + builder.websiteRedirectLocation(websiteRedirectLocation); + } + if (sseCustomerAlgorithm != null) { + builder.sseCustomerAlgorithm(sseCustomerAlgorithm); + } + if (sseCustomerKey != null) { + builder.sseCustomerKey(sseCustomerKey); + } + if (sseCustomerKeyMD5 != null) { + builder.sseCustomerKeyMD5(sseCustomerKeyMD5); + } + if (ssekmsKeyId != null) { + builder.ssekmsKeyId(ssekmsKeyId); + } + if (ssekmsEncryptionContext != null) { + builder.ssekmsEncryptionContext(ssekmsEncryptionContext); + } + if (bucketKeyEnabled != null) { + builder.bucketKeyEnabled(bucketKeyEnabled); + } + if (requestPayer != null) { + builder.requestPayer(requestPayer); + } + if (tagging != null) { + builder.taggingDirective(TaggingDirective.REPLACE); + builder.tagging(tagging); + } + if (objectLockMode != null) { + builder.objectLockMode(objectLockMode); + } + if (objectLockRetainUntilDate != null) { + builder.objectLockRetainUntilDate(objectLockRetainUntilDate); + } + if (objectLockLegalHoldStatus != null) { + builder.objectLockLegalHoldStatus(objectLockLegalHoldStatus); + } + if (expectedBucketOwner != null) { + builder.expectedBucketOwner(expectedBucketOwner); + } + if (checksumAlgorithm != null) { + builder.checksumAlgorithm(checksumAlgorithm); + } + } + void apply(CreateMultipartUploadRequest.Builder builder) { if (acl != null) { builder.acl(acl); diff --git a/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3Operations.java b/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3Operations.java index 0fca61e3e..3b687129e 100644 --- a/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3Operations.java +++ b/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3Operations.java @@ -164,6 +164,19 @@ default S3Resource upload(String bucketName, String key, InputStream inputStream */ S3Resource download(String bucketName, String key); + /** + * Copies an object from one S3 location to another. + * + * @param sourceBucketName - the source bucket name + * @param sourceKey - the source object key + * @param destinationBucketName - the destination bucket name + * @param destinationKey - the destination object key + * @param objectMetadata - the object metadata + * @return copied object represented as {@link S3Resource} + */ + S3Resource copy(String sourceBucketName, String sourceKey, String destinationBucketName, String destinationKey, + @Nullable ObjectMetadata objectMetadata); + /** * Creates a signed URL for retrieving an object from S3. * diff --git a/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3Template.java b/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3Template.java index 92194bc14..c3b72ee96 100644 --- a/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3Template.java +++ b/spring-cloud-aws-s3/src/main/java/io/awspring/cloud/s3/S3Template.java @@ -24,6 +24,7 @@ import org.springframework.util.Assert; import org.springframework.util.StreamUtils; import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.CopyObjectRequest; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; @@ -194,6 +195,25 @@ public S3Resource download(String bucketName, String key) { return new S3Resource(bucketName, key, s3Client, s3OutputStreamProvider); } + @Override + public S3Resource copy(String sourceBucketName, String sourceKey, String destinationBucketName, + String destinationKey, @Nullable ObjectMetadata objectMetadata) { + Assert.notNull(sourceBucketName, "sourceBucketName is required"); + Assert.notNull(sourceKey, "sourceKey is required"); + Assert.notNull(destinationBucketName, "destinationBucketName is required"); + Assert.notNull(destinationKey, "destinationKey is required"); + + CopyObjectRequest.Builder requestBuilder = CopyObjectRequest.builder().sourceBucket(sourceBucketName) + .sourceKey(sourceKey).destinationBucket(destinationBucketName).destinationKey(destinationKey); + + if (objectMetadata != null) { + objectMetadata.apply(requestBuilder); + } + + s3Client.copyObject(requestBuilder.build()); + return new S3Resource(destinationBucketName, destinationKey, s3Client, s3OutputStreamProvider); + } + @Override public URL createSignedGetURL(String bucketName, String key, Duration duration) { Assert.notNull(bucketName, "bucketName is required"); diff --git a/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/S3TemplateIntegrationTests.java b/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/S3TemplateIntegrationTests.java index d524ab410..6f005657e 100644 --- a/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/S3TemplateIntegrationTests.java +++ b/spring-cloud-aws-s3/src/test/java/io/awspring/cloud/s3/S3TemplateIntegrationTests.java @@ -260,6 +260,35 @@ void downloadsFile() throws IOException { } } + @Test + void copiesObject() throws IOException { + client.putObject(r -> r.bucket(BUCKET_NAME).key("file.txt"), RequestBody.fromString("hello")); + + S3Resource copied = s3Template.copy(BUCKET_NAME, "file.txt", BUCKET_NAME, + "file-copy.txt", null); + + assertThat(copied).isNotNull(); + assertThat(copied.getLocation().getObject()).isEqualTo("file-copy.txt"); + ResponseInputStream response = client + .getObject(r -> r.bucket(BUCKET_NAME).key("file-copy.txt")); + String result = StreamUtils.copyToString(response, StandardCharsets.UTF_8); + assertThat(result).isEqualTo("hello"); + } + + @Test + void copiesObjectWithMetadata() { + client.putObject(r -> r.bucket(BUCKET_NAME).key("file.gz"), RequestBody.fromString("hello")); + + S3Resource copied = s3Template.copy(BUCKET_NAME, "file.gz", BUCKET_NAME, "file-copy.gz", + ObjectMetadata.builder().contentEncoding("gzip").contentType("application/gzip").build()); + + assertThat(copied).isNotNull(); + HeadObjectResponse headObjectResponse = client + .headObject(HeadObjectRequest.builder().bucket(BUCKET_NAME).key("file-copy.gz").build()); + assertThat(headObjectResponse.contentEncoding()).isEqualTo("gzip"); + assertThat(headObjectResponse.contentType()).isEqualTo("application/gzip"); + } + @Test void createsWorkingSignedGetURL() throws IOException { client.putObject(r -> r.bucket(BUCKET_NAME).key("file.txt"), RequestBody.fromString("hello"));