diff --git a/ci/cloudbuild/builds/lib/integration.sh b/ci/cloudbuild/builds/lib/integration.sh index 73da705584dff..146222fb31382 100644 --- a/ci/cloudbuild/builds/lib/integration.sh +++ b/ci/cloudbuild/builds/lib/integration.sh @@ -31,7 +31,7 @@ source module ci/lib/io.sh export PATH="${HOME}/.local/bin:${PATH}" python3 -m pip uninstall -y --quiet googleapis-storage-testbench python3 -m pip install --upgrade --user --quiet --disable-pip-version-check \ - "git+https://github.com/googleapis/storage-testbench@v0.46.0" + "git+https://github.com/googleapis/storage-testbench@v0.52.0" # Some of the tests will need a valid roots.pem file. rm -f /dev/shm/roots.pem diff --git a/ci/lib/run_gcs_httpbin_emulator_utils.sh b/ci/lib/run_gcs_httpbin_emulator_utils.sh index e456c968e2071..ba4aea8b720d9 100644 --- a/ci/lib/run_gcs_httpbin_emulator_utils.sh +++ b/ci/lib/run_gcs_httpbin_emulator_utils.sh @@ -127,4 +127,8 @@ create_testbench_resources() { curl -s -o /dev/null -X POST --data-binary @- \ -H "Content-Type: application/json" \ "${CLOUD_STORAGE_EMULATOR_ENDPOINT}/storage/v1/b?project=${GOOGLE_CLOUD_PROJECT}" + printf '{"name": "%s"}' "${GOOGLE_CLOUD_CPP_STORAGE_TEST_FOLDER_BUCKET_NAME}" | + curl -s -o /dev/null -X POST --data-binary @- \ + -H "Content-Type: application/json" \ + "${CLOUD_STORAGE_EMULATOR_ENDPOINT}/storage/v1/b?project=${GOOGLE_CLOUD_PROJECT}" } diff --git a/google/cloud/storage/client.h b/google/cloud/storage/client.h index 16bf678101c03..d0b01d03da5b1 100644 --- a/google/cloud/storage/client.h +++ b/google/cloud/storage/client.h @@ -1372,6 +1372,39 @@ class Client { return connection_->UpdateObject(request); } + /** + * Moves an existing object to a new or existing object within a HNS enabled + * bucket. + * + * @param bucket_name the name of the bucket in which to move the object. The + * bucket must be HNS enabled. + * @param source_object_name the name of the source object to move. + * @param destination_object_name the destination name of the object after the + * move is completed. + * @param options a list of optional query parameters and/or request headers. + * Valid types for this operation include + * `IfSourceGenerationMatch`, `IfSourceGenerationNotMatch`, + * `IfSourceMetagenerationMatch`, `IfSourceMetagenerationNotMatch`, + * `IfGenerationMatch`, `IfGenerationNotMatch`, `IfMetagenerationMatch` + * `IfMetagenerationNotMatch`, `projection`. + * + * @par Idempotency + * This operation is only idempotent if restricted by pre-conditions. + */ + template + StatusOr MoveObject(std::string bucket_name, + std::string source_object_name, + std::string destination_object_name, + Options&&... options) { + google::cloud::internal::OptionsSpan const span( + SpanOptions(std::forward(options)...)); + internal::MoveObjectRequest request(std::move(bucket_name), + std::move(source_object_name), + std::move(destination_object_name)); + request.set_multiple_options(std::forward(options)...); + return connection_->MoveObject(request); + } + /** * Patches the metadata in a Google Cloud Storage Object. * diff --git a/google/cloud/storage/idempotency_policy.cc b/google/cloud/storage/idempotency_policy.cc index 58a16514cce53..fde09fc0ad6ac 100644 --- a/google/cloud/storage/idempotency_policy.cc +++ b/google/cloud/storage/idempotency_policy.cc @@ -92,6 +92,10 @@ bool AlwaysRetryIdempotencyPolicy::IsIdempotent( internal::UpdateObjectRequest const&) const { return true; } +bool AlwaysRetryIdempotencyPolicy::IsIdempotent( + internal::MoveObjectRequest const&) const { + return true; +} bool AlwaysRetryIdempotencyPolicy::IsIdempotent( internal::PatchObjectRequest const&) const { return true; @@ -341,6 +345,11 @@ bool StrictIdempotencyPolicy::IsIdempotent( request.HasOption(); } +bool StrictIdempotencyPolicy::IsIdempotent( + internal::MoveObjectRequest const& request) const { + return request.HasOption(); +} + bool StrictIdempotencyPolicy::IsIdempotent( internal::PatchObjectRequest const& request) const { return request.HasOption() || diff --git a/google/cloud/storage/idempotency_policy.h b/google/cloud/storage/idempotency_policy.h index cd7fefa0dddec..86108050f987a 100644 --- a/google/cloud/storage/idempotency_policy.h +++ b/google/cloud/storage/idempotency_policy.h @@ -102,6 +102,9 @@ class IdempotencyPolicy { internal::DeleteObjectRequest const& request) const = 0; virtual bool IsIdempotent( internal::UpdateObjectRequest const& request) const = 0; + virtual bool IsIdempotent(internal::MoveObjectRequest const&) const { + return false; + }; virtual bool IsIdempotent( internal::PatchObjectRequest const& request) const = 0; virtual bool IsIdempotent( @@ -238,6 +241,7 @@ class AlwaysRetryIdempotencyPolicy : public IdempotencyPolicy { internal::DeleteObjectRequest const& request) const override; bool IsIdempotent( internal::UpdateObjectRequest const& request) const override; + bool IsIdempotent(internal::MoveObjectRequest const& request) const override; bool IsIdempotent(internal::PatchObjectRequest const& request) const override; bool IsIdempotent( internal::ComposeObjectRequest const& request) const override; @@ -370,6 +374,7 @@ class StrictIdempotencyPolicy : public IdempotencyPolicy { internal::DeleteObjectRequest const& request) const override; bool IsIdempotent( internal::UpdateObjectRequest const& request) const override; + bool IsIdempotent(internal::MoveObjectRequest const& request) const override; bool IsIdempotent(internal::PatchObjectRequest const& request) const override; bool IsIdempotent( internal::ComposeObjectRequest const& request) const override; diff --git a/google/cloud/storage/idempotency_policy_test.cc b/google/cloud/storage/idempotency_policy_test.cc index 4064c1823a4aa..ce613c2eb818c 100644 --- a/google/cloud/storage/idempotency_policy_test.cc +++ b/google/cloud/storage/idempotency_policy_test.cc @@ -231,6 +231,21 @@ TEST(StrictIdempotencyPolicyTest, UpdateObjectIfMetagenerationMatch) { EXPECT_TRUE(policy.IsIdempotent(request)); } +TEST(StrictIdempotencyPolicyTest, MoveObject) { + StrictIdempotencyPolicy policy; + internal::MoveObjectRequest request( + "test-bucket-name", "test-src-object-name", "test-dst-object-name"); + EXPECT_FALSE(policy.IsIdempotent(request)); +} + +TEST(StrictIdempotencyPolicyTest, MoveObjectIfGenerationMatch) { + StrictIdempotencyPolicy policy; + internal::MoveObjectRequest request( + "test-bucket-name", "test-src-object-name", "test-dst-object-name"); + request.set_option(IfGenerationMatch(7)); + EXPECT_TRUE(policy.IsIdempotent(request)); +} + TEST(StrictIdempotencyPolicyTest, PatchObject) { StrictIdempotencyPolicy policy; internal::PatchObjectRequest request("test-bucket-name", "test-object-name", diff --git a/google/cloud/storage/internal/connection_impl.cc b/google/cloud/storage/internal/connection_impl.cc index 9f375570af33e..8be3ac7c8d17b 100644 --- a/google/cloud/storage/internal/connection_impl.cc +++ b/google/cloud/storage/internal/connection_impl.cc @@ -451,6 +451,22 @@ StatusOr StorageConnectionImpl::UpdateObject( google::cloud::internal::CurrentOptions(), request, __func__); } +StatusOr StorageConnectionImpl::MoveObject( + MoveObjectRequest const& request) { + auto const idempotency = current_idempotency_policy().IsIdempotent(request) + ? Idempotency::kIdempotent + : Idempotency::kNonIdempotent; + return RestRetryLoop( + current_retry_policy(), current_backoff_policy(), idempotency, + [token = MakeIdempotencyToken(), this]( + rest_internal::RestContext& context, Options const& options, + auto const& request) { + context.AddHeader(kIdempotencyTokenHeader, token); + return stub_->MoveObject(context, options, request); + }, + google::cloud::internal::CurrentOptions(), request, __func__); +} + StatusOr StorageConnectionImpl::PatchObject( PatchObjectRequest const& request) { auto const idempotency = current_idempotency_policy().IsIdempotent(request) diff --git a/google/cloud/storage/internal/connection_impl.h b/google/cloud/storage/internal/connection_impl.h index 44424f32aa386..ab510c5db8ed2 100644 --- a/google/cloud/storage/internal/connection_impl.h +++ b/google/cloud/storage/internal/connection_impl.h @@ -80,6 +80,8 @@ class StorageConnectionImpl StatusOr DeleteObject(DeleteObjectRequest const&) override; StatusOr UpdateObject( UpdateObjectRequest const& request) override; + StatusOr MoveObject( + MoveObjectRequest const& request) override; StatusOr PatchObject( PatchObjectRequest const& request) override; StatusOr ComposeObject( diff --git a/google/cloud/storage/internal/connection_impl_object_copy_test.cc b/google/cloud/storage/internal/connection_impl_object_copy_test.cc index 3f3ef67ed799f..fd570a058f4f3 100644 --- a/google/cloud/storage/internal/connection_impl_object_copy_test.cc +++ b/google/cloud/storage/internal/connection_impl_object_copy_test.cc @@ -122,6 +122,34 @@ TEST(StorageConnectionImpl, RewriteObjectPermanentFailure) { EXPECT_THAT(permanent.captured_authority_options(), RetryLoopUsesOptions()); } +TEST(StorageConnectionImpl, MoveObjectTooManyFailures) { + auto transient = MockRetryClientFunction(TransientError()); + auto mock = std::make_unique(); + EXPECT_CALL(*mock, options); + EXPECT_CALL(*mock, MoveObject).Times(3).WillRepeatedly(transient); + auto client = + StorageConnectionImpl::Create(std::move(mock), RetryTestOptions()); + google::cloud::internal::OptionsSpan span(client->options()); + auto response = client->MoveObject(MoveObjectRequest()).status(); + EXPECT_THAT(response, StoppedOnTooManyTransients("MoveObject")); + EXPECT_THAT(transient.captured_tokens(), RetryLoopUsesSingleToken()); + EXPECT_THAT(transient.captured_authority_options(), RetryLoopUsesOptions()); +} + +TEST(StorageConnectionImpl, MoveObjectPermanentFailure) { + auto permanent = MockRetryClientFunction(PermanentError()); + auto mock = std::make_unique(); + EXPECT_CALL(*mock, options); + EXPECT_CALL(*mock, MoveObject).WillOnce(permanent); + auto client = + StorageConnectionImpl::Create(std::move(mock), RetryTestOptions()); + google::cloud::internal::OptionsSpan span(client->options()); + auto response = client->MoveObject(MoveObjectRequest()).status(); + EXPECT_THAT(response, StoppedOnPermanentError("MoveObject")); + EXPECT_THAT(permanent.captured_tokens(), RetryLoopUsesSingleToken()); + EXPECT_THAT(permanent.captured_authority_options(), RetryLoopUsesOptions()); +} + } // namespace } // namespace internal GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END diff --git a/google/cloud/storage/internal/generic_stub.h b/google/cloud/storage/internal/generic_stub.h index d4fe29af21d88..ddf35b377488a 100644 --- a/google/cloud/storage/internal/generic_stub.h +++ b/google/cloud/storage/internal/generic_stub.h @@ -121,6 +121,9 @@ class GenericStub { virtual StatusOr UpdateObject( rest_internal::RestContext&, Options const&, storage::internal::UpdateObjectRequest const&) = 0; + virtual StatusOr MoveObject( + rest_internal::RestContext&, Options const&, + storage::internal::MoveObjectRequest const&) = 0; virtual StatusOr PatchObject( rest_internal::RestContext&, Options const&, storage::internal::PatchObjectRequest const&) = 0; diff --git a/google/cloud/storage/internal/generic_stub_adapter.cc b/google/cloud/storage/internal/generic_stub_adapter.cc index 8bf67c9794dde..afbe99bd1c715 100644 --- a/google/cloud/storage/internal/generic_stub_adapter.cc +++ b/google/cloud/storage/internal/generic_stub_adapter.cc @@ -126,6 +126,11 @@ class GenericStubAdapter : public GenericStub { storage::internal::UpdateObjectRequest const& request) override { return impl_->UpdateObject(request); } + StatusOr MoveObject( + rest_internal::RestContext&, Options const&, + storage::internal::MoveObjectRequest const& request) override { + return impl_->MoveObject(request); + } StatusOr PatchObject( rest_internal::RestContext&, Options const&, storage::internal::PatchObjectRequest const& request) override { diff --git a/google/cloud/storage/internal/grpc/object_request_parser.cc b/google/cloud/storage/internal/grpc/object_request_parser.cc index b9215ff5013fb..1ff0bc6ed1279 100644 --- a/google/cloud/storage/internal/grpc/object_request_parser.cc +++ b/google/cloud/storage/internal/grpc/object_request_parser.cc @@ -666,10 +666,39 @@ StatusOr ToProto( return result; } +StatusOr ToProto( + storage::internal::MoveObjectRequest const& request) { + google::storage::v2::MoveObjectRequest result; + SetGenerationConditions(result, request); + SetMetagenerationConditions(result, request); + if (request.HasOption()) { + result.set_if_source_generation_match( + request.GetOption().value()); + } + if (request.HasOption()) { + result.set_if_source_generation_not_match( + request.GetOption().value()); + } + if (request.HasOption()) { + result.set_if_source_metageneration_match( + request.GetOption().value()); + } + if (request.HasOption()) { + result.set_if_source_metageneration_not_match( + request.GetOption().value()); + } + result.set_bucket(GrpcBucketIdToName(request.bucket_name())); + result.set_source_object(request.source_object_name()); + result.set_destination_object(request.destination_object_name()); + + return result; +} + StatusOr ToProto( storage::internal::RestoreObjectRequest const& request) { google::storage::v2::RestoreObjectRequest result; auto status = SetCommonObjectParameters(result, request); + if (!status.ok()) return status; result.set_bucket(GrpcBucketIdToName(request.bucket_name())); result.set_object(request.object_name()); diff --git a/google/cloud/storage/internal/grpc/object_request_parser.h b/google/cloud/storage/internal/grpc/object_request_parser.h index dea75432dbc3c..6e90bcb942e45 100644 --- a/google/cloud/storage/internal/grpc/object_request_parser.h +++ b/google/cloud/storage/internal/grpc/object_request_parser.h @@ -67,6 +67,9 @@ StatusOr ToProto( StatusOr ToProto( storage::internal::RestoreObjectRequest const& request); +StatusOr ToProto( + storage::internal::MoveObjectRequest const& request); + StatusOr ToProto( storage::internal::ResumableUploadRequest const& request); diff --git a/google/cloud/storage/internal/grpc/object_request_parser_test.cc b/google/cloud/storage/internal/grpc/object_request_parser_test.cc index ef3bff279a9f6..8ee4db454a5e1 100644 --- a/google/cloud/storage/internal/grpc/object_request_parser_test.cc +++ b/google/cloud/storage/internal/grpc/object_request_parser_test.cc @@ -406,6 +406,51 @@ TEST(GrpcObjectRequestParser, ReadOffsetEmptyRange) { EXPECT_THAT(actual, StatusIs(StatusCode::kInvalidArgument)); } +TEST(GrpcObjectRequestParser, MoveObjectSimpleRequest) { + auto constexpr kTextProto = R"pb( + bucket: "projects/_/buckets/test-bucket" + source_object: "source-object" + destination_object: "destination-object" + )pb"; + google::storage::v2::MoveObjectRequest expected; + ASSERT_TRUE(TextFormat::ParseFromString(kTextProto, &expected)); + storage::internal::MoveObjectRequest req("test-bucket", "source-object", + "destination-object"); + auto actual = ToProto(req); + ASSERT_STATUS_OK(actual); + EXPECT_THAT(*actual, IsProtoEqual(expected)); +} + +TEST(GrpcObjectRequestParser, MoveObjectRequestAllOptions) { + auto constexpr kTextProto = R"pb( + bucket: "projects/_/buckets/test-bucket" + source_object: "source-object" + destination_object: "destination-object" + if_source_generation_match: 1 + if_source_generation_not_match: 2 + if_source_metageneration_match: 3 + if_source_metageneration_not_match: 4 + if_generation_match: 5 + if_generation_not_match: 6 + if_metageneration_match: 7 + if_metageneration_not_match: 8 + )pb"; + google::storage::v2::MoveObjectRequest expected; + ASSERT_TRUE(TextFormat::ParseFromString(kTextProto, &expected)); + storage::internal::MoveObjectRequest req("test-bucket", "source-object", + "destination-object"); + req.set_multiple_options( + storage::IfSourceGenerationMatch(1), + storage::IfSourceGenerationNotMatch(2), + storage::IfSourceMetagenerationMatch(3), + storage::IfSourceMetagenerationNotMatch(4), storage::IfGenerationMatch(5), + storage::IfGenerationNotMatch(6), storage::IfMetagenerationMatch(7), + storage::IfMetagenerationNotMatch(8)); + auto actual = ToProto(req); + ASSERT_STATUS_OK(actual); + EXPECT_THAT(*actual, IsProtoEqual(expected)); +} + TEST(GrpcObjectRequestParser, PatchObjectRequestAllOptions) { auto constexpr kTextProto = R"pb( predefined_acl: "projectPrivate" diff --git a/google/cloud/storage/internal/grpc/stub.cc b/google/cloud/storage/internal/grpc/stub.cc index 1c73127954366..9a7120c6ebae8 100644 --- a/google/cloud/storage/internal/grpc/stub.cc +++ b/google/cloud/storage/internal/grpc/stub.cc @@ -483,6 +483,19 @@ StatusOr GrpcStub::UpdateObject( return FromProto(*response, options); } +StatusOr GrpcStub::MoveObject( + rest_internal::RestContext& context, Options const& options, + storage::internal::MoveObjectRequest const& request) { + auto proto = ToProto(request); + if (!proto) return std::move(proto).status(); + grpc::ClientContext ctx; + ApplyQueryParameters(ctx, options, request); + AddIdempotencyToken(ctx, context); + auto response = stub_->MoveObject(ctx, options, *proto); + if (!response) return std::move(response).status(); + return FromProto(*response, options); +} + StatusOr GrpcStub::PatchObject( rest_internal::RestContext& context, Options const& options, storage::internal::PatchObjectRequest const& request) { diff --git a/google/cloud/storage/internal/grpc/stub.h b/google/cloud/storage/internal/grpc/stub.h index 8598e337ea3e1..d5cfe4db066e4 100644 --- a/google/cloud/storage/internal/grpc/stub.h +++ b/google/cloud/storage/internal/grpc/stub.h @@ -107,6 +107,9 @@ class GrpcStub : public GenericStub { StatusOr UpdateObject( rest_internal::RestContext& context, Options const& options, storage::internal::UpdateObjectRequest const& request) override; + StatusOr MoveObject( + rest_internal::RestContext& context, Options const& options, + storage::internal::MoveObjectRequest const& request) override; StatusOr PatchObject( rest_internal::RestContext& context, Options const& options, storage::internal::PatchObjectRequest const& request) override; diff --git a/google/cloud/storage/internal/grpc/stub_test.cc b/google/cloud/storage/internal/grpc/stub_test.cc index aa8cd46b62cf9..c71bab3f183ea 100644 --- a/google/cloud/storage/internal/grpc/stub_test.cc +++ b/google/cloud/storage/internal/grpc/stub_test.cc @@ -658,6 +658,28 @@ TEST_F(GrpcClientTest, UpdateObject) { EXPECT_EQ(response.status(), PermanentError()); } +TEST_F(GrpcClientTest, MoveObject) { + auto mock = std::make_shared(); + EXPECT_CALL(*mock, MoveObject) + .WillOnce([this](grpc::ClientContext& context, Options const&, + v2::MoveObjectRequest const& request) { + auto metadata = GetMetadata(context); + EXPECT_THAT(metadata, UnorderedElementsAre(Pair(kIdempotencyTokenHeader, + "test-token-1234"))); + EXPECT_THAT(request.bucket(), "projects/_/buckets/test-bucket"); + EXPECT_THAT(request.source_object(), "test-source-object"); + EXPECT_THAT(request.destination_object(), "test-destination-object"); + return PermanentError(); + }); + auto client = CreateTestClient(mock); + auto context = TestContext(); + auto response = client->MoveObject( + context, TestOptions(), + storage::internal::MoveObjectRequest("test-bucket", "test-source-object", + "test-destination-object")); + EXPECT_EQ(response.status(), PermanentError()); +} + TEST_F(GrpcClientTest, PatchObject) { auto mock = std::make_shared(); EXPECT_CALL(*mock, UpdateObject) diff --git a/google/cloud/storage/internal/logging_stub.cc b/google/cloud/storage/internal/logging_stub.cc index 52b92f42e8757..4c89e41fd5bbf 100644 --- a/google/cloud/storage/internal/logging_stub.cc +++ b/google/cloud/storage/internal/logging_stub.cc @@ -224,6 +224,16 @@ StatusOr LoggingStub::UpdateObject( context, options, request, __func__); } +StatusOr LoggingStub::MoveObject( + rest_internal::RestContext& context, Options const& options, + MoveObjectRequest const& request) { + return LogWrapper( + [this](auto& context, auto const& options, auto& request) { + return stub_->MoveObject(context, options, request); + }, + context, options, request, __func__); +} + StatusOr LoggingStub::PatchObject( rest_internal::RestContext& context, Options const& options, PatchObjectRequest const& request) { diff --git a/google/cloud/storage/internal/logging_stub.h b/google/cloud/storage/internal/logging_stub.h index 697c783762284..61106793ac2cd 100644 --- a/google/cloud/storage/internal/logging_stub.h +++ b/google/cloud/storage/internal/logging_stub.h @@ -88,6 +88,9 @@ class LoggingStub : public storage_internal::GenericStub { StatusOr UpdateObject( rest_internal::RestContext&, Options const&, UpdateObjectRequest const& request) override; + StatusOr MoveObject( + rest_internal::RestContext&, Options const&, + MoveObjectRequest const& request) override; StatusOr PatchObject( rest_internal::RestContext&, Options const&, PatchObjectRequest const& request) override; diff --git a/google/cloud/storage/internal/object_requests.cc b/google/cloud/storage/internal/object_requests.cc index bf86694a37979..1a61dc38d19af 100644 --- a/google/cloud/storage/internal/object_requests.cc +++ b/google/cloud/storage/internal/object_requests.cc @@ -382,6 +382,14 @@ std::ostream& operator<<(std::ostream& os, RewriteObjectRequest const& r) { return os << "}"; } +std::ostream& operator<<(std::ostream& os, MoveObjectRequest const& r) { + os << "MoveObjectRequest={bucket_name=" << r.bucket_name() + << ", source_object_name=" << r.source_object_name() + << ", destination_object_name=" << r.destination_object_name(); + r.DumpOptions(os, ", "); + return os << "}"; +} + std::ostream& operator<<(std::ostream& os, RestoreObjectRequest const& r) { os << "RestoreObjectRequest={bucket_name=" << r.bucket_name() << ", object_name=" << r.object_name() diff --git a/google/cloud/storage/internal/object_requests.h b/google/cloud/storage/internal/object_requests.h index 3702b0b044830..4e18ef4bc190d 100644 --- a/google/cloud/storage/internal/object_requests.h +++ b/google/cloud/storage/internal/object_requests.h @@ -289,6 +289,39 @@ class ComposeObjectRequest std::ostream& operator<<(std::ostream& os, ComposeObjectRequest const& r); +/** + * Represents a request to the `Objects: move` API. + */ +class MoveObjectRequest + : public GenericObjectRequest< + MoveObjectRequest, IfGenerationMatch, IfGenerationNotMatch, + IfMetagenerationMatch, IfMetagenerationNotMatch, + IfSourceGenerationMatch, IfSourceGenerationNotMatch, + IfSourceMetagenerationMatch, IfSourceMetagenerationNotMatch, + Projection> { + public: + MoveObjectRequest() = default; + explicit MoveObjectRequest(std::string bucket_name, + std::string source_object_name, + std::string destination_object_name) + : bucket_name_(std::move(bucket_name)), + source_object_name_(std::move(source_object_name)), + destination_object_name_(std::move(destination_object_name)) {} + + std::string const& bucket_name() const { return bucket_name_; } + std::string const& source_object_name() const { return source_object_name_; } + std::string const& destination_object_name() const { + return destination_object_name_; + } + + private: + std::string bucket_name_; + std::string source_object_name_; + std::string destination_object_name_; +}; + +std::ostream& operator<<(std::ostream& os, MoveObjectRequest const& r); + /** * Represents a request to the `Objects: patch` API. */ diff --git a/google/cloud/storage/internal/object_requests_test.cc b/google/cloud/storage/internal/object_requests_test.cc index 1acf55ba1f07d..4e25fc2d11036 100644 --- a/google/cloud/storage/internal/object_requests_test.cc +++ b/google/cloud/storage/internal/object_requests_test.cc @@ -542,6 +542,33 @@ TEST(ObjectRequestsTest, RestoreObject) { EXPECT_THAT(actual, HasSubstr("copySourceAcl=true")); } +TEST(ObjectRequestsTest, MoveObject) { + MoveObjectRequest request("test-bucket", "source-object-name", + "destination-object-name"); + EXPECT_EQ("test-bucket", request.bucket_name()); + EXPECT_EQ("source-object-name", request.source_object_name()); + EXPECT_EQ("destination-object-name", request.destination_object_name()); + request.set_multiple_options( + IfGenerationMatch(1), IfGenerationNotMatch(2), IfMetagenerationMatch(3), + IfMetagenerationNotMatch(4), IfSourceGenerationMatch(5), + IfSourceGenerationNotMatch(6), IfSourceMetagenerationMatch(7), + IfSourceMetagenerationNotMatch(8)); + std::ostringstream os; + os << request; + std::string actual = os.str(); + EXPECT_THAT(actual, HasSubstr("test-bucket")); + EXPECT_THAT(actual, HasSubstr("source-object-name")); + EXPECT_THAT(actual, HasSubstr("destination-object-name")); + EXPECT_THAT(actual, HasSubstr("ifGenerationMatch=1")); + EXPECT_THAT(actual, HasSubstr("ifGenerationNotMatch=2")); + EXPECT_THAT(actual, HasSubstr("ifMetagenerationMatch=3")); + EXPECT_THAT(actual, HasSubstr("ifMetagenerationNotMatch=4")); + EXPECT_THAT(actual, HasSubstr("ifSourceGenerationMatch=5")); + EXPECT_THAT(actual, HasSubstr("ifSourceGenerationNotMatch=6")); + EXPECT_THAT(actual, HasSubstr("ifSourceMetagenerationMatch=7")); + EXPECT_THAT(actual, HasSubstr("ifSourceMetagenerationNotMatch=8")); +} + TEST(ObjectRequestsTest, ResumableUpload) { ResumableUploadRequest request("source-bucket", "source-object"); EXPECT_EQ("source-bucket", request.bucket_name()); diff --git a/google/cloud/storage/internal/rest/stub.cc b/google/cloud/storage/internal/rest/stub.cc index 01df4588d7b65..c3fdad7ab5f34 100644 --- a/google/cloud/storage/internal/rest/stub.cc +++ b/google/cloud/storage/internal/rest/stub.cc @@ -558,6 +558,24 @@ StatusOr RestStub::UpdateObject( {absl::MakeConstSpan(payload)})); } +StatusOr RestStub::MoveObject( + rest_internal::RestContext& context, Options const& options, + MoveObjectRequest const& request) { + RestRequestBuilder builder(absl::StrCat( + "storage/", options.get(), "/b/", + request.bucket_name(), "/o/", UrlEncode(request.source_object_name()), + "/moveTo/o/", UrlEncode(request.destination_object_name()))); + auto auth = AddAuthorizationHeader(options, builder); + if (!auth.ok()) return auth; + request.AddOptionsToHttpRequest(builder); + builder.AddHeader("Content-Type", "application/json"); + std::string json_payload("{}"); + + return CheckedFromString( + storage_rest_client_->Post(context, std::move(builder).BuildRequest(), + {absl::MakeConstSpan(json_payload)})); +} + StatusOr RestStub::PatchObject( rest_internal::RestContext& context, Options const& options, PatchObjectRequest const& request) { diff --git a/google/cloud/storage/internal/rest/stub.h b/google/cloud/storage/internal/rest/stub.h index 157a5a54ed7d1..a0d525f93e3db 100644 --- a/google/cloud/storage/internal/rest/stub.h +++ b/google/cloud/storage/internal/rest/stub.h @@ -103,6 +103,9 @@ class RestStub : public storage_internal::GenericStub { StatusOr UpdateObject( rest_internal::RestContext& context, Options const& options, UpdateObjectRequest const& request) override; + StatusOr MoveObject( + rest_internal::RestContext& context, Options const& options, + MoveObjectRequest const& request) override; StatusOr PatchObject( rest_internal::RestContext& context, Options const& options, PatchObjectRequest const& request) override; diff --git a/google/cloud/storage/internal/rest/stub_test.cc b/google/cloud/storage/internal/rest/stub_test.cc index 17a99f79d473c..7f9a8f461f033 100644 --- a/google/cloud/storage/internal/rest/stub_test.cc +++ b/google/cloud/storage/internal/rest/stub_test.cc @@ -363,6 +363,18 @@ TEST(RestStubTest, UpdateObject) { StatusIs(PermanentError().code(), PermanentError().message())); } +TEST(RestStubTest, MoveObject) { + auto mock = std::make_shared(); + EXPECT_CALL(*mock, + Post(ExpectedContext(), ExpectedRequest(), ExpectedPayload())) + .WillOnce(Return(PermanentError())); + auto tested = std::make_unique(Options{}, mock, mock); + auto context = TestContext(); + auto status = tested->MoveObject(context, TestOptions(), MoveObjectRequest()); + EXPECT_THAT(status, + StatusIs(PermanentError().code(), PermanentError().message())); +} + TEST(RestStubTest, PatchObject) { auto mock = std::make_shared(); EXPECT_CALL(*mock, Patch(ExpectedContext(), ExpectedRequest(), _)) diff --git a/google/cloud/storage/internal/storage_connection.h b/google/cloud/storage/internal/storage_connection.h index 8885b0b6bcb0d..72e121bbb08d5 100644 --- a/google/cloud/storage/internal/storage_connection.h +++ b/google/cloud/storage/internal/storage_connection.h @@ -93,6 +93,7 @@ class StorageConnection { ListObjectsRequest const&) = 0; virtual StatusOr DeleteObject(DeleteObjectRequest const&) = 0; virtual StatusOr UpdateObject(UpdateObjectRequest const&) = 0; + virtual StatusOr MoveObject(MoveObjectRequest const&) = 0; virtual StatusOr PatchObject(PatchObjectRequest const&) = 0; virtual StatusOr ComposeObject( ComposeObjectRequest const&) = 0; diff --git a/google/cloud/storage/internal/tracing_connection.cc b/google/cloud/storage/internal/tracing_connection.cc index c3a1ededd45f9..8e5b1fafd8479 100644 --- a/google/cloud/storage/internal/tracing_connection.cc +++ b/google/cloud/storage/internal/tracing_connection.cc @@ -162,6 +162,13 @@ StatusOr TracingConnection::UpdateObject( return internal::EndSpan(*span, impl_->UpdateObject(request)); } +StatusOr TracingConnection::MoveObject( + storage::internal::MoveObjectRequest const& request) { + auto span = internal::MakeSpan("storage::Client::MoveObject"); + auto scope = opentelemetry::trace::Scope(span); + return internal::EndSpan(*span, impl_->MoveObject(request)); +} + StatusOr TracingConnection::PatchObject( storage::internal::PatchObjectRequest const& request) { auto span = internal::MakeSpan("storage::Client::PatchObject"); diff --git a/google/cloud/storage/internal/tracing_connection.h b/google/cloud/storage/internal/tracing_connection.h index 1c8ca2a20ad1d..4778aa1cf4dc3 100644 --- a/google/cloud/storage/internal/tracing_connection.h +++ b/google/cloud/storage/internal/tracing_connection.h @@ -77,6 +77,8 @@ class TracingConnection : public storage::internal::StorageConnection { storage::internal::DeleteObjectRequest const& request) override; StatusOr UpdateObject( storage::internal::UpdateObjectRequest const& request) override; + StatusOr MoveObject( + storage::internal::MoveObjectRequest const& request) override; StatusOr PatchObject( storage::internal::PatchObjectRequest const& request) override; StatusOr ComposeObject( diff --git a/google/cloud/storage/internal/tracing_connection_test.cc b/google/cloud/storage/internal/tracing_connection_test.cc index bbf1e21d1c91c..c5a613de835e0 100644 --- a/google/cloud/storage/internal/tracing_connection_test.cc +++ b/google/cloud/storage/internal/tracing_connection_test.cc @@ -488,6 +488,28 @@ TEST(TracingClientTest, UpdateObject) { "gl-cpp.status_code", code_str))))); } +TEST(TracingClientTest, MoveObject) { + auto span_catcher = InstallSpanCatcher(); + auto mock = std::make_shared(); + EXPECT_CALL(*mock, MoveObject).WillOnce([](auto const&) { + EXPECT_TRUE(ThereIsAnActiveSpan()); + return PermanentError(); + }); + auto under_test = TracingConnection(mock); + auto actual = under_test.MoveObject(storage::internal::MoveObjectRequest()); + auto const code = PermanentError().code(); + auto const code_str = StatusCodeToString(code); + auto const msg = PermanentError().message(); + EXPECT_THAT(actual, StatusIs(code)); + EXPECT_THAT(span_catcher->GetSpans(), + ElementsAre(AllOf( + SpanNamed("storage::Client::MoveObject"), + SpanHasInstrumentationScope(), SpanKindIsClient(), + SpanWithStatus(opentelemetry::trace::StatusCode::kError, msg), + SpanHasAttributes(OTelAttribute( + "gl-cpp.status_code", code_str))))); +} + TEST(TracingClientTest, PatchObject) { auto span_catcher = InstallSpanCatcher(); auto mock = std::make_shared(); diff --git a/google/cloud/storage/testing/mock_client.h b/google/cloud/storage/testing/mock_client.h index 8f8c7e7026e6d..3462e7d68795e 100644 --- a/google/cloud/storage/testing/mock_client.h +++ b/google/cloud/storage/testing/mock_client.h @@ -80,6 +80,8 @@ class MockClient : public google::cloud::storage::internal::StorageConnection { (internal::DeleteObjectRequest const&), (override)); MOCK_METHOD(StatusOr, UpdateObject, (internal::UpdateObjectRequest const&), (override)); + MOCK_METHOD(StatusOr, MoveObject, + (internal::MoveObjectRequest const&), (override)); MOCK_METHOD(StatusOr, PatchObject, (internal::PatchObjectRequest const&), (override)); MOCK_METHOD(StatusOr, ComposeObject, diff --git a/google/cloud/storage/testing/mock_generic_stub.h b/google/cloud/storage/testing/mock_generic_stub.h index c51627800711b..4bc5e1d74795a 100644 --- a/google/cloud/storage/testing/mock_generic_stub.h +++ b/google/cloud/storage/testing/mock_generic_stub.h @@ -107,6 +107,10 @@ class MockGenericStub : public storage_internal::GenericStub { (rest_internal::RestContext&, Options const&, storage::internal::UpdateObjectRequest const&), (override)); + MOCK_METHOD(StatusOr, MoveObject, + (rest_internal::RestContext&, Options const&, + storage::internal::MoveObjectRequest const&), + (override)); MOCK_METHOD(StatusOr, PatchObject, (rest_internal::RestContext&, Options const&, storage::internal::PatchObjectRequest const&), diff --git a/google/cloud/storage/testing/object_integration_test.cc b/google/cloud/storage/testing/object_integration_test.cc index 043fbb309cee7..b3d9d7aa1eb0a 100644 --- a/google/cloud/storage/testing/object_integration_test.cc +++ b/google/cloud/storage/testing/object_integration_test.cc @@ -71,6 +71,11 @@ void ObjectIntegrationTest::SetUp() { "GOOGLE_CLOUD_CPP_STORAGE_TEST_BUCKET_NAME") .value_or(""); ASSERT_FALSE(bucket_name_.empty()); + folder_enabled_bucket_name_ = + google::cloud::internal::GetEnv( + "GOOGLE_CLOUD_CPP_STORAGE_TEST_FOLDER_BUCKET_NAME") + .value_or(""); + ASSERT_FALSE(folder_enabled_bucket_name_.empty()); } std::string ObjectIntegrationTest::MakeEntityName() const { diff --git a/google/cloud/storage/testing/object_integration_test.h b/google/cloud/storage/testing/object_integration_test.h index dfecadf313d64..43f50b4c8e297 100644 --- a/google/cloud/storage/testing/object_integration_test.h +++ b/google/cloud/storage/testing/object_integration_test.h @@ -34,6 +34,7 @@ class ObjectIntegrationTest std::string project_id_; std::string bucket_name_; + std::string folder_enabled_bucket_name_; }; } // namespace testing diff --git a/google/cloud/storage/tests/object_integration_test.cc b/google/cloud/storage/tests/object_integration_test.cc index 26ec99762f274..134157180e410 100644 --- a/google/cloud/storage/tests/object_integration_test.cc +++ b/google/cloud/storage/tests/object_integration_test.cc @@ -849,6 +849,28 @@ TEST_F(ObjectIntegrationTest, RestoreObject) { EXPECT_EQ(restore.value().metageneration(), 1); } +TEST_F(ObjectIntegrationTest, MoveObject) { + auto client = MakeIntegrationTestClient(); + auto src_object_name = MakeRandomObjectName(); + auto dst_object_name = MakeRandomObjectName(); + std::string expected = LoremIpsum(); + + auto stream = client.WriteObject(folder_enabled_bucket_name_, src_object_name, + IfGenerationMatch(0)); + stream << expected; + stream.Close(); + auto metadata = stream.metadata(); + ASSERT_STATUS_OK(metadata); + ScheduleForDelete(*metadata); + + auto move = client.MoveObject(folder_enabled_bucket_name_, src_object_name, + dst_object_name); + ASSERT_STATUS_OK(move); + + EXPECT_NE(metadata.value().generation(), move.value().generation()); + EXPECT_EQ(move.value().metageneration(), 1); +} + } // anonymous namespace GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END } // namespace storage