From 250d5c6a9111a38fcb3151b368f1238d133f5687 Mon Sep 17 00:00:00 2001 From: sbiscigl Date: Thu, 11 Jun 2026 19:53:37 -0400 Subject: [PATCH] copy tags and metadata in s3crt copy object when directive is defined --- .../include/aws/s3-crt/S3CrtClient.h | 3 + .../aws-cpp-sdk-s3-crt/source/S3CrtClient.cpp | 127 ++++++++++++++++ .../BucketAndObjectOperationTest.cpp | 89 ++++++++++++ .../velocity/cpp/s3/S3ClientHeader.vm | 3 + .../velocity/cpp/s3/SmithyS3ClientHeader.vm | 3 + .../cpp/s3/s3-crt/S3CrtSpecificOperations.vm | 136 ++++++++++++++++++ .../s3-crt/SmithyS3CrtSpecificOperations.vm | 136 ++++++++++++++++++ 7 files changed, 497 insertions(+) diff --git a/generated/src/aws-cpp-sdk-s3-crt/include/aws/s3-crt/S3CrtClient.h b/generated/src/aws-cpp-sdk-s3-crt/include/aws/s3-crt/S3CrtClient.h index 3b0646e2a878..049fa35c843f 100644 --- a/generated/src/aws-cpp-sdk-s3-crt/include/aws/s3-crt/S3CrtClient.h +++ b/generated/src/aws-cpp-sdk-s3-crt/include/aws/s3-crt/S3CrtClient.h @@ -8533,6 +8533,9 @@ class AWS_S3CRT_API S3CrtClient : public Aws::Client::AWSXMLClient, const Aws::AmazonWebServiceRequest* request, const Aws::Http::URI& uri, Aws::Http::HttpMethod method) const; + Model::CopyObjectOutcome PopulateCopyObjectProperties(const Model::CopyObjectRequest& request, + const std::shared_ptr& httpRequest) const; + typedef Aws::Utils::Outcome, S3CrtError> InvokeOperationOutcome; InvokeOperationOutcome InvokeServiceOperation(const AmazonWebServiceRequest& request, diff --git a/generated/src/aws-cpp-sdk-s3-crt/source/S3CrtClient.cpp b/generated/src/aws-cpp-sdk-s3-crt/source/S3CrtClient.cpp index 3cdc6ac8eeb9..57c1f370a3a4 100644 --- a/generated/src/aws-cpp-sdk-s3-crt/source/S3CrtClient.cpp +++ b/generated/src/aws-cpp-sdk-s3-crt/source/S3CrtClient.cpp @@ -819,6 +819,126 @@ void S3CrtClient::InitCommonCrtRequestOption(CrtRequestCallbackUserData* userDat options->finish_callback = S3CrtRequestFinishCallback; } +Model::CopyObjectOutcome S3CrtClient::PopulateCopyObjectProperties(const Model::CopyObjectRequest& request, + const std::shared_ptr& httpRequest) const { + const bool copyMetadata = request.MetadataDirectiveHasBeenSet() && request.GetMetadataDirective() == Model::MetadataDirective::COPY; + const bool copyTags = request.TaggingDirectiveHasBeenSet() && request.GetTaggingDirective() == Model::TaggingDirective::COPY; + + if (!copyMetadata && !copyTags) { + return Model::CopyObjectOutcome(Model::CopyObjectResult()); + } + + // Parse "[/]bucket/key[?versionId=...]" from x-amz-copy-source. rfind: keys may contain '?'. + Aws::String copySource = request.GetCopySource(); + Aws::String sourceVersionId; + const auto queryPos = copySource.rfind('?'); + if (queryPos != Aws::String::npos) { + const Aws::String query = copySource.substr(queryPos + 1); + copySource = copySource.substr(0, queryPos); + const auto versionPos = query.find("versionId="); + if (versionPos != Aws::String::npos) { + sourceVersionId = query.substr(versionPos + Aws::String("versionId=").size()); + const auto ampPos = sourceVersionId.find('&'); + if (ampPos != Aws::String::npos) { + sourceVersionId = sourceVersionId.substr(0, ampPos); + } + } + } + if (!copySource.empty() && copySource.front() == '/') { + copySource = copySource.substr(1); + } + const auto slashPos = copySource.find('/'); + if (slashPos == Aws::String::npos) { + return Model::CopyObjectOutcome(Aws::Client::AWSError(S3CrtErrors::INVALID_PARAMETER_VALUE, "INVALID_PARAMETER_VALUE", + "Could not parse bucket and key from CopySource for property copy", + false)); + } + const Aws::String sourceBucket = copySource.substr(0, slashPos); + const Aws::String sourceKey = copySource.substr(slashPos + 1); + + Model::HeadObjectRequest headRequest; + headRequest.SetBucket(sourceBucket); + headRequest.SetKey(sourceKey); + if (!sourceVersionId.empty()) { + headRequest.SetVersionId(sourceVersionId); + } + auto headOutcome = HeadObject(headRequest); + if (!headOutcome.IsSuccess()) { + return Model::CopyObjectOutcome(headOutcome.GetError()); + } + const auto& head = headOutcome.GetResult(); + const Aws::String sourceETag = head.GetETag(); + + // Pin the source version so UploadPartCopy reads a stable object. + if (sourceVersionId.empty() && !head.GetVersionId().empty()) { + sourceVersionId = head.GetVersionId(); + httpRequest->SetHeaderValue("x-amz-copy-source", Aws::Http::URI::URLEncodePath(copySource) + "?versionId=" + sourceVersionId); + } + if (!httpRequest->HasHeader("x-amz-copy-source-if-match") && !sourceETag.empty()) { + httpRequest->SetHeaderValue("x-amz-copy-source-if-match", sourceETag); + } + + // small objects are single-part copies where S3 honors the directives natively, skip injection + if (head.GetContentLength() < (1LL * 1024 * 1024 * 1024)) { + return Model::CopyObjectOutcome(Model::CopyObjectResult()); + } + + if (copyMetadata) { + if (!head.GetContentType().empty()) { + httpRequest->SetHeaderValue("content-type", head.GetContentType()); + } + if (!head.GetContentEncoding().empty()) { + httpRequest->SetHeaderValue("content-encoding", head.GetContentEncoding()); + } + if (!head.GetContentDisposition().empty()) { + httpRequest->SetHeaderValue("content-disposition", head.GetContentDisposition()); + } + if (!head.GetContentLanguage().empty()) { + httpRequest->SetHeaderValue("content-language", head.GetContentLanguage()); + } + if (!head.GetCacheControl().empty()) { + httpRequest->SetHeaderValue("cache-control", head.GetCacheControl()); + } + if (!head.GetExpiresString().empty()) { + httpRequest->SetHeaderValue("expires", head.GetExpires().ToGmtString(Aws::Utils::DateFormat::RFC822)); + } + for (const auto& item : head.GetMetadata()) { + httpRequest->SetHeaderValue("x-amz-meta-" + item.first, item.second); + } + // REPLACE so CreateMultipartUpload emits these headers. + httpRequest->SetHeaderValue("x-amz-metadata-directive", "REPLACE"); + } + + if (copyTags) { + Model::GetObjectTaggingRequest taggingRequest; + taggingRequest.SetBucket(sourceBucket); + taggingRequest.SetKey(sourceKey); + if (!sourceVersionId.empty()) { + taggingRequest.SetVersionId(sourceVersionId); + } + auto taggingOutcome = GetObjectTagging(taggingRequest); + if (!taggingOutcome.IsSuccess()) { + return Model::CopyObjectOutcome(taggingOutcome.GetError()); + } + Aws::StringStream tagStream; + bool firstTag = true; + for (const auto& tag : taggingOutcome.GetResult().GetTagSet()) { + if (!firstTag) { + tagStream << "&"; + } + tagStream << Aws::Utils::StringUtils::URLEncode(tag.GetKey().c_str()) << "=" + << Aws::Utils::StringUtils::URLEncode(tag.GetValue().c_str()); + firstTag = false; + } + if (!firstTag) { + httpRequest->SetHeaderValue("x-amz-tagging", tagStream.str()); + httpRequest->SetHeaderValue("x-amz-tagging-directive", "REPLACE"); + } + } + + return Model::CopyObjectOutcome(Model::CopyObjectResult()); +} + static void CopyObjectRequestShutdownCallback(void* user_data) { if (!user_data) { AWS_LOGSTREAM_ERROR("CopyObject", "user data passed is NULL "); @@ -915,6 +1035,13 @@ void S3CrtClient::CopyObjectAsync(const CopyObjectRequest& request, const CopyOb "Unable to create s3 meta request", false)), handlerContext); } + { + auto copyPropertiesOutcome = PopulateCopyObjectProperties(request, userData->request); + if (!copyPropertiesOutcome.IsSuccess()) { + Aws::Delete(userData); + return handler(this, request, copyPropertiesOutcome, handlerContext); + } + } if (handlerContext) { handlerContext->GetMonitorContext().StartMonitorContext(Aws::String{"S3CrtClient"}, request.GetServiceRequestName(), userData->request); } diff --git a/tests/aws-cpp-sdk-s3-crt-integration-tests/BucketAndObjectOperationTest.cpp b/tests/aws-cpp-sdk-s3-crt-integration-tests/BucketAndObjectOperationTest.cpp index 04221ca94bf8..7d307c431a61 100644 --- a/tests/aws-cpp-sdk-s3-crt-integration-tests/BucketAndObjectOperationTest.cpp +++ b/tests/aws-cpp-sdk-s3-crt-integration-tests/BucketAndObjectOperationTest.cpp @@ -43,6 +43,10 @@ #include #include #include +#include +#include +#include +#include #include #include #include @@ -988,6 +992,91 @@ namespace AWS_ASSERT_SUCCESS(copyOutcome); } + TEST_F(BucketAndObjectOperationTest, TestCopyObjectCopiesMetadataAndTags) + { + Aws::String fullBucketName = CalculateBucketName(BASE_OBJECTS_BUCKET_NAME.c_str()); + SCOPED_TRACE(Aws::String("FullBucketName ") + fullBucketName); + CreateBucketRequest createBucketRequest; + createBucketRequest.SetBucket(fullBucketName); + createBucketRequest.SetACL(BucketCannedACL::private_); + CreateBucketOutcome createBucketOutcome = Client->CreateBucket(createBucketRequest); + AWS_ASSERT_SUCCESS(createBucketOutcome); + ASSERT_TRUE(WaitForBucketToPropagate(fullBucketName)); + TagTestBucket(fullBucketName, Client); + + const char* sourceKey = "copy-props-source"; + const char* destKey = "copy-props-destination"; + + auto objectStream = Aws::MakeShared(ALLOCATION_TAG); + *objectStream << "high level copy metadata and tags test payload"; + objectStream->flush(); + PutObjectRequest putObjectRequest; + putObjectRequest.SetBucket(fullBucketName); + putObjectRequest.SetKey(sourceKey); + putObjectRequest.SetBody(objectStream); + putObjectRequest.SetContentLength(static_cast(putObjectRequest.GetBody()->tellp())); + putObjectRequest.SetContentType("application/x-high-level-copy"); + putObjectRequest.AddMetadata("project", "highlevelcopy"); + putObjectRequest.AddMetadata("owner", "sdk-team"); + PutObjectOutcome putObjectOutcome = Client->PutObject(putObjectRequest); + AWS_ASSERT_SUCCESS(putObjectOutcome); + ASSERT_TRUE(WaitForObjectToPropagate(fullBucketName, sourceKey)); + + { + PutObjectTaggingRequest putTaggingRequest; + putTaggingRequest.SetBucket(fullBucketName); + putTaggingRequest.SetKey(sourceKey); + Tagging tagging; + Tag t1; t1.SetKey("env"); t1.SetValue("test"); + Tag t2; t2.SetKey("team"); t2.SetValue("sdk"); + tagging.AddTagSet(t1); + tagging.AddTagSet(t2); + putTaggingRequest.SetTagging(tagging); + auto putTaggingOutcome = Client->PutObjectTagging(putTaggingRequest); + AWS_ASSERT_SUCCESS(putTaggingOutcome); + } + + CopyObjectRequest copyRequest; + copyRequest.WithBucket(fullBucketName) + .WithKey(destKey) + .WithCopySource(fullBucketName + "/" + sourceKey) + .WithMetadataDirective(MetadataDirective::COPY) + .WithTaggingDirective(TaggingDirective::COPY); + auto copyOutcome = Client->CopyObject(copyRequest); + AWS_ASSERT_SUCCESS(copyOutcome); + ASSERT_TRUE(WaitForObjectToPropagate(fullBucketName, destKey)); + + { + HeadObjectRequest headRequest; + headRequest.SetBucket(fullBucketName); + headRequest.SetKey(destKey); + auto headOutcome = Client->HeadObject(headRequest); + AWS_ASSERT_SUCCESS(headOutcome); + const auto& metadata = headOutcome.GetResult().GetMetadata(); + ASSERT_EQ(1u, metadata.count("project")); + ASSERT_STREQ("highlevelcopy", metadata.at("project").c_str()); + ASSERT_EQ(1u, metadata.count("owner")); + ASSERT_STREQ("sdk-team", metadata.at("owner").c_str()); + ASSERT_STREQ("application/x-high-level-copy", headOutcome.GetResult().GetContentType().c_str()); + } + + { + GetObjectTaggingRequest getTaggingRequest; + getTaggingRequest.SetBucket(fullBucketName); + getTaggingRequest.SetKey(destKey); + auto getTaggingOutcome = Client->GetObjectTagging(getTaggingRequest); + AWS_ASSERT_SUCCESS(getTaggingOutcome); + Aws::Map tags; + for (const auto& tag : getTaggingOutcome.GetResult().GetTagSet()) + { + tags[tag.GetKey()] = tag.GetValue(); + } + ASSERT_EQ(2u, tags.size()); + ASSERT_STREQ("test", tags["env"].c_str()); + ASSERT_STREQ("sdk", tags["team"].c_str()); + } + } + TEST_F(BucketAndObjectOperationTest, TestObjectOperationWithEventStream) { GTEST_SKIP() << "Select objects is not supported on new AWS accounts"; diff --git a/tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/s3/S3ClientHeader.vm b/tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/s3/S3ClientHeader.vm index 696b765b02d5..f8af76b7c88c 100644 --- a/tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/s3/S3ClientHeader.vm +++ b/tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/s3/S3ClientHeader.vm @@ -254,6 +254,9 @@ namespace ${rootNamespace} aws_s3_meta_request_options *options, const Aws::AmazonWebServiceRequest *request, const Aws::Http::URI &uri, Aws::Http::HttpMethod method) const; + + Model::CopyObjectOutcome PopulateCopyObjectProperties(const Model::CopyObjectRequest &request, + const std::shared_ptr &httpRequest) const; #else #if(!$serviceModel.endpointRules) void init(const Aws::Client::ClientConfiguration& clientConfiguration); diff --git a/tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/s3/SmithyS3ClientHeader.vm b/tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/s3/SmithyS3ClientHeader.vm index b7efc350f766..aaba43c76d0d 100644 --- a/tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/s3/SmithyS3ClientHeader.vm +++ b/tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/s3/SmithyS3ClientHeader.vm @@ -266,6 +266,9 @@ namespace ${rootNamespace} aws_s3_meta_request_options *options, const Aws::AmazonWebServiceRequest *request, const Aws::Http::URI &uri, Aws::Http::HttpMethod method) const; + + Model::CopyObjectOutcome PopulateCopyObjectProperties(const Model::CopyObjectRequest &request, + const std::shared_ptr &httpRequest) const; #else #if(!$serviceModel.endpointRules) void init(const Aws::Client::ClientConfiguration& clientConfiguration); diff --git a/tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/s3/s3-crt/S3CrtSpecificOperations.vm b/tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/s3/s3-crt/S3CrtSpecificOperations.vm index 92b0487d052a..8b0ea6905fba 100644 --- a/tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/s3/s3-crt/S3CrtSpecificOperations.vm +++ b/tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/s3/s3-crt/S3CrtSpecificOperations.vm @@ -305,6 +305,132 @@ void S3CrtClient::InitCommonCrtRequestOption(CrtRequestCallbackUserData *userDat options->progress_callback = S3CrtRequestProgressCallback; options->finish_callback = S3CrtRequestFinishCallback; } + +Model::CopyObjectOutcome S3CrtClient::PopulateCopyObjectProperties(const Model::CopyObjectRequest &request, + const std::shared_ptr &httpRequest) const +{ + const bool copyMetadata = request.MetadataDirectiveHasBeenSet() && + request.GetMetadataDirective() == Model::MetadataDirective::COPY; + const bool copyTags = request.TaggingDirectiveHasBeenSet() && + request.GetTaggingDirective() == Model::TaggingDirective::COPY; + + if (!copyMetadata && !copyTags) + { + return Model::CopyObjectOutcome(Model::CopyObjectResult()); + } + + // Parse "[/]bucket/key[?versionId=...]" from x-amz-copy-source. rfind: keys may contain '?'. + Aws::String copySource = request.GetCopySource(); + Aws::String sourceVersionId; + const auto queryPos = copySource.rfind('?'); + if (queryPos != Aws::String::npos) + { + const Aws::String query = copySource.substr(queryPos + 1); + copySource = copySource.substr(0, queryPos); + const auto versionPos = query.find("versionId="); + if (versionPos != Aws::String::npos) + { + sourceVersionId = query.substr(versionPos + Aws::String("versionId=").size()); + const auto ampPos = sourceVersionId.find('&'); + if (ampPos != Aws::String::npos) + { + sourceVersionId = sourceVersionId.substr(0, ampPos); + } + } + } + if (!copySource.empty() && copySource.front() == '/') + { + copySource = copySource.substr(1); + } + const auto slashPos = copySource.find('/'); + if (slashPos == Aws::String::npos) + { + return Model::CopyObjectOutcome(Aws::Client::AWSError(S3CrtErrors::INVALID_PARAMETER_VALUE, + "INVALID_PARAMETER_VALUE", "Could not parse bucket and key from CopySource for property copy", false)); + } + const Aws::String sourceBucket = copySource.substr(0, slashPos); + const Aws::String sourceKey = copySource.substr(slashPos + 1); + + Model::HeadObjectRequest headRequest; + headRequest.SetBucket(sourceBucket); + headRequest.SetKey(sourceKey); + if (!sourceVersionId.empty()) + { + headRequest.SetVersionId(sourceVersionId); + } + auto headOutcome = HeadObject(headRequest); + if (!headOutcome.IsSuccess()) + { + return Model::CopyObjectOutcome(headOutcome.GetError()); + } + const auto& head = headOutcome.GetResult(); + const Aws::String sourceETag = head.GetETag(); + + // Pin the source version so UploadPartCopy reads a stable object. + if (sourceVersionId.empty() && !head.GetVersionId().empty()) + { + sourceVersionId = head.GetVersionId(); + httpRequest->SetHeaderValue("x-amz-copy-source", Aws::Http::URI::URLEncodePath(copySource) + "?versionId=" + sourceVersionId); + } + if (!httpRequest->HasHeader("x-amz-copy-source-if-match") && !sourceETag.empty()) + { + httpRequest->SetHeaderValue("x-amz-copy-source-if-match", sourceETag); + } + + // small objects are single-part copies where S3 honors the directives natively, skip injection + if (head.GetContentLength() < (1LL * 1024 * 1024 * 1024)) + { + return Model::CopyObjectOutcome(Model::CopyObjectResult()); + } + + if (copyMetadata) + { + if (!head.GetContentType().empty()) { httpRequest->SetHeaderValue("content-type", head.GetContentType()); } + if (!head.GetContentEncoding().empty()) { httpRequest->SetHeaderValue("content-encoding", head.GetContentEncoding()); } + if (!head.GetContentDisposition().empty()) { httpRequest->SetHeaderValue("content-disposition", head.GetContentDisposition()); } + if (!head.GetContentLanguage().empty()) { httpRequest->SetHeaderValue("content-language", head.GetContentLanguage()); } + if (!head.GetCacheControl().empty()) { httpRequest->SetHeaderValue("cache-control", head.GetCacheControl()); } + if (!head.GetExpiresString().empty()) { httpRequest->SetHeaderValue("expires", head.GetExpires().ToGmtString(Aws::Utils::DateFormat::RFC822)); } + for (const auto& item : head.GetMetadata()) + { + httpRequest->SetHeaderValue("x-amz-meta-" + item.first, item.second); + } + // REPLACE so CreateMultipartUpload emits these headers. + httpRequest->SetHeaderValue("x-amz-metadata-directive", "REPLACE"); + } + + if (copyTags) + { + Model::GetObjectTaggingRequest taggingRequest; + taggingRequest.SetBucket(sourceBucket); + taggingRequest.SetKey(sourceKey); + if (!sourceVersionId.empty()) + { + taggingRequest.SetVersionId(sourceVersionId); + } + auto taggingOutcome = GetObjectTagging(taggingRequest); + if (!taggingOutcome.IsSuccess()) + { + return Model::CopyObjectOutcome(taggingOutcome.GetError()); + } + Aws::StringStream tagStream; + bool firstTag = true; + for (const auto& tag : taggingOutcome.GetResult().GetTagSet()) + { + if (!firstTag) { tagStream << "&"; } + tagStream << Aws::Utils::StringUtils::URLEncode(tag.GetKey().c_str()) << "=" + << Aws::Utils::StringUtils::URLEncode(tag.GetValue().c_str()); + firstTag = false; + } + if (!firstTag) + { + httpRequest->SetHeaderValue("x-amz-tagging", tagStream.str()); + httpRequest->SetHeaderValue("x-amz-tagging-directive", "REPLACE"); + } + } + + return Model::CopyObjectOutcome(Model::CopyObjectResult()); +} #end #foreach($operation in $serviceModel.operations) @@ -436,6 +562,16 @@ void ${className}::${operation.name}Async(${constText}${operation.request.shape. } #else InitCommonCrtRequestOption(userData, &options, &request, uri, Aws::Http::HttpMethod::HTTP_${operation.http.method}); +#end +#if($operation.name == "CopyObject") + { + auto copyPropertiesOutcome = PopulateCopyObjectProperties(request, userData->request); + if (!copyPropertiesOutcome.IsSuccess()) + { + Aws::Delete(userData); + return handler(this, request, copyPropertiesOutcome, handlerContext); + } + } #end if(handlerContext) { diff --git a/tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/s3/s3-crt/SmithyS3CrtSpecificOperations.vm b/tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/s3/s3-crt/SmithyS3CrtSpecificOperations.vm index 009a2c5f8b25..4f36ae398020 100644 --- a/tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/s3/s3-crt/SmithyS3CrtSpecificOperations.vm +++ b/tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/s3/s3-crt/SmithyS3CrtSpecificOperations.vm @@ -305,6 +305,132 @@ void S3CrtClient::InitCommonCrtRequestOption(CrtRequestCallbackUserData *userDat options->progress_callback = S3CrtRequestProgressCallback; options->finish_callback = S3CrtRequestFinishCallback; } + +Model::CopyObjectOutcome S3CrtClient::PopulateCopyObjectProperties(const Model::CopyObjectRequest &request, + const std::shared_ptr &httpRequest) const +{ + const bool copyMetadata = request.MetadataDirectiveHasBeenSet() && + request.GetMetadataDirective() == Model::MetadataDirective::COPY; + const bool copyTags = request.TaggingDirectiveHasBeenSet() && + request.GetTaggingDirective() == Model::TaggingDirective::COPY; + + if (!copyMetadata && !copyTags) + { + return Model::CopyObjectOutcome(Model::CopyObjectResult()); + } + + // Parse "[/]bucket/key[?versionId=...]" from x-amz-copy-source. rfind: keys may contain '?'. + Aws::String copySource = request.GetCopySource(); + Aws::String sourceVersionId; + const auto queryPos = copySource.rfind('?'); + if (queryPos != Aws::String::npos) + { + const Aws::String query = copySource.substr(queryPos + 1); + copySource = copySource.substr(0, queryPos); + const auto versionPos = query.find("versionId="); + if (versionPos != Aws::String::npos) + { + sourceVersionId = query.substr(versionPos + Aws::String("versionId=").size()); + const auto ampPos = sourceVersionId.find('&'); + if (ampPos != Aws::String::npos) + { + sourceVersionId = sourceVersionId.substr(0, ampPos); + } + } + } + if (!copySource.empty() && copySource.front() == '/') + { + copySource = copySource.substr(1); + } + const auto slashPos = copySource.find('/'); + if (slashPos == Aws::String::npos) + { + return Model::CopyObjectOutcome(Aws::Client::AWSError(S3CrtErrors::INVALID_PARAMETER_VALUE, + "INVALID_PARAMETER_VALUE", "Could not parse bucket and key from CopySource for property copy", false)); + } + const Aws::String sourceBucket = copySource.substr(0, slashPos); + const Aws::String sourceKey = copySource.substr(slashPos + 1); + + Model::HeadObjectRequest headRequest; + headRequest.SetBucket(sourceBucket); + headRequest.SetKey(sourceKey); + if (!sourceVersionId.empty()) + { + headRequest.SetVersionId(sourceVersionId); + } + auto headOutcome = HeadObject(headRequest); + if (!headOutcome.IsSuccess()) + { + return Model::CopyObjectOutcome(headOutcome.GetError()); + } + const auto& head = headOutcome.GetResult(); + const Aws::String sourceETag = head.GetETag(); + + // Pin the source version so UploadPartCopy reads a stable object. + if (sourceVersionId.empty() && !head.GetVersionId().empty()) + { + sourceVersionId = head.GetVersionId(); + httpRequest->SetHeaderValue("x-amz-copy-source", Aws::Http::URI::URLEncodePath(copySource) + "?versionId=" + sourceVersionId); + } + if (!httpRequest->HasHeader("x-amz-copy-source-if-match") && !sourceETag.empty()) + { + httpRequest->SetHeaderValue("x-amz-copy-source-if-match", sourceETag); + } + + // small objects are single-part copies where S3 honors the directives natively, skip injection + if (head.GetContentLength() < (1LL * 1024 * 1024 * 1024)) + { + return Model::CopyObjectOutcome(Model::CopyObjectResult()); + } + + if (copyMetadata) + { + if (!head.GetContentType().empty()) { httpRequest->SetHeaderValue("content-type", head.GetContentType()); } + if (!head.GetContentEncoding().empty()) { httpRequest->SetHeaderValue("content-encoding", head.GetContentEncoding()); } + if (!head.GetContentDisposition().empty()) { httpRequest->SetHeaderValue("content-disposition", head.GetContentDisposition()); } + if (!head.GetContentLanguage().empty()) { httpRequest->SetHeaderValue("content-language", head.GetContentLanguage()); } + if (!head.GetCacheControl().empty()) { httpRequest->SetHeaderValue("cache-control", head.GetCacheControl()); } + if (!head.GetExpiresString().empty()) { httpRequest->SetHeaderValue("expires", head.GetExpires().ToGmtString(Aws::Utils::DateFormat::RFC822)); } + for (const auto& item : head.GetMetadata()) + { + httpRequest->SetHeaderValue("x-amz-meta-" + item.first, item.second); + } + // REPLACE so CreateMultipartUpload emits these headers. + httpRequest->SetHeaderValue("x-amz-metadata-directive", "REPLACE"); + } + + if (copyTags) + { + Model::GetObjectTaggingRequest taggingRequest; + taggingRequest.SetBucket(sourceBucket); + taggingRequest.SetKey(sourceKey); + if (!sourceVersionId.empty()) + { + taggingRequest.SetVersionId(sourceVersionId); + } + auto taggingOutcome = GetObjectTagging(taggingRequest); + if (!taggingOutcome.IsSuccess()) + { + return Model::CopyObjectOutcome(taggingOutcome.GetError()); + } + Aws::StringStream tagStream; + bool firstTag = true; + for (const auto& tag : taggingOutcome.GetResult().GetTagSet()) + { + if (!firstTag) { tagStream << "&"; } + tagStream << Aws::Utils::StringUtils::URLEncode(tag.GetKey().c_str()) << "=" + << Aws::Utils::StringUtils::URLEncode(tag.GetValue().c_str()); + firstTag = false; + } + if (!firstTag) + { + httpRequest->SetHeaderValue("x-amz-tagging", tagStream.str()); + httpRequest->SetHeaderValue("x-amz-tagging-directive", "REPLACE"); + } + } + + return Model::CopyObjectOutcome(Model::CopyObjectResult()); +} #end #foreach($operation in $serviceModel.operations) @@ -454,6 +580,16 @@ void ${className}::${operation.name}Async(${constText}${operation.request.shape. } #else InitCommonCrtRequestOption(userData, &options, &request, uri, Aws::Http::HttpMethod::HTTP_${operation.http.method}); +#end +#if($operation.name == "CopyObject") + { + auto copyPropertiesOutcome = PopulateCopyObjectProperties(request, userData->request); + if (!copyPropertiesOutcome.IsSuccess()) + { + Aws::Delete(userData); + return handler(this, request, copyPropertiesOutcome, handlerContext); + } + } #end if(handlerContext) {