Skip to content
Draft
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
2 changes: 2 additions & 0 deletions google/cloud/storage/google_cloud_cpp_storage.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ google_cloud_cpp_storage_hdrs = [
"oauth2/refreshing_credentials_wrapper.h",
"oauth2/service_account_credentials.h",
"object_access_control.h",
"object_contexts.h",
"object_metadata.h",
"object_read_stream.h",
"object_retention.h",
Expand Down Expand Up @@ -251,6 +252,7 @@ google_cloud_cpp_storage_srcs = [
"oauth2/refreshing_credentials_wrapper.cc",
"oauth2/service_account_credentials.cc",
"object_access_control.cc",
"object_contexts.cc",
"object_metadata.cc",
"object_read_stream.cc",
"object_retention.cc",
Expand Down
2 changes: 2 additions & 0 deletions google/cloud/storage/google_cloud_cpp_storage.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ add_library(
oauth2/service_account_credentials.h
object_access_control.cc
object_access_control.h
object_contexts.cc
object_contexts.h
object_metadata.cc
object_metadata.h
object_read_stream.cc
Expand Down
39 changes: 39 additions & 0 deletions google/cloud/storage/internal/grpc/object_request_parser.cc
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,17 @@ Status SetObjectMetadata(google::storage::v2::Object& resource,
*resource.mutable_custom_time() =
google::cloud::internal::ToProtoTimestamp(metadata.custom_time());
}
if (metadata.has_contexts()) {
auto& contexts_proto = *resource.mutable_contexts();
if (metadata.contexts().has_custom()) {
auto& custom_map = *contexts_proto.mutable_custom();
for (auto const& kv : metadata.contexts().custom()) {
if (kv.second.has_value()) {
custom_map[kv.first].set_value(kv.second->value);
}
}
}
}
return Status{};
}

Expand Down Expand Up @@ -288,6 +299,17 @@ StatusOr<google::storage::v2::ComposeObjectRequest> ToProto(
*destination.mutable_custom_time() =
google::cloud::internal::ToProtoTimestamp(metadata.custom_time());
}
if (metadata.has_contexts()) {
auto& contexts_proto = *destination.mutable_contexts();
if (metadata.contexts().has_custom()) {
auto& custom_map = *contexts_proto.mutable_custom();
for (auto const& kv : metadata.contexts().custom()) {
if (kv.second.has_value()) {
custom_map[kv.first].set_value(kv.second->value);
}
}
}
}
}
for (auto const& s : request.source_objects()) {
google::storage::v2::ComposeObjectRequest::SourceObject source;
Expand Down Expand Up @@ -516,6 +538,23 @@ StatusOr<google::storage::v2::UpdateObjectRequest> ToProto(
request.metadata().custom_time());
}

if (request.metadata().has_contexts()) {
result.mutable_update_mask()->add_paths("contexts");
auto& contexts_proto = *object.mutable_contexts();
if (request.metadata().contexts().has_custom()) {
auto& custom_map = *contexts_proto.mutable_custom();
for (auto const& kv : request.metadata().contexts().custom()) {
if (kv.second.has_value()) {
custom_map[kv.first].set_value(kv.second->value);
} else {
custom_map.erase(kv.first);
}
}
} else {
contexts_proto.clear_custom();
}
}

// We need to check each modifiable field.
result.mutable_update_mask()->add_paths("cache_control");
object.set_cache_control(request.metadata().cache_control());
Expand Down
68 changes: 68 additions & 0 deletions google/cloud/storage/internal/object_metadata_parser.cc
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,35 @@ void SetIfNotEmpty(nlohmann::json& json, char const* key,
json[key] = value;
}

/**
* Populates the "contexts" field in the JSON object from the given metadata.
*/
void SetJsonContextsIfNotEmpty(nlohmann::json& json,
ObjectMetadata const& meta) {
if (!meta.has_contexts()) {
return;
}
if (meta.contexts().has_custom()) {
nlohmann::json custom_json;
for (auto const& kv : meta.contexts().custom()) {
if (kv.second.has_value()) {
nlohmann::json item;
item["value"] = kv.second.value().value;
item["createTime"] = google::cloud::internal::FormatRfc3339(
kv.second.value().create_time);
item["updateTime"] = google::cloud::internal::FormatRfc3339(
kv.second.value().update_time);
custom_json[kv.first] = std::move(item);
} else {
custom_json[kv.first] = nullptr;
}
}
json["contexts"] = nlohmann::json{{"custom", std::move(custom_json)}};
} else {
json["contexts"] = nlohmann::json{{"custom", nullptr}};
}
}

Status ParseAcl(ObjectMetadata& meta, nlohmann::json const& json) {
auto i = json.find("acl");
if (i == json.end()) return Status{};
Expand Down Expand Up @@ -160,6 +189,40 @@ Status ParseRetention(ObjectMetadata& meta, nlohmann::json const& json) {
return Status{};
}

Status ParseContexts(ObjectMetadata& meta, nlohmann::json const& json) {
auto f_contexts = json.find("contexts");
if (f_contexts == json.end()) return Status{};

auto f_custom = f_contexts->find("custom");
if (f_custom == f_contexts->end()) return Status{};

ObjectContexts contexts;
for (auto const& kv : f_custom->items()) {
if (kv.value().is_null()) {
contexts.upsert_custom_context(kv.key(), absl::nullopt);

} else {
ObjectCustomContextPayload payload;
auto value = kv.value().value("value", "");
payload.value = value;

auto create_time =
internal::ParseTimestampField(kv.value(), "createTime");
if (!create_time) return std::move(create_time).status();
payload.create_time = *create_time;

auto update_time =
internal::ParseTimestampField(kv.value(), "updateTime");
if (!update_time) return std::move(update_time).status();
payload.update_time = *update_time;

contexts.upsert_custom_context(kv.key(), std::move(payload));
}
}
meta.set_contexts(std::move(contexts));
return Status{};
}

Status ParseSize(ObjectMetadata& meta, nlohmann::json const& json) {
auto v = internal::ParseUnsignedLongField(json, "size");
if (!v) return std::move(v).status();
Expand Down Expand Up @@ -296,6 +359,7 @@ StatusOr<ObjectMetadata> ObjectMetadataParser::FromJson(
ParseOwner,
ParseRetentionExpirationTime,
ParseRetention,
ParseContexts,
[](ObjectMetadata& meta, nlohmann::json const& json) {
return SetStringField(meta, json, "selfLink",
&ObjectMetadata::set_self_link);
Expand Down Expand Up @@ -372,6 +436,8 @@ nlohmann::json ObjectMetadataJsonForCompose(ObjectMetadata const& meta) {
meta.retention().retain_until_time)}};
}

SetJsonContextsIfNotEmpty(metadata_as_json, meta);

return metadata_as_json;
}

Expand Down Expand Up @@ -430,6 +496,8 @@ nlohmann::json ObjectMetadataJsonForUpdate(ObjectMetadata const& meta) {
meta.retention().retain_until_time)}};
}

SetJsonContextsIfNotEmpty(metadata_as_json, meta);

return metadata_as_json;
}

Expand Down
55 changes: 55 additions & 0 deletions google/cloud/storage/object_contexts.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright 2026 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/object_contexts.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,
ObjectCustomContextPayload const& rhs) {
return os << "ObjectCustomContextPayload={value=" << rhs.value
<< ", create_time="
<< google::cloud::internal::FormatRfc3339(rhs.create_time)
<< ", update_time="
<< google::cloud::internal::FormatRfc3339(rhs.update_time) << "}";
}

std::ostream& operator<<(std::ostream& os, ObjectContexts const& rhs) {
os << "ObjectContexts={custom={";
if (rhs.has_custom()) {
char const* sep = "";
for (auto const& kv : rhs.custom()) {
os << sep << kv.first << "=";
if (kv.second.has_value()) {
os << kv.second.value();
} else {
os << "null";
}
sep = ",\n";
}
} else {
os << "null";
}
return os << "}}";
}

GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
} // namespace storage
} // namespace cloud
} // namespace google
126 changes: 126 additions & 0 deletions google/cloud/storage/object_contexts.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright 2026 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_OBJECT_CONTEXTS_H
#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_OBJECT_CONTEXTS_H

#include "google/cloud/storage/version.h"
#include "absl/types/optional.h"
#include <chrono>
#include <map>
#include <string>

namespace google {
namespace cloud {
namespace storage {
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN

/**
* Represents the payload of a user-defined object context.
*/
struct ObjectCustomContextPayload {
std::string value;

std::chrono::system_clock::time_point create_time;

std::chrono::system_clock::time_point update_time;
};

inline bool operator==(ObjectCustomContextPayload const& lhs,
ObjectCustomContextPayload const& rhs) {
return std::tie(lhs.value, lhs.create_time, lhs.update_time) ==
std::tie(rhs.value, rhs.create_time, rhs.update_time);
};

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

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

/**
* Specifies the custom contexts of an object.
*/
struct ObjectContexts {
public:
// Returns true if the map itself exists.
bool has_custom() const { return custom_map_.has_value(); }

/**
* Returns true if the map exists AND the key is present AND the value is
* a valid value.
*/
bool has_custom(std::string const& key) const {
if (!has_custom()) {
return false;
}
return custom_map_->find(key) != custom_map_->end() &&
custom_map_->at(key).has_value();
}

/**
* The `custom` attribute of the object contexts.
* Values are now absl::optional.
*
* It is undefined behavior to call this member function if
* `has_custom() == false`.
*/
std::map<std::string, absl::optional<ObjectCustomContextPayload>> const&
custom() const {
return *custom_map_;
}

/**
* Upserts a context. Passing absl::nullopt for the value
* represents a "null" entry in the map.
*/
void upsert_custom_context(std::string const& key,
absl::optional<ObjectCustomContextPayload> value) {
if (!has_custom()) {
custom_map_.emplace();
}

(*custom_map_)[key] = std::move(value);
}

void reset_custom() { custom_map_.reset(); }

bool operator==(ObjectContexts const& other) const {
return custom_map_ == other.custom_map_;
}

bool operator!=(ObjectContexts const& other) const {
return !(*this == other);
}

private:
/**
* Represents the map of user-defined object contexts.
* Inner optional allows keys to point to a "null" value.
*/
absl::optional<
std::map<std::string, absl::optional<ObjectCustomContextPayload>>>
custom_map_;
};

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

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

#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_OBJECT_CONTEXTS_H
4 changes: 4 additions & 0 deletions google/cloud/storage/object_metadata.cc
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ bool operator==(ObjectMetadata const& lhs, ObjectMetadata const& rhs) {
&& lhs.updated_ == rhs.updated_ //
&& lhs.soft_delete_time_ == rhs.soft_delete_time_ //
&& lhs.hard_delete_time_ == rhs.hard_delete_time_ //
&& lhs.contexts_ == rhs.contexts_ //
;
}

Expand Down Expand Up @@ -133,6 +134,9 @@ std::ostream& operator<<(std::ostream& os, ObjectMetadata const& rhs) {
if (rhs.has_hard_delete_time()) {
os << ", hard_delete_time=" << FormatRfc3339(rhs.hard_delete_time());
}
if (rhs.has_contexts()) {
os << ", contexts=" << rhs.contexts();
}
return os << "}";
}

Expand Down
Loading