Skip to content

Commit 636833a

Browse files
committed
copy tags and metadata in s3crt copy object when directive is defined
1 parent d078cfd commit 636833a

7 files changed

Lines changed: 477 additions & 0 deletions

File tree

generated/src/aws-cpp-sdk-s3-crt/include/aws/s3-crt/S3CrtClient.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8533,6 +8533,9 @@ class AWS_S3CRT_API S3CrtClient : public Aws::Client::AWSXMLClient,
85338533
const Aws::AmazonWebServiceRequest* request, const Aws::Http::URI& uri,
85348534
Aws::Http::HttpMethod method) const;
85358535

8536+
Model::CopyObjectOutcome PopulateCopyObjectProperties(const Model::CopyObjectRequest& request,
8537+
const std::shared_ptr<Aws::Http::HttpRequest>& httpRequest) const;
8538+
85368539
typedef Aws::Utils::Outcome<Aws::AmazonWebServiceResult<RESPONSE>, S3CrtError> InvokeOperationOutcome;
85378540

85388541
InvokeOperationOutcome InvokeServiceOperation(const AmazonWebServiceRequest& request,

generated/src/aws-cpp-sdk-s3-crt/source/S3CrtClient.cpp

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,121 @@ void S3CrtClient::InitCommonCrtRequestOption(CrtRequestCallbackUserData* userDat
819819
options->finish_callback = S3CrtRequestFinishCallback;
820820
}
821821

822+
Model::CopyObjectOutcome S3CrtClient::PopulateCopyObjectProperties(const Model::CopyObjectRequest& request,
823+
const std::shared_ptr<Aws::Http::HttpRequest>& httpRequest) const {
824+
const bool copyMetadata = request.MetadataDirectiveHasBeenSet() && request.GetMetadataDirective() == Model::MetadataDirective::COPY;
825+
const bool copyTags = request.TaggingDirectiveHasBeenSet() && request.GetTaggingDirective() == Model::TaggingDirective::COPY;
826+
827+
if (!copyMetadata && !copyTags) {
828+
return Model::CopyObjectOutcome(Model::CopyObjectResult());
829+
}
830+
831+
// Parse "[/]bucket/key[?versionId=...]" from x-amz-copy-source. rfind: keys may contain '?'.
832+
Aws::String copySource = request.GetCopySource();
833+
Aws::String sourceVersionId;
834+
const auto queryPos = copySource.rfind('?');
835+
if (queryPos != Aws::String::npos) {
836+
const Aws::String query = copySource.substr(queryPos + 1);
837+
copySource = copySource.substr(0, queryPos);
838+
const auto versionPos = query.find("versionId=");
839+
if (versionPos != Aws::String::npos) {
840+
sourceVersionId = query.substr(versionPos + Aws::String("versionId=").size());
841+
const auto ampPos = sourceVersionId.find('&');
842+
if (ampPos != Aws::String::npos) {
843+
sourceVersionId = sourceVersionId.substr(0, ampPos);
844+
}
845+
}
846+
}
847+
if (!copySource.empty() && copySource.front() == '/') {
848+
copySource = copySource.substr(1);
849+
}
850+
const auto slashPos = copySource.find('/');
851+
if (slashPos == Aws::String::npos) {
852+
return Model::CopyObjectOutcome(Aws::Client::AWSError<S3CrtErrors>(S3CrtErrors::INVALID_PARAMETER_VALUE, "INVALID_PARAMETER_VALUE",
853+
"Could not parse bucket and key from CopySource for property copy",
854+
false));
855+
}
856+
const Aws::String sourceBucket = copySource.substr(0, slashPos);
857+
const Aws::String sourceKey = copySource.substr(slashPos + 1);
858+
859+
Model::HeadObjectRequest headRequest;
860+
headRequest.SetBucket(sourceBucket);
861+
headRequest.SetKey(sourceKey);
862+
if (!sourceVersionId.empty()) {
863+
headRequest.SetVersionId(sourceVersionId);
864+
}
865+
auto headOutcome = HeadObject(headRequest);
866+
if (!headOutcome.IsSuccess()) {
867+
return Model::CopyObjectOutcome(headOutcome.GetError());
868+
}
869+
const auto& head = headOutcome.GetResult();
870+
const Aws::String sourceETag = head.GetETag();
871+
872+
// Pin the source version so UploadPartCopy reads a stable object.
873+
if (sourceVersionId.empty() && !head.GetVersionId().empty()) {
874+
sourceVersionId = head.GetVersionId();
875+
httpRequest->SetHeaderValue("x-amz-copy-source", Aws::Http::URI::URLEncodePath(copySource) + "?versionId=" + sourceVersionId);
876+
}
877+
if (!httpRequest->HasHeader("x-amz-copy-source-if-match") && !sourceETag.empty()) {
878+
httpRequest->SetHeaderValue("x-amz-copy-source-if-match", sourceETag);
879+
}
880+
881+
if (copyMetadata) {
882+
if (!head.GetContentType().empty()) {
883+
httpRequest->SetHeaderValue("content-type", head.GetContentType());
884+
}
885+
if (!head.GetContentEncoding().empty()) {
886+
httpRequest->SetHeaderValue("content-encoding", head.GetContentEncoding());
887+
}
888+
if (!head.GetContentDisposition().empty()) {
889+
httpRequest->SetHeaderValue("content-disposition", head.GetContentDisposition());
890+
}
891+
if (!head.GetContentLanguage().empty()) {
892+
httpRequest->SetHeaderValue("content-language", head.GetContentLanguage());
893+
}
894+
if (!head.GetCacheControl().empty()) {
895+
httpRequest->SetHeaderValue("cache-control", head.GetCacheControl());
896+
}
897+
if (!head.GetExpiresString().empty()) {
898+
httpRequest->SetHeaderValue("expires", head.GetExpires().ToGmtString(Aws::Utils::DateFormat::RFC822));
899+
}
900+
for (const auto& item : head.GetMetadata()) {
901+
httpRequest->SetHeaderValue("x-amz-meta-" + item.first, item.second);
902+
}
903+
// REPLACE so CreateMultipartUpload emits these headers.
904+
httpRequest->SetHeaderValue("x-amz-metadata-directive", "REPLACE");
905+
}
906+
907+
if (copyTags) {
908+
Model::GetObjectTaggingRequest taggingRequest;
909+
taggingRequest.SetBucket(sourceBucket);
910+
taggingRequest.SetKey(sourceKey);
911+
if (!sourceVersionId.empty()) {
912+
taggingRequest.SetVersionId(sourceVersionId);
913+
}
914+
auto taggingOutcome = GetObjectTagging(taggingRequest);
915+
if (!taggingOutcome.IsSuccess()) {
916+
return Model::CopyObjectOutcome(taggingOutcome.GetError());
917+
}
918+
Aws::StringStream tagStream;
919+
bool firstTag = true;
920+
for (const auto& tag : taggingOutcome.GetResult().GetTagSet()) {
921+
if (!firstTag) {
922+
tagStream << "&";
923+
}
924+
tagStream << Aws::Utils::StringUtils::URLEncode(tag.GetKey().c_str()) << "="
925+
<< Aws::Utils::StringUtils::URLEncode(tag.GetValue().c_str());
926+
firstTag = false;
927+
}
928+
if (!firstTag) {
929+
httpRequest->SetHeaderValue("x-amz-tagging", tagStream.str());
930+
httpRequest->SetHeaderValue("x-amz-tagging-directive", "REPLACE");
931+
}
932+
}
933+
934+
return Model::CopyObjectOutcome(Model::CopyObjectResult());
935+
}
936+
822937
static void CopyObjectRequestShutdownCallback(void* user_data) {
823938
if (!user_data) {
824939
AWS_LOGSTREAM_ERROR("CopyObject", "user data passed is NULL ");
@@ -915,6 +1030,12 @@ void S3CrtClient::CopyObjectAsync(const CopyObjectRequest& request, const CopyOb
9151030
"Unable to create s3 meta request", false)),
9161031
handlerContext);
9171032
}
1033+
{
1034+
auto copyPropertiesOutcome = PopulateCopyObjectProperties(request, userData->request);
1035+
if (!copyPropertiesOutcome.IsSuccess()) {
1036+
return handler(this, request, copyPropertiesOutcome, handlerContext);
1037+
}
1038+
}
9181039
if (handlerContext) {
9191040
handlerContext->GetMonitorContext().StartMonitorContext(Aws::String{"S3CrtClient"}, request.GetServiceRequestName(), userData->request);
9201041
}

tests/aws-cpp-sdk-s3-crt-integration-tests/BucketAndObjectOperationTest.cpp

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@
4343
#include <aws/s3-crt/model/SelectObjectContentRequest.h>
4444
#include <aws/s3-crt/model/Tagging.h>
4545
#include <aws/s3-crt/model/PutBucketTaggingRequest.h>
46+
#include <aws/s3-crt/model/GetObjectTaggingRequest.h>
47+
#include <aws/s3-crt/model/PutObjectTaggingRequest.h>
48+
#include <aws/s3-crt/model/MetadataDirective.h>
49+
#include <aws/s3-crt/model/TaggingDirective.h>
4650
#include <aws/s3-crt/ClientConfiguration.h>
4751
#include <aws/testing/ProxyConfig.h>
4852
#include <aws/testing/platform/PlatformTesting.h>
@@ -988,6 +992,91 @@ namespace
988992
AWS_ASSERT_SUCCESS(copyOutcome);
989993
}
990994

995+
TEST_F(BucketAndObjectOperationTest, TestCopyObjectCopiesMetadataAndTags)
996+
{
997+
Aws::String fullBucketName = CalculateBucketName(BASE_OBJECTS_BUCKET_NAME.c_str());
998+
SCOPED_TRACE(Aws::String("FullBucketName ") + fullBucketName);
999+
CreateBucketRequest createBucketRequest;
1000+
createBucketRequest.SetBucket(fullBucketName);
1001+
createBucketRequest.SetACL(BucketCannedACL::private_);
1002+
CreateBucketOutcome createBucketOutcome = Client->CreateBucket(createBucketRequest);
1003+
AWS_ASSERT_SUCCESS(createBucketOutcome);
1004+
ASSERT_TRUE(WaitForBucketToPropagate(fullBucketName));
1005+
TagTestBucket(fullBucketName, Client);
1006+
1007+
const char* sourceKey = "copy-props-source";
1008+
const char* destKey = "copy-props-destination";
1009+
1010+
auto objectStream = Aws::MakeShared<Aws::StringStream>(ALLOCATION_TAG);
1011+
*objectStream << "high level copy metadata and tags test payload";
1012+
objectStream->flush();
1013+
PutObjectRequest putObjectRequest;
1014+
putObjectRequest.SetBucket(fullBucketName);
1015+
putObjectRequest.SetKey(sourceKey);
1016+
putObjectRequest.SetBody(objectStream);
1017+
putObjectRequest.SetContentLength(static_cast<long>(putObjectRequest.GetBody()->tellp()));
1018+
putObjectRequest.SetContentType("application/x-high-level-copy");
1019+
putObjectRequest.AddMetadata("project", "highlevelcopy");
1020+
putObjectRequest.AddMetadata("owner", "sdk-team");
1021+
PutObjectOutcome putObjectOutcome = Client->PutObject(putObjectRequest);
1022+
AWS_ASSERT_SUCCESS(putObjectOutcome);
1023+
ASSERT_TRUE(WaitForObjectToPropagate(fullBucketName, sourceKey));
1024+
1025+
{
1026+
PutObjectTaggingRequest putTaggingRequest;
1027+
putTaggingRequest.SetBucket(fullBucketName);
1028+
putTaggingRequest.SetKey(sourceKey);
1029+
Tagging tagging;
1030+
Tag t1; t1.SetKey("env"); t1.SetValue("test");
1031+
Tag t2; t2.SetKey("team"); t2.SetValue("sdk");
1032+
tagging.AddTagSet(t1);
1033+
tagging.AddTagSet(t2);
1034+
putTaggingRequest.SetTagging(tagging);
1035+
auto putTaggingOutcome = Client->PutObjectTagging(putTaggingRequest);
1036+
AWS_ASSERT_SUCCESS(putTaggingOutcome);
1037+
}
1038+
1039+
CopyObjectRequest copyRequest;
1040+
copyRequest.WithBucket(fullBucketName)
1041+
.WithKey(destKey)
1042+
.WithCopySource(fullBucketName + "/" + sourceKey)
1043+
.WithMetadataDirective(MetadataDirective::COPY)
1044+
.WithTaggingDirective(TaggingDirective::COPY);
1045+
auto copyOutcome = Client->CopyObject(copyRequest);
1046+
AWS_ASSERT_SUCCESS(copyOutcome);
1047+
ASSERT_TRUE(WaitForObjectToPropagate(fullBucketName, destKey));
1048+
1049+
{
1050+
HeadObjectRequest headRequest;
1051+
headRequest.SetBucket(fullBucketName);
1052+
headRequest.SetKey(destKey);
1053+
auto headOutcome = Client->HeadObject(headRequest);
1054+
AWS_ASSERT_SUCCESS(headOutcome);
1055+
const auto& metadata = headOutcome.GetResult().GetMetadata();
1056+
ASSERT_EQ(1u, metadata.count("project"));
1057+
ASSERT_STREQ("highlevelcopy", metadata.at("project").c_str());
1058+
ASSERT_EQ(1u, metadata.count("owner"));
1059+
ASSERT_STREQ("sdk-team", metadata.at("owner").c_str());
1060+
ASSERT_STREQ("application/x-high-level-copy", headOutcome.GetResult().GetContentType().c_str());
1061+
}
1062+
1063+
{
1064+
GetObjectTaggingRequest getTaggingRequest;
1065+
getTaggingRequest.SetBucket(fullBucketName);
1066+
getTaggingRequest.SetKey(destKey);
1067+
auto getTaggingOutcome = Client->GetObjectTagging(getTaggingRequest);
1068+
AWS_ASSERT_SUCCESS(getTaggingOutcome);
1069+
Aws::Map<Aws::String, Aws::String> tags;
1070+
for (const auto& tag : getTaggingOutcome.GetResult().GetTagSet())
1071+
{
1072+
tags[tag.GetKey()] = tag.GetValue();
1073+
}
1074+
ASSERT_EQ(2u, tags.size());
1075+
ASSERT_STREQ("test", tags["env"].c_str());
1076+
ASSERT_STREQ("sdk", tags["team"].c_str());
1077+
}
1078+
}
1079+
9911080
TEST_F(BucketAndObjectOperationTest, TestObjectOperationWithEventStream)
9921081
{
9931082
GTEST_SKIP() << "Select objects is not supported on new AWS accounts";

tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/s3/S3ClientHeader.vm

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,9 @@ namespace ${rootNamespace}
254254
aws_s3_meta_request_options *options,
255255
const Aws::AmazonWebServiceRequest *request,
256256
const Aws::Http::URI &uri, Aws::Http::HttpMethod method) const;
257+
258+
Model::CopyObjectOutcome PopulateCopyObjectProperties(const Model::CopyObjectRequest &request,
259+
const std::shared_ptr<Aws::Http::HttpRequest> &httpRequest) const;
257260
#else
258261
#if(!$serviceModel.endpointRules)
259262
void init(const Aws::Client::ClientConfiguration& clientConfiguration);

tools/code-generation/generator/src/main/resources/com/amazonaws/util/awsclientgenerator/velocity/cpp/s3/SmithyS3ClientHeader.vm

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,9 @@ namespace ${rootNamespace}
266266
aws_s3_meta_request_options *options,
267267
const Aws::AmazonWebServiceRequest *request,
268268
const Aws::Http::URI &uri, Aws::Http::HttpMethod method) const;
269+
270+
Model::CopyObjectOutcome PopulateCopyObjectProperties(const Model::CopyObjectRequest &request,
271+
const std::shared_ptr<Aws::Http::HttpRequest> &httpRequest) const;
269272
#else
270273
#if(!$serviceModel.endpointRules)
271274
void init(const Aws::Client::ClientConfiguration& clientConfiguration);

0 commit comments

Comments
 (0)