diff --git a/sdk/storage/inc/blobs/blob_options.hpp b/sdk/storage/inc/blobs/blob_options.hpp index cc14cfaa8..24b7912b6 100644 --- a/sdk/storage/inc/blobs/blob_options.hpp +++ b/sdk/storage/inc/blobs/blob_options.hpp @@ -137,6 +137,28 @@ namespace Azure { namespace Storage { namespace Blobs { Azure::Core::Context Context; }; + /** + * @brief Optional parameters for BlobServiceClient::SetProperties. + */ + struct SetBlobServicePropertiesOptions + { + /** + * @brief Context for cancelling long running operations. + */ + Azure::Core::Context Context; + }; + + /** + * @brief Optional parameters for BlobServiceClient::GetProperties. + */ + struct GetBlobServicePropertiesOptions + { + /** + * @brief Context for cancelling long running operations. + */ + Azure::Core::Context Context; + }; + /** * @brief Container client options used to initalize BlobContainerClient. */ diff --git a/sdk/storage/inc/blobs/blob_service_client.hpp b/sdk/storage/inc/blobs/blob_service_client.hpp index 114359589..75b86c636 100644 --- a/sdk/storage/inc/blobs/blob_service_client.hpp +++ b/sdk/storage/inc/blobs/blob_service_client.hpp @@ -124,6 +124,34 @@ namespace Azure { namespace Storage { namespace Blobs { const std::string& expiresOn, const GetUserDelegationKeyOptions& options = GetUserDelegationKeyOptions()) const; + /** + * @brief Sets properties for a storage account’s Blob service endpoint, including + * properties for Storage Analytics, CORS (Cross-Origin Resource Sharing) rules and soft delete + * settings. You can also use this operation to set the default request version for all incoming + * requests to the Blob service that do not have a version specified. + * + * @param + * properties The blob service properties. + * @param options Optional parameters to execute + * this function. + * @return A SetServicePropertiesInfo on successfully setting the + * properties. + */ + Azure::Core::Response SetProperties( + BlobServiceProperties properties, + const SetBlobServicePropertiesOptions& options = SetBlobServicePropertiesOptions()) const; + + /** + * @brief Gets the properties of a storage account’s blob service, including properties + * for Storage Analytics and CORS (Cross-Origin Resource Sharing) rules. + * + * @param options Optional parameters to execute this function. + * @return A BlobServiceProperties + * describing the service properties. + */ + Azure::Core::Response GetProperties( + const GetBlobServicePropertiesOptions& options = GetBlobServicePropertiesOptions()) const; + protected: UriBuilder m_serviceUrl; std::shared_ptr m_pipeline; diff --git a/sdk/storage/inc/blobs/protocol/blob_rest_client.hpp b/sdk/storage/inc/blobs/protocol/blob_rest_client.hpp index 0d8702452..4208845bb 100644 --- a/sdk/storage/inc/blobs/protocol/blob_rest_client.hpp +++ b/sdk/storage/inc/blobs/protocol/blob_rest_client.hpp @@ -247,6 +247,15 @@ namespace Azure { namespace Storage { namespace Blobs { Azure::Core::Nullable EncryptionKeySHA256; }; // struct BlobContentInfo + struct BlobCorsRule + { + std::string AllowedOrigins; + std::string AllowedMethods; + std::string AllowedHeaders; + std::string ExposedHeaders; + int32_t MaxAgeInSeconds = 0; + }; // struct BlobCorsRule + struct BlobHttpHeaders { std::string ContentType; @@ -354,6 +363,12 @@ namespace Azure { namespace Storage { namespace Blobs { std::string Name; }; // struct BlobPrefix + struct BlobRetentionPolicy + { + bool Enabled = false; + Azure::Core::Nullable Days; + }; // struct BlobRetentionPolicy + struct BlobSnapshotInfo { std::string Snapshot; @@ -363,6 +378,14 @@ namespace Azure { namespace Storage { namespace Blobs { Azure::Core::Nullable EncryptionKeySHA256; }; // struct BlobSnapshotInfo + struct BlobStaticWebsite + { + bool Enabled = false; + Azure::Core::Nullable IndexDocument; + Azure::Core::Nullable DefaultIndexDocumentPath; + Azure::Core::Nullable ErrorDocument404Path; + }; // struct BlobStaticWebsite + enum class BlobType { Unknown, @@ -824,6 +847,10 @@ namespace Azure { namespace Storage { namespace Blobs { { }; // struct SetBlobAccessTierInfo + struct SetServicePropertiesInfo + { + }; // struct SetServicePropertiesInfo + struct UndeleteBlobInfo { }; // struct UndeleteBlobInfo @@ -839,6 +866,15 @@ namespace Azure { namespace Storage { namespace Blobs { std::string Value; }; // struct UserDelegationKey + struct BlobAnalyticsLogging + { + std::string Version; + bool Delete = false; + bool Read = false; + bool Write = false; + BlobRetentionPolicy RetentionPolicy; + }; // struct BlobAnalyticsLogging + struct BlobBlockListInfo { std::string ETag; @@ -925,6 +961,14 @@ namespace Azure { namespace Storage { namespace Blobs { Azure::Core::Nullable EncryptionKeySHA256; }; // struct BlobItem + struct BlobMetrics + { + std::string Version; + bool Enabled = false; + BlobRetentionPolicy RetentionPolicy; + Azure::Core::Nullable IncludeApis; + }; // struct BlobMetrics + struct BlobProperties { std::string ETag; @@ -952,6 +996,17 @@ namespace Azure { namespace Storage { namespace Blobs { Azure::Core::Nullable CopyCompletionTime; }; // struct BlobProperties + struct BlobServiceProperties + { + BlobAnalyticsLogging Logging; + BlobMetrics HourMetrics; + BlobMetrics MinuteMetrics; + std::vector Cors; + std::string DefaultServiceVersion; + BlobRetentionPolicy DeleteRetentionPolicy; + BlobStaticWebsite StaticWebsite; + }; // struct BlobServiceProperties + struct BlobsFlatSegment { std::string ServiceEndpoint; @@ -1065,6 +1120,7 @@ namespace Azure { namespace Storage { namespace Blobs { XmlWriter writer; GetUserDelegationKeyOptionsToXml(writer, options); xml_body = writer.GetDocument(); + writer.Write(XmlNode{XmlNodeType::End}); } Azure::Core::Http::MemoryBodyStream xml_body_stream( reinterpret_cast(xml_body.data()), xml_body.length()); @@ -1098,7 +1154,222 @@ namespace Azure { namespace Storage { namespace Blobs { std::move(response), std::move(pHttpResponse)); } + struct GetPropertiesOptions + { + Azure::Core::Nullable Timeout; + }; // struct GetPropertiesOptions + + static Azure::Core::Response GetProperties( + Azure::Core::Context context, + Azure::Core::Http::HttpPipeline& pipeline, + const std::string& url, + const GetPropertiesOptions& options) + { + auto request = Azure::Core::Http::Request(Azure::Core::Http::HttpMethod::Get, url); + request.AddQueryParameter("restype", "service"); + request.AddQueryParameter("comp", "properties"); + request.AddHeader("x-ms-version", c_APIVersion); + if (options.Timeout.HasValue()) + { + request.AddQueryParameter("timeout", std::to_string(options.Timeout.GetValue())); + } + auto pHttpResponse = pipeline.Send(context, request); + Azure::Core::Http::RawResponse& httpResponse = *pHttpResponse; + BlobServiceProperties response; + auto http_status_code + = static_cast::type>( + httpResponse.GetStatusCode()); + if (!(http_status_code == 200)) + { + throw StorageError::CreateFromResponse(context, std::move(pHttpResponse)); + } + { + const auto& httpResponseBody = httpResponse.GetBody(); + XmlReader reader( + reinterpret_cast(httpResponseBody.data()), httpResponseBody.size()); + response = BlobServicePropertiesFromXml(reader); + } + return Azure::Core::Response( + std::move(response), std::move(pHttpResponse)); + } + + struct SetPropertiesOptions + { + Azure::Core::Nullable Timeout; + BlobServiceProperties Properties; + }; // struct SetPropertiesOptions + + static Azure::Core::Response SetProperties( + Azure::Core::Context context, + Azure::Core::Http::HttpPipeline& pipeline, + const std::string& url, + const SetPropertiesOptions& options) + { + std::string xml_body; + { + XmlWriter writer; + SetPropertiesOptionsToXml(writer, options); + xml_body = writer.GetDocument(); + writer.Write(XmlNode{XmlNodeType::End}); + } + Azure::Core::Http::MemoryBodyStream xml_body_stream( + reinterpret_cast(xml_body.data()), xml_body.length()); + auto request + = Azure::Core::Http::Request(Azure::Core::Http::HttpMethod::Put, url, &xml_body_stream); + request.AddHeader("Content-Length", std::to_string(xml_body_stream.Length())); + request.AddQueryParameter("restype", "service"); + request.AddQueryParameter("comp", "properties"); + request.AddHeader("x-ms-version", c_APIVersion); + if (options.Timeout.HasValue()) + { + request.AddQueryParameter("timeout", std::to_string(options.Timeout.GetValue())); + } + auto pHttpResponse = pipeline.Send(context, request); + Azure::Core::Http::RawResponse& httpResponse = *pHttpResponse; + SetServicePropertiesInfo response; + auto http_status_code + = static_cast::type>( + httpResponse.GetStatusCode()); + if (!(http_status_code == 202)) + { + throw StorageError::CreateFromResponse(context, std::move(pHttpResponse)); + } + return Azure::Core::Response( + std::move(response), std::move(pHttpResponse)); + } + private: + static BlobServiceProperties BlobServicePropertiesFromXml(XmlReader& reader) + { + BlobServiceProperties ret; + enum class XmlTagName + { + k_StorageServiceProperties, + k_Logging, + k_HourMetrics, + k_MinuteMetrics, + k_Cors, + k_CorsRule, + k_DefaultServiceVersion, + k_DeleteRetentionPolicy, + k_StaticWebsite, + k_Unknown, + }; + std::vector path; + while (true) + { + auto node = reader.Read(); + if (node.Type == XmlNodeType::End) + { + break; + } + else if (node.Type == XmlNodeType::EndTag) + { + if (path.size() > 0) + { + path.pop_back(); + } + else + { + break; + } + } + else if (node.Type == XmlNodeType::StartTag) + { + if (std::strcmp(node.Name, "StorageServiceProperties") == 0) + { + path.emplace_back(XmlTagName::k_StorageServiceProperties); + } + else if (std::strcmp(node.Name, "Logging") == 0) + { + path.emplace_back(XmlTagName::k_Logging); + } + else if (std::strcmp(node.Name, "HourMetrics") == 0) + { + path.emplace_back(XmlTagName::k_HourMetrics); + } + else if (std::strcmp(node.Name, "MinuteMetrics") == 0) + { + path.emplace_back(XmlTagName::k_MinuteMetrics); + } + else if (std::strcmp(node.Name, "Cors") == 0) + { + path.emplace_back(XmlTagName::k_Cors); + } + else if (std::strcmp(node.Name, "CorsRule") == 0) + { + path.emplace_back(XmlTagName::k_CorsRule); + } + else if (std::strcmp(node.Name, "DefaultServiceVersion") == 0) + { + path.emplace_back(XmlTagName::k_DefaultServiceVersion); + } + else if (std::strcmp(node.Name, "DeleteRetentionPolicy") == 0) + { + path.emplace_back(XmlTagName::k_DeleteRetentionPolicy); + } + else if (std::strcmp(node.Name, "StaticWebsite") == 0) + { + path.emplace_back(XmlTagName::k_StaticWebsite); + } + else + { + path.emplace_back(XmlTagName::k_Unknown); + } + if (path.size() == 2 && path[0] == XmlTagName::k_StorageServiceProperties + && path[1] == XmlTagName::k_Logging) + { + ret.Logging = BlobAnalyticsLoggingFromXml(reader); + path.pop_back(); + } + else if ( + path.size() == 2 && path[0] == XmlTagName::k_StorageServiceProperties + && path[1] == XmlTagName::k_HourMetrics) + { + ret.HourMetrics = BlobMetricsFromXml(reader); + path.pop_back(); + } + else if ( + path.size() == 2 && path[0] == XmlTagName::k_StorageServiceProperties + && path[1] == XmlTagName::k_MinuteMetrics) + { + ret.MinuteMetrics = BlobMetricsFromXml(reader); + path.pop_back(); + } + else if ( + path.size() == 3 && path[0] == XmlTagName::k_StorageServiceProperties + && path[1] == XmlTagName::k_Cors && path[2] == XmlTagName::k_CorsRule) + { + ret.Cors.emplace_back(BlobCorsRuleFromXml(reader)); + path.pop_back(); + } + else if ( + path.size() == 2 && path[0] == XmlTagName::k_StorageServiceProperties + && path[1] == XmlTagName::k_DeleteRetentionPolicy) + { + ret.DeleteRetentionPolicy = BlobRetentionPolicyFromXml(reader); + path.pop_back(); + } + else if ( + path.size() == 2 && path[0] == XmlTagName::k_StorageServiceProperties + && path[1] == XmlTagName::k_StaticWebsite) + { + ret.StaticWebsite = BlobStaticWebsiteFromXml(reader); + path.pop_back(); + } + } + else if (node.Type == XmlNodeType::Text) + { + if (path.size() == 2 && path[0] == XmlTagName::k_StorageServiceProperties + && path[1] == XmlTagName::k_DefaultServiceVersion) + { + ret.DefaultServiceVersion = node.Value; + } + } + } + return ret; + } + static ListContainersSegment ListContainersSegmentFromXml(XmlReader& reader) { ListContainersSegment ret; @@ -1321,6 +1592,92 @@ namespace Azure { namespace Storage { namespace Blobs { return ret; } + static BlobAnalyticsLogging BlobAnalyticsLoggingFromXml(XmlReader& reader) + { + BlobAnalyticsLogging ret; + enum class XmlTagName + { + k_Version, + k_Delete, + k_Read, + k_Write, + k_RetentionPolicy, + k_Unknown, + }; + std::vector path; + while (true) + { + auto node = reader.Read(); + if (node.Type == XmlNodeType::End) + { + break; + } + else if (node.Type == XmlNodeType::EndTag) + { + if (path.size() > 0) + { + path.pop_back(); + } + else + { + break; + } + } + else if (node.Type == XmlNodeType::StartTag) + { + if (std::strcmp(node.Name, "Version") == 0) + { + path.emplace_back(XmlTagName::k_Version); + } + else if (std::strcmp(node.Name, "Delete") == 0) + { + path.emplace_back(XmlTagName::k_Delete); + } + else if (std::strcmp(node.Name, "Read") == 0) + { + path.emplace_back(XmlTagName::k_Read); + } + else if (std::strcmp(node.Name, "Write") == 0) + { + path.emplace_back(XmlTagName::k_Write); + } + else if (std::strcmp(node.Name, "RetentionPolicy") == 0) + { + path.emplace_back(XmlTagName::k_RetentionPolicy); + } + else + { + path.emplace_back(XmlTagName::k_Unknown); + } + if (path.size() == 1 && path[0] == XmlTagName::k_RetentionPolicy) + { + ret.RetentionPolicy = BlobRetentionPolicyFromXml(reader); + path.pop_back(); + } + } + else if (node.Type == XmlNodeType::Text) + { + if (path.size() == 1 && path[0] == XmlTagName::k_Version) + { + ret.Version = node.Value; + } + else if (path.size() == 1 && path[0] == XmlTagName::k_Delete) + { + ret.Delete = std::strcmp(node.Value, "true") == 0; + } + else if (path.size() == 1 && path[0] == XmlTagName::k_Read) + { + ret.Read = std::strcmp(node.Value, "true") == 0; + } + else if (path.size() == 1 && path[0] == XmlTagName::k_Write) + { + ret.Write = std::strcmp(node.Value, "true") == 0; + } + } + } + return ret; + } + static BlobContainerItem BlobContainerItemFromXml(XmlReader& reader) { BlobContainerItem ret; @@ -1473,6 +1830,302 @@ namespace Azure { namespace Storage { namespace Blobs { return ret; } + static BlobCorsRule BlobCorsRuleFromXml(XmlReader& reader) + { + BlobCorsRule ret; + enum class XmlTagName + { + k_AllowedOrigins, + k_AllowedMethods, + k_MaxAgeInSeconds, + k_ExposedHeaders, + k_AllowedHeaders, + k_Unknown, + }; + std::vector path; + while (true) + { + auto node = reader.Read(); + if (node.Type == XmlNodeType::End) + { + break; + } + else if (node.Type == XmlNodeType::EndTag) + { + if (path.size() > 0) + { + path.pop_back(); + } + else + { + break; + } + } + else if (node.Type == XmlNodeType::StartTag) + { + if (std::strcmp(node.Name, "AllowedOrigins") == 0) + { + path.emplace_back(XmlTagName::k_AllowedOrigins); + } + else if (std::strcmp(node.Name, "AllowedMethods") == 0) + { + path.emplace_back(XmlTagName::k_AllowedMethods); + } + else if (std::strcmp(node.Name, "MaxAgeInSeconds") == 0) + { + path.emplace_back(XmlTagName::k_MaxAgeInSeconds); + } + else if (std::strcmp(node.Name, "ExposedHeaders") == 0) + { + path.emplace_back(XmlTagName::k_ExposedHeaders); + } + else if (std::strcmp(node.Name, "AllowedHeaders") == 0) + { + path.emplace_back(XmlTagName::k_AllowedHeaders); + } + else + { + path.emplace_back(XmlTagName::k_Unknown); + } + } + else if (node.Type == XmlNodeType::Text) + { + if (path.size() == 1 && path[0] == XmlTagName::k_AllowedOrigins) + { + ret.AllowedOrigins = node.Value; + } + else if (path.size() == 1 && path[0] == XmlTagName::k_AllowedMethods) + { + ret.AllowedMethods = node.Value; + } + else if (path.size() == 1 && path[0] == XmlTagName::k_MaxAgeInSeconds) + { + ret.MaxAgeInSeconds = std::stoi(node.Value); + } + else if (path.size() == 1 && path[0] == XmlTagName::k_ExposedHeaders) + { + ret.ExposedHeaders = node.Value; + } + else if (path.size() == 1 && path[0] == XmlTagName::k_AllowedHeaders) + { + ret.AllowedHeaders = node.Value; + } + } + } + return ret; + } + + static BlobMetrics BlobMetricsFromXml(XmlReader& reader) + { + BlobMetrics ret; + enum class XmlTagName + { + k_Version, + k_Enabled, + k_IncludeAPIs, + k_RetentionPolicy, + k_Unknown, + }; + std::vector path; + while (true) + { + auto node = reader.Read(); + if (node.Type == XmlNodeType::End) + { + break; + } + else if (node.Type == XmlNodeType::EndTag) + { + if (path.size() > 0) + { + path.pop_back(); + } + else + { + break; + } + } + else if (node.Type == XmlNodeType::StartTag) + { + if (std::strcmp(node.Name, "Version") == 0) + { + path.emplace_back(XmlTagName::k_Version); + } + else if (std::strcmp(node.Name, "Enabled") == 0) + { + path.emplace_back(XmlTagName::k_Enabled); + } + else if (std::strcmp(node.Name, "IncludeAPIs") == 0) + { + path.emplace_back(XmlTagName::k_IncludeAPIs); + } + else if (std::strcmp(node.Name, "RetentionPolicy") == 0) + { + path.emplace_back(XmlTagName::k_RetentionPolicy); + } + else + { + path.emplace_back(XmlTagName::k_Unknown); + } + if (path.size() == 1 && path[0] == XmlTagName::k_RetentionPolicy) + { + ret.RetentionPolicy = BlobRetentionPolicyFromXml(reader); + path.pop_back(); + } + } + else if (node.Type == XmlNodeType::Text) + { + if (path.size() == 1 && path[0] == XmlTagName::k_Version) + { + ret.Version = node.Value; + } + else if (path.size() == 1 && path[0] == XmlTagName::k_Enabled) + { + ret.Enabled = std::strcmp(node.Value, "true") == 0; + } + else if (path.size() == 1 && path[0] == XmlTagName::k_IncludeAPIs) + { + ret.IncludeApis = std::strcmp(node.Value, "true") == 0; + } + } + } + return ret; + } + + static BlobRetentionPolicy BlobRetentionPolicyFromXml(XmlReader& reader) + { + BlobRetentionPolicy ret; + enum class XmlTagName + { + k_Enabled, + k_Days, + k_Unknown, + }; + std::vector path; + while (true) + { + auto node = reader.Read(); + if (node.Type == XmlNodeType::End) + { + break; + } + else if (node.Type == XmlNodeType::EndTag) + { + if (path.size() > 0) + { + path.pop_back(); + } + else + { + break; + } + } + else if (node.Type == XmlNodeType::StartTag) + { + if (std::strcmp(node.Name, "Enabled") == 0) + { + path.emplace_back(XmlTagName::k_Enabled); + } + else if (std::strcmp(node.Name, "Days") == 0) + { + path.emplace_back(XmlTagName::k_Days); + } + else + { + path.emplace_back(XmlTagName::k_Unknown); + } + } + else if (node.Type == XmlNodeType::Text) + { + if (path.size() == 1 && path[0] == XmlTagName::k_Enabled) + { + ret.Enabled = std::strcmp(node.Value, "true") == 0; + } + else if (path.size() == 1 && path[0] == XmlTagName::k_Days) + { + ret.Days = std::stoi(node.Value); + } + } + } + return ret; + } + + static BlobStaticWebsite BlobStaticWebsiteFromXml(XmlReader& reader) + { + BlobStaticWebsite ret; + enum class XmlTagName + { + k_Enabled, + k_IndexDocument, + k_DefaultIndexDocumentPath, + k_ErrorDocument404Path, + k_Unknown, + }; + std::vector path; + while (true) + { + auto node = reader.Read(); + if (node.Type == XmlNodeType::End) + { + break; + } + else if (node.Type == XmlNodeType::EndTag) + { + if (path.size() > 0) + { + path.pop_back(); + } + else + { + break; + } + } + else if (node.Type == XmlNodeType::StartTag) + { + if (std::strcmp(node.Name, "Enabled") == 0) + { + path.emplace_back(XmlTagName::k_Enabled); + } + else if (std::strcmp(node.Name, "IndexDocument") == 0) + { + path.emplace_back(XmlTagName::k_IndexDocument); + } + else if (std::strcmp(node.Name, "DefaultIndexDocumentPath") == 0) + { + path.emplace_back(XmlTagName::k_DefaultIndexDocumentPath); + } + else if (std::strcmp(node.Name, "ErrorDocument404Path") == 0) + { + path.emplace_back(XmlTagName::k_ErrorDocument404Path); + } + else + { + path.emplace_back(XmlTagName::k_Unknown); + } + } + else if (node.Type == XmlNodeType::Text) + { + if (path.size() == 1 && path[0] == XmlTagName::k_Enabled) + { + ret.Enabled = std::strcmp(node.Value, "true") == 0; + } + else if (path.size() == 1 && path[0] == XmlTagName::k_IndexDocument) + { + ret.IndexDocument = node.Value; + } + else if (path.size() == 1 && path[0] == XmlTagName::k_DefaultIndexDocumentPath) + { + ret.DefaultIndexDocumentPath = node.Value; + } + else if (path.size() == 1 && path[0] == XmlTagName::k_ErrorDocument404Path) + { + ret.ErrorDocument404Path = node.Value; + } + } + } + return ret; + } + static std::map MetadataFromXml(XmlReader& reader) { std::map ret; @@ -1519,7 +2172,146 @@ namespace Azure { namespace Storage { namespace Blobs { writer.Write(XmlNode{XmlNodeType::Text, nullptr, options.ExpiresOn.data()}); writer.Write(XmlNode{XmlNodeType::EndTag}); writer.Write(XmlNode{XmlNodeType::EndTag}); - writer.Write(XmlNode{XmlNodeType::End}); + } + + static void SetPropertiesOptionsToXml(XmlWriter& writer, const SetPropertiesOptions& options) + { + writer.Write(XmlNode{XmlNodeType::StartTag, "StorageServiceProperties"}); + BlobServicePropertiesToXml(writer, options.Properties); + writer.Write(XmlNode{XmlNodeType::EndTag}); + } + + static void BlobServicePropertiesToXml( + XmlWriter& writer, + const BlobServiceProperties& options) + { + writer.Write(XmlNode{XmlNodeType::StartTag, "Logging"}); + BlobAnalyticsLoggingToXml(writer, options.Logging); + writer.Write(XmlNode{XmlNodeType::EndTag}); + writer.Write(XmlNode{XmlNodeType::StartTag, "HourMetrics"}); + BlobMetricsToXml(writer, options.HourMetrics); + writer.Write(XmlNode{XmlNodeType::EndTag}); + writer.Write(XmlNode{XmlNodeType::StartTag, "MinuteMetrics"}); + BlobMetricsToXml(writer, options.MinuteMetrics); + writer.Write(XmlNode{XmlNodeType::EndTag}); + writer.Write(XmlNode{XmlNodeType::StartTag, "Cors"}); + for (const auto& i : options.Cors) + { + BlobCorsRuleToXml(writer, i); + } + writer.Write(XmlNode{XmlNodeType::EndTag}); + writer.Write(XmlNode{XmlNodeType::StartTag, "DefaultServiceVersion"}); + writer.Write(XmlNode{XmlNodeType::Text, nullptr, options.DefaultServiceVersion.data()}); + writer.Write(XmlNode{XmlNodeType::EndTag}); + writer.Write(XmlNode{XmlNodeType::StartTag, "DeleteRetentionPolicy"}); + BlobRetentionPolicyToXml(writer, options.DeleteRetentionPolicy); + writer.Write(XmlNode{XmlNodeType::EndTag}); + writer.Write(XmlNode{XmlNodeType::StartTag, "StaticWebsite"}); + BlobStaticWebsiteToXml(writer, options.StaticWebsite); + writer.Write(XmlNode{XmlNodeType::EndTag}); + } + + static void BlobAnalyticsLoggingToXml(XmlWriter& writer, const BlobAnalyticsLogging& options) + { + writer.Write(XmlNode{XmlNodeType::StartTag, "Version"}); + writer.Write(XmlNode{XmlNodeType::Text, nullptr, options.Version.data()}); + writer.Write(XmlNode{XmlNodeType::EndTag}); + writer.Write(XmlNode{XmlNodeType::StartTag, "Delete"}); + writer.Write(XmlNode{XmlNodeType::Text, nullptr, options.Delete ? "true" : "false"}); + writer.Write(XmlNode{XmlNodeType::EndTag}); + writer.Write(XmlNode{XmlNodeType::StartTag, "Read"}); + writer.Write(XmlNode{XmlNodeType::Text, nullptr, options.Read ? "true" : "false"}); + writer.Write(XmlNode{XmlNodeType::EndTag}); + writer.Write(XmlNode{XmlNodeType::StartTag, "Write"}); + writer.Write(XmlNode{XmlNodeType::Text, nullptr, options.Write ? "true" : "false"}); + writer.Write(XmlNode{XmlNodeType::EndTag}); + writer.Write(XmlNode{XmlNodeType::StartTag, "RetentionPolicy"}); + BlobRetentionPolicyToXml(writer, options.RetentionPolicy); + writer.Write(XmlNode{XmlNodeType::EndTag}); + } + + static void BlobCorsRuleToXml(XmlWriter& writer, const BlobCorsRule& options) + { + writer.Write(XmlNode{XmlNodeType::StartTag, "CorsRule"}); + writer.Write(XmlNode{XmlNodeType::StartTag, "AllowedOrigins"}); + writer.Write(XmlNode{XmlNodeType::Text, nullptr, options.AllowedOrigins.data()}); + writer.Write(XmlNode{XmlNodeType::EndTag}); + writer.Write(XmlNode{XmlNodeType::StartTag, "AllowedMethods"}); + writer.Write(XmlNode{XmlNodeType::Text, nullptr, options.AllowedMethods.data()}); + writer.Write(XmlNode{XmlNodeType::EndTag}); + writer.Write(XmlNode{XmlNodeType::StartTag, "AllowedHeaders"}); + writer.Write(XmlNode{XmlNodeType::Text, nullptr, options.AllowedHeaders.data()}); + writer.Write(XmlNode{XmlNodeType::EndTag}); + writer.Write(XmlNode{XmlNodeType::StartTag, "ExposedHeaders"}); + writer.Write(XmlNode{XmlNodeType::Text, nullptr, options.ExposedHeaders.data()}); + writer.Write(XmlNode{XmlNodeType::EndTag}); + writer.Write(XmlNode{XmlNodeType::StartTag, "MaxAgeInSeconds"}); + writer.Write( + XmlNode{XmlNodeType::Text, nullptr, std::to_string(options.MaxAgeInSeconds).data()}); + writer.Write(XmlNode{XmlNodeType::EndTag}); + writer.Write(XmlNode{XmlNodeType::EndTag}); + } + + static void BlobMetricsToXml(XmlWriter& writer, const BlobMetrics& options) + { + writer.Write(XmlNode{XmlNodeType::StartTag, "Version"}); + writer.Write(XmlNode{XmlNodeType::Text, nullptr, options.Version.data()}); + writer.Write(XmlNode{XmlNodeType::EndTag}); + writer.Write(XmlNode{XmlNodeType::StartTag, "Enabled"}); + writer.Write(XmlNode{XmlNodeType::Text, nullptr, options.Enabled ? "true" : "false"}); + writer.Write(XmlNode{XmlNodeType::EndTag}); + if (options.IncludeApis.HasValue()) + { + writer.Write(XmlNode{XmlNodeType::StartTag, "IncludeAPIs"}); + writer.Write(XmlNode{ + XmlNodeType::Text, nullptr, options.IncludeApis.GetValue() ? "true" : "false"}); + writer.Write(XmlNode{XmlNodeType::EndTag}); + } + writer.Write(XmlNode{XmlNodeType::StartTag, "RetentionPolicy"}); + BlobRetentionPolicyToXml(writer, options.RetentionPolicy); + writer.Write(XmlNode{XmlNodeType::EndTag}); + } + + static void BlobRetentionPolicyToXml(XmlWriter& writer, const BlobRetentionPolicy& options) + { + writer.Write(XmlNode{XmlNodeType::StartTag, "Enabled"}); + writer.Write(XmlNode{XmlNodeType::Text, nullptr, options.Enabled ? "true" : "false"}); + writer.Write(XmlNode{XmlNodeType::EndTag}); + if (options.Days.HasValue()) + { + writer.Write(XmlNode{XmlNodeType::StartTag, "Days"}); + writer.Write( + XmlNode{XmlNodeType::Text, nullptr, std::to_string(options.Days.GetValue()).data()}); + writer.Write(XmlNode{XmlNodeType::EndTag}); + } + } + + static void BlobStaticWebsiteToXml(XmlWriter& writer, const BlobStaticWebsite& options) + { + writer.Write(XmlNode{XmlNodeType::StartTag, "Enabled"}); + writer.Write(XmlNode{XmlNodeType::Text, nullptr, options.Enabled ? "true" : "false"}); + writer.Write(XmlNode{XmlNodeType::EndTag}); + if (options.IndexDocument.HasValue()) + { + writer.Write(XmlNode{XmlNodeType::StartTag, "IndexDocument"}); + writer.Write( + XmlNode{XmlNodeType::Text, nullptr, options.IndexDocument.GetValue().data()}); + writer.Write(XmlNode{XmlNodeType::EndTag}); + } + if (options.DefaultIndexDocumentPath.HasValue()) + { + writer.Write(XmlNode{XmlNodeType::StartTag, "DefaultIndexDocumentPath"}); + writer.Write(XmlNode{ + XmlNodeType::Text, nullptr, options.DefaultIndexDocumentPath.GetValue().data()}); + writer.Write(XmlNode{XmlNodeType::EndTag}); + } + if (options.ErrorDocument404Path.HasValue()) + { + writer.Write(XmlNode{XmlNodeType::StartTag, "ErrorDocument404Path"}); + writer.Write( + XmlNode{XmlNodeType::Text, nullptr, options.ErrorDocument404Path.GetValue().data()}); + writer.Write(XmlNode{XmlNodeType::EndTag}); + } } }; // class Service @@ -2582,6 +3374,7 @@ namespace Azure { namespace Storage { namespace Blobs { { throw StorageError::CreateFromResponse(context, std::move(pHttpResponse)); } + response.BodyStream = httpResponse.GetBodyStream(); response.ETag = httpResponse.GetHeaders().at("etag"); response.LastModified = httpResponse.GetHeaders().at("last-modified"); auto response_content_md5_iterator = httpResponse.GetHeaders().find("content-md5"); @@ -2686,7 +3479,6 @@ namespace Azure { namespace Storage { namespace Blobs { = std::stoll(response_committed_block_count_iterator->second); } response.BlobType = BlobTypeFromString(httpResponse.GetHeaders().at("x-ms-blob-type")); - response.BodyStream = httpResponse.GetBodyStream(); return Azure::Core::Response( std::move(response), std::move(pHttpResponse)); } @@ -3881,6 +4673,7 @@ namespace Azure { namespace Storage { namespace Blobs { XmlWriter writer; CommitBlockListOptionsToXml(writer, options); xml_body = writer.GetDocument(); + writer.Write(XmlNode{XmlNodeType::End}); } Azure::Core::Http::MemoryBodyStream xml_body_stream( reinterpret_cast(xml_body.data()), xml_body.length()); @@ -4194,7 +4987,6 @@ namespace Azure { namespace Storage { namespace Blobs { XmlNode{XmlNodeType::StartTag, BlockTypeToString(i.first).data(), i.second.data()}); } writer.Write(XmlNode{XmlNodeType::EndTag}); - writer.Write(XmlNode{XmlNodeType::End}); } }; // class BlockBlob diff --git a/sdk/storage/src/blobs/blob_service_client.cpp b/sdk/storage/src/blobs/blob_service_client.cpp index 8d7df2749..408a61cf6 100644 --- a/sdk/storage/src/blobs/blob_service_client.cpp +++ b/sdk/storage/src/blobs/blob_service_client.cpp @@ -141,4 +141,22 @@ namespace Azure { namespace Storage { namespace Blobs { options.Context, *m_pipeline, m_serviceUrl.ToString(), protocolLayerOptions); } + Azure::Core::Response BlobServiceClient::SetProperties( + BlobServiceProperties properties, + const SetBlobServicePropertiesOptions& options) const + { + BlobRestClient::Service::SetPropertiesOptions protocolLayerOptions; + protocolLayerOptions.Properties = std::move(properties); + return BlobRestClient::Service::SetProperties( + options.Context, *m_pipeline, m_serviceUrl.ToString(), protocolLayerOptions); + } + + Azure::Core::Response BlobServiceClient::GetProperties( + const GetBlobServicePropertiesOptions& options) const + { + BlobRestClient::Service::GetPropertiesOptions protocolLayerOptions; + return BlobRestClient::Service::GetProperties( + options.Context, *m_pipeline, m_serviceUrl.ToString(), protocolLayerOptions); + } + }}} // namespace Azure::Storage::Blobs diff --git a/sdk/storage/test/blobs/blob_service_client_test.cpp b/sdk/storage/test/blobs/blob_service_client_test.cpp index 456b82a5f..b5e3086f1 100644 --- a/sdk/storage/test/blobs/blob_service_client_test.cpp +++ b/sdk/storage/test/blobs/blob_service_client_test.cpp @@ -4,6 +4,74 @@ #include "blobs/blob.hpp" #include "test_base.hpp" +#include + +namespace Azure { namespace Storage { namespace Blobs { + + bool operator==( + const Azure::Storage::Blobs::BlobRetentionPolicy& lhs, + const Azure::Storage::Blobs::BlobRetentionPolicy& rhs) + { + if (lhs.Enabled != rhs.Enabled) + { + return false; + } + if (lhs.Days.HasValue() != rhs.Days.HasValue()) + { + return false; + } + if (lhs.Days.HasValue() && rhs.Days.HasValue() && lhs.Days.GetValue() != rhs.Days.GetValue()) + { + return false; + } + return true; + } + + bool operator==( + const Azure::Storage::Blobs::BlobCorsRule& lhs, + const Azure::Storage::Blobs::BlobCorsRule& rhs) + { + return lhs.AllowedHeaders == rhs.AllowedHeaders && lhs.AllowedMethods == rhs.AllowedMethods + && lhs.AllowedOrigins == rhs.AllowedOrigins && lhs.ExposedHeaders == rhs.ExposedHeaders + && lhs.MaxAgeInSeconds == rhs.MaxAgeInSeconds; + } + + bool operator==( + const Azure::Storage::Blobs::BlobStaticWebsite& lhs, + const Azure::Storage::Blobs::BlobStaticWebsite& rhs) + { + if (lhs.Enabled != rhs.Enabled) + { + return false; + } + if (lhs.DefaultIndexDocumentPath.HasValue() != rhs.DefaultIndexDocumentPath.HasValue()) + { + return false; + } + if (lhs.DefaultIndexDocumentPath.HasValue() && rhs.DefaultIndexDocumentPath.HasValue() + && lhs.DefaultIndexDocumentPath.GetValue() != rhs.DefaultIndexDocumentPath.GetValue()) + { + return false; + } + if (lhs.DefaultIndexDocumentPath.HasValue() != rhs.DefaultIndexDocumentPath.HasValue()) + { + return false; + } + if (lhs.ErrorDocument404Path.HasValue() && rhs.ErrorDocument404Path.HasValue() + && lhs.ErrorDocument404Path.GetValue() != rhs.ErrorDocument404Path.GetValue()) + { + return false; + } + if (lhs.IndexDocument.HasValue() && rhs.IndexDocument.HasValue() + && lhs.IndexDocument.GetValue() != rhs.IndexDocument.GetValue()) + { + return false; + } + return true; + } + +}}} // namespace Azure::Storage::Blobs + namespace Azure { namespace Storage { namespace Test { class BlobServiceClientTest : public ::testing::Test { @@ -95,4 +163,138 @@ namespace Azure { namespace Storage { namespace Test { } } + TEST_F(BlobServiceClientTest, GetProperties) + { + auto ret = m_blobServiceClient.GetProperties(); + auto properties = *ret; + auto logging = properties.Logging; + EXPECT_FALSE(logging.Version.empty()); + if (logging.RetentionPolicy.Enabled) + { + EXPECT_TRUE(logging.RetentionPolicy.Days.HasValue()); + } + auto hourMetrics = properties.HourMetrics; + if (hourMetrics.Enabled) + { + EXPECT_FALSE(hourMetrics.Version.empty()); + if (hourMetrics.RetentionPolicy.Enabled) + { + EXPECT_TRUE(hourMetrics.RetentionPolicy.Days.HasValue()); + } + } + auto minuteMetrics = properties.HourMetrics; + if (minuteMetrics.Enabled) + { + EXPECT_FALSE(minuteMetrics.Version.empty()); + if (minuteMetrics.RetentionPolicy.Enabled) + { + EXPECT_TRUE(minuteMetrics.RetentionPolicy.Days.HasValue()); + } + } + EXPECT_FALSE(properties.DefaultServiceVersion.empty()); + auto deleteRetentionPolicy = properties.DeleteRetentionPolicy; + if (deleteRetentionPolicy.Enabled) + { + EXPECT_TRUE(deleteRetentionPolicy.Days.HasValue()); + } + } + + TEST_F(BlobServiceClientTest, DISABLED_SetProperties) + { + Blobs::BlobServiceProperties properties = *m_blobServiceClient.GetProperties(); + auto originalProperties = properties; + + properties.Logging.Delete = !properties.Logging.Delete; + properties.Logging.Read = !properties.Logging.Read; + properties.Logging.Write = !properties.Logging.Write; + properties.Logging.RetentionPolicy.Enabled = true; + properties.Logging.RetentionPolicy.Days = 3; + + properties.HourMetrics.Enabled = true; + properties.HourMetrics.RetentionPolicy.Enabled = true; + properties.HourMetrics.RetentionPolicy.Days = 4; + properties.HourMetrics.IncludeApis = true; + + properties.MinuteMetrics.Enabled = true; + properties.MinuteMetrics.RetentionPolicy.Enabled = true; + properties.MinuteMetrics.RetentionPolicy.Days = 4; + properties.MinuteMetrics.IncludeApis = true; + + properties.DefaultServiceVersion = Blobs::c_APIVersion; + + properties.StaticWebsite.Enabled = true; + properties.StaticWebsite.IndexDocument = "index.html"; + properties.StaticWebsite.ErrorDocument404Path = "404.html"; + properties.StaticWebsite.DefaultIndexDocumentPath.Reset(); + + Blobs::BlobCorsRule corsRule; + corsRule.AllowedOrigins = "http://www.example1.com"; + corsRule.AllowedMethods = "GET,PUT"; + corsRule.AllowedHeaders = "x-ms-header1,x-ms-header2"; + corsRule.ExposedHeaders = "x-ms-header3"; + corsRule.MaxAgeInSeconds = 10; + properties.Cors.emplace_back(corsRule); + + corsRule.AllowedOrigins = "http://www.example2.com"; + corsRule.AllowedMethods = "DELETE"; + corsRule.AllowedHeaders = "x-ms-header1"; + corsRule.ExposedHeaders = "x-ms-header2,x-ms-header3"; + corsRule.MaxAgeInSeconds = 20; + properties.Cors.emplace_back(corsRule); + + properties.DeleteRetentionPolicy.Enabled = true; + properties.DeleteRetentionPolicy.Days = 5; + + EXPECT_NO_THROW(m_blobServiceClient.SetProperties(properties)); + // It takes some time before the new properties comes into effect. + using namespace std::chrono_literals; + std::this_thread::sleep_for(10s); + auto downloadedProperties = *m_blobServiceClient.GetProperties(); + EXPECT_EQ(downloadedProperties.Logging.Version, properties.Logging.Version); + EXPECT_EQ(downloadedProperties.Logging.Delete, properties.Logging.Delete); + EXPECT_EQ(downloadedProperties.Logging.Read, properties.Logging.Read); + EXPECT_EQ(downloadedProperties.Logging.Write, properties.Logging.Write); + EXPECT_EQ(downloadedProperties.Logging.RetentionPolicy, properties.Logging.RetentionPolicy); + + EXPECT_EQ(downloadedProperties.HourMetrics.Version, properties.HourMetrics.Version); + EXPECT_EQ(downloadedProperties.HourMetrics.Enabled, properties.HourMetrics.Enabled); + EXPECT_EQ( + downloadedProperties.HourMetrics.IncludeApis.HasValue(), + properties.HourMetrics.IncludeApis.HasValue()); + if (downloadedProperties.HourMetrics.IncludeApis.HasValue() + == properties.HourMetrics.IncludeApis.HasValue()) + { + EXPECT_EQ( + downloadedProperties.HourMetrics.IncludeApis.GetValue(), + properties.HourMetrics.IncludeApis.GetValue()); + } + EXPECT_EQ( + downloadedProperties.HourMetrics.RetentionPolicy, properties.HourMetrics.RetentionPolicy); + + EXPECT_EQ(downloadedProperties.MinuteMetrics.Version, properties.MinuteMetrics.Version); + EXPECT_EQ(downloadedProperties.MinuteMetrics.Enabled, properties.MinuteMetrics.Enabled); + EXPECT_EQ( + downloadedProperties.MinuteMetrics.IncludeApis.HasValue(), + properties.MinuteMetrics.IncludeApis.HasValue()); + if (downloadedProperties.MinuteMetrics.IncludeApis.HasValue() + == properties.MinuteMetrics.IncludeApis.HasValue()) + { + EXPECT_EQ( + downloadedProperties.MinuteMetrics.IncludeApis.GetValue(), + properties.MinuteMetrics.IncludeApis.GetValue()); + } + EXPECT_EQ( + downloadedProperties.MinuteMetrics.RetentionPolicy, + properties.MinuteMetrics.RetentionPolicy); + + EXPECT_EQ(downloadedProperties.DefaultServiceVersion, properties.DefaultServiceVersion); + EXPECT_EQ(downloadedProperties.Cors, properties.Cors); + + EXPECT_EQ(downloadedProperties.StaticWebsite, properties.StaticWebsite); + + EXPECT_EQ(downloadedProperties.DeleteRetentionPolicy, properties.DeleteRetentionPolicy); + + m_blobServiceClient.SetProperties(originalProperties); + } + }}} // namespace Azure::Storage::Test