Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(storage): support Bucket soft-delete metadata #13623

Merged
Merged
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
26 changes: 24 additions & 2 deletions google/cloud/storage/bucket_metadata.cc
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ bool operator==(BucketMetadata const& lhs, BucketMetadata const& rhs) {
&& lhs.retention_policy_ == rhs.retention_policy_ //
&& lhs.rpo_ == rhs.rpo_ //
&& lhs.self_link_ == rhs.self_link_ //
&& lhs.soft_delete_policy_ == rhs.soft_delete_policy_ //
&& lhs.storage_class_ == rhs.storage_class_ //
&& lhs.time_created_ == rhs.time_created_ //
&& lhs.updated_ == rhs.updated_ //
Expand Down Expand Up @@ -182,8 +183,11 @@ std::ostream& operator<<(std::ostream& os, BucketMetadata const& rhs) {
}

os << ", project_number=" << rhs.project_number()
<< ", self_link=" << rhs.self_link()
<< ", storage_class=" << rhs.storage_class() << ", time_created="
<< ", self_link=" << rhs.self_link();
if (rhs.has_soft_delete_policy()) {
os << ", soft_delete_policy=" << rhs.soft_delete_policy();
}
os << ", storage_class=" << rhs.storage_class() << ", time_created="
<< google::cloud::internal::FormatRfc3339(rhs.time_created())
<< ", updated=" << google::cloud::internal::FormatRfc3339(rhs.updated());

Expand Down Expand Up @@ -487,6 +491,24 @@ BucketMetadataPatchBuilder& BucketMetadataPatchBuilder::ResetRpo() {
return *this;
}

BucketMetadataPatchBuilder& BucketMetadataPatchBuilder::SetSoftDeletePolicy(
BucketSoftDeletePolicy const& v) {
// Only the retentionDurationSeconds field is writeable, so do not modify the
// other fields.
impl_.AddSubPatch(
"softDeletePolicy",
internal::PatchBuilder().SetIntField(
"retentionDurationSeconds",
static_cast<std::uint64_t>(v.retention_duration.count())));
return *this;
}

BucketMetadataPatchBuilder&
BucketMetadataPatchBuilder::ResetSoftDeletePolicy() {
impl_.RemoveField("softDeletePolicy");
return *this;
}

BucketMetadataPatchBuilder& BucketMetadataPatchBuilder::SetStorageClass(
std::string const& v) {
if (v.empty()) {
Expand Down
49 changes: 49 additions & 0 deletions google/cloud/storage/bucket_metadata.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "google/cloud/storage/bucket_logging.h"
#include "google/cloud/storage/bucket_retention_policy.h"
#include "google/cloud/storage/bucket_rpo.h"
#include "google/cloud/storage/bucket_soft_delete_policy.h"
#include "google/cloud/storage/bucket_versioning.h"
#include "google/cloud/storage/bucket_website.h"
#include "google/cloud/storage/internal/patch_builder.h"
Expand Down Expand Up @@ -464,6 +465,42 @@ class BucketMetadata {
}
///@}

/// @name Accessors and modifiers for the soft delete policy.
///@{
bool has_soft_delete_policy() const {
return soft_delete_policy_.has_value();
}
BucketSoftDeletePolicy const& soft_delete_policy() const {
return *soft_delete_policy_;
}
absl::optional<BucketSoftDeletePolicy> const& soft_delete_policy_as_optional()
const {
return soft_delete_policy_;
}
BucketMetadata& set_soft_delete_policy(BucketSoftDeletePolicy v) {
soft_delete_policy_ = std::move(v);
return *this;
}

/**
* Sets the soft delete policy.
*
* The retention period is the only writable attribute in a retention policy.
* This function makes it easier to set the retention policy when the
* `BucketMetadata` object is used to update or patch the bucket.
*/
BucketMetadata& set_soft_delete_policy(
std::chrono::seconds retention_duration) {
return set_soft_delete_policy(BucketSoftDeletePolicy{
retention_duration, std::chrono::system_clock::time_point{}});
}

BucketMetadata& reset_soft_delete_policy() {
soft_delete_policy_.reset();
return *this;
}
///@}

/// @name Access and modify the default storage class attribute.
///@{
std::string const& storage_class() const { return storage_class_; }
Expand Down Expand Up @@ -590,6 +627,7 @@ class BucketMetadata {
absl::optional<BucketRetentionPolicy> retention_policy_;
std::string rpo_;
std::string self_link_;
absl::optional<BucketSoftDeletePolicy> soft_delete_policy_;
std::string storage_class_;
std::chrono::system_clock::time_point time_created_;
std::chrono::system_clock::time_point updated_;
Expand Down Expand Up @@ -683,6 +721,17 @@ class BucketMetadataPatchBuilder {
BucketMetadataPatchBuilder& SetRpo(std::string const& v);
BucketMetadataPatchBuilder& ResetRpo();

BucketMetadataPatchBuilder& SetSoftDeletePolicy(
BucketSoftDeletePolicy const& v);
BucketMetadataPatchBuilder& SetSoftDeletePolicy(
std::chrono::seconds retention_duration) {
// This is the only parameter that the application can set, so make it easy
// for them to set it.
return SetSoftDeletePolicy(BucketSoftDeletePolicy{
retention_duration, std::chrono::system_clock::time_point{}});
}
BucketMetadataPatchBuilder& ResetSoftDeletePolicy();

BucketMetadataPatchBuilder& SetStorageClass(std::string const& v);
BucketMetadataPatchBuilder& ResetStorageClass();

Expand Down
78 changes: 78 additions & 0 deletions google/cloud/storage/bucket_metadata_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ namespace {
using ::testing::ElementsAre;
using ::testing::HasSubstr;
using ::testing::Not;
using ::testing::Optional;

BucketMetadata CreateBucketMetadataForTest() {
// This metadata object has some impossible combination of fields in it. The
Expand Down Expand Up @@ -160,6 +161,10 @@ BucketMetadata CreateBucketMetadataForTest() {
},
"rpo": "DEFAULT",
"selfLink": "https://storage.googleapis.com/storage/v1/b/test-bucket",
"softDeletePolicy": {
"retentionDurationSeconds": 604800,
"effectiveTime": "2024-02-15T12:34:56Z"
},
"storageClass": "STANDARD",
"timeCreated": "2018-05-19T19:31:14Z",
"updated": "2018-05-19T19:31:24Z",
Expand Down Expand Up @@ -282,6 +287,14 @@ TEST(BucketMetadataTest, Parse) {

EXPECT_EQ("https://storage.googleapis.com/storage/v1/b/test-bucket",
actual.self_link());

// soft_delete_policy
ASSERT_THAT(actual.soft_delete_policy_as_optional(),
Optional(BucketSoftDeletePolicy{
std::chrono::seconds(604800),
google::cloud::internal::ParseRfc3339("2024-02-15T12:34:56Z")
.value()}));

EXPECT_EQ(storage_class::Standard(), actual.storage_class());
// Use `date -u +%s --date='2018-05-19T19:31:14Z'` to get the magic number:
auto magic_timestamp = 1526758274L;
Expand Down Expand Up @@ -380,6 +393,12 @@ TEST(BucketMetadataTest, IOStream) {
// rpo()
EXPECT_THAT(actual, HasSubstr("rpo=DEFAULT"));

// soft_delete_policy()
EXPECT_THAT(
actual,
HasSubstr("soft_delete_policy=BucketSoftDeletePolicy={retention_duration="
"604800s, effective_time=2024-02-15T12:34:56Z}"));

// versioning()
EXPECT_THAT(actual, HasSubstr("versioning.enabled=true"));

Expand Down Expand Up @@ -516,6 +535,14 @@ TEST(BucketMetadataTest, ToJsonString) {
EXPECT_TRUE(actual.contains("rpo"));
EXPECT_EQ("DEFAULT", actual.value("rpo", ""));

// soft_delete_policy()
EXPECT_TRUE(actual.contains("softDeletePolicy"));
// The effectiveTime field is only set by the service and should not be sent
// in requests. Therefore, `ToJsonString()` does not output its value.
auto const expected_soft_delete_policy =
nlohmann::json{{"retentionDurationSeconds", 604800}};
EXPECT_EQ(expected_soft_delete_policy, actual["softDeletePolicy"]);

// storage_class()
ASSERT_EQ("STANDARD", actual.value("storageClass", ""));

Expand Down Expand Up @@ -876,6 +903,33 @@ TEST(BucketMetadataTest, SetRPO) {
EXPECT_THAT(os.str(), HasSubstr("rpo=ASYNC_TURBO"));
}

/// @test Verify we can change the soft delete policy in BucketMetadata.
TEST(BucketMetadataTest, SetSoftDeletePolicy) {
auto expected = CreateBucketMetadataForTest();
BucketSoftDeletePolicy change{
std::chrono::seconds(3600),
google::cloud::internal::ParseRfc3339("2024-02-15T23:45:60Z").value(),
};
auto copy = expected;
copy.set_soft_delete_policy(change);
ASSERT_THAT(copy.soft_delete_policy_as_optional(), Optional(change));
EXPECT_EQ(change, copy.soft_delete_policy());
EXPECT_NE(expected, copy);
}

/// @test Verify we can change the soft delete policy in BucketMetadata.
TEST(BucketMetadataTest, ResetSoftDeletePolicy) {
auto expected = CreateBucketMetadataForTest();
EXPECT_TRUE(expected.has_soft_delete_policy());
auto copy = expected;
copy.reset_soft_delete_policy();
EXPECT_FALSE(copy.has_soft_delete_policy());
EXPECT_NE(expected, copy);
std::ostringstream os;
os << copy;
EXPECT_THAT(os.str(), Not(HasSubstr("soft_delete_policy=")));
}

/// @test Verify we can clear the versioning field in BucketMetadata.
TEST(BucketMetadataTest, ClearVersioning) {
auto expected = CreateBucketMetadataForTest();
Expand Down Expand Up @@ -1377,6 +1431,30 @@ TEST(BucketMetadataPatchBuilder, ResetRpo) {
ASSERT_TRUE(json["rpo"].is_null()) << json;
}

TEST(BucketMetadataPatchBuilder, SetSoftDeletePolicy) {
BucketMetadataPatchBuilder builder;
builder.SetSoftDeletePolicy(BucketSoftDeletePolicy{
std::chrono::seconds(604800),
google::cloud::internal::ParseRfc3339("2024-03-01T12:00:00Z").value()});

auto actual_patch = builder.BuildPatch();
auto actual_json = nlohmann::json::parse(actual_patch);
auto expected_json =
nlohmann::json{{"softDeletePolicy",
nlohmann::json{{"retentionDurationSeconds", 604800}}}};
EXPECT_EQ(expected_json, actual_json);
}

TEST(BucketMetadataPatchBuilder, ResetSoftDeletePolicy) {
BucketMetadataPatchBuilder builder;
builder.ResetSoftDeletePolicy();

auto actual = builder.BuildPatch();
auto json = nlohmann::json::parse(actual);
ASSERT_EQ(1U, json.count("softDeletePolicy")) << json;
ASSERT_TRUE(json["softDeletePolicy"].is_null()) << json;
}

TEST(BucketMetadataPatchBuilder, SetStorageClass) {
BucketMetadataPatchBuilder builder;
builder.SetStorageClass("NEARLINE");
Expand Down
34 changes: 34 additions & 0 deletions google/cloud/storage/bucket_soft_delete_policy.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "google/cloud/storage/bucket_soft_delete_policy.h"
#include "google/cloud/internal/format_time_point.h"
#include <iostream>

namespace google {
namespace cloud {
namespace storage {
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN

std::ostream& operator<<(std::ostream& os, BucketSoftDeletePolicy const& rhs) {
return os << "BucketSoftDeletePolicy={retention_duration="
<< rhs.retention_duration.count() << "s, effective_time="
<< google::cloud::internal::FormatRfc3339(rhs.effective_time)
<< "}";
}

GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
} // namespace storage
} // namespace cloud
} // namespace google
57 changes: 57 additions & 0 deletions google/cloud/storage/bucket_soft_delete_policy.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#ifndef GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_BUCKET_SOFT_DELETE_POLICY_H
#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_BUCKET_SOFT_DELETE_POLICY_H

#include "google/cloud/storage/version.h"
#include <chrono>
#include <iosfwd>
#include <tuple>

namespace google {
namespace cloud {
namespace storage {
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN

/**
* The soft delete policy for a bucket.
*
* The soft delete policy prevents soft-deleted objects from being permanently
* deleted.
*/
struct BucketSoftDeletePolicy {
std::chrono::seconds retention_duration;
std::chrono::system_clock::time_point effective_time;
};

inline bool operator==(BucketSoftDeletePolicy const& lhs,
BucketSoftDeletePolicy const& rhs) {
return std::tie(lhs.retention_duration, lhs.effective_time) ==
std::tie(rhs.retention_duration, rhs.effective_time);
}

inline bool operator!=(BucketSoftDeletePolicy const& lhs,
BucketSoftDeletePolicy const& rhs) {
return !(lhs == rhs);
}

std::ostream& operator<<(std::ostream& os, BucketSoftDeletePolicy const& rhs);

GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
} // namespace storage
} // namespace cloud
} // namespace google

#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_BUCKET_SOFT_DELETE_POLICY_H
Loading