diff --git a/sdk/storage/azure_storage_blob/CHANGELOG.md b/sdk/storage/azure_storage_blob/CHANGELOG.md index d5884c6195b..7276e8151ac 100644 --- a/sdk/storage/azure_storage_blob/CHANGELOG.md +++ b/sdk/storage/azure_storage_blob/CHANGELOG.md @@ -4,8 +4,14 @@ ### Features Added +- Updated the default service version to `2026-10-06`. +- The service-calculated CRC64 is now surfaced as `content_crc64` on upload responses, alongside `content_md5`, when a content MD5 is provided with the request. This applies to `stage_block`, `stage_block_from_url`, `upload_pages`, `upload_pages_from_url`, `append_block`, `append_block_from_url`, `upload` and `upload_blob_from_url`. + ### Breaking Changes +- Added the `AccessTier::Smart` and `ArchiveStatus::RehydratePendingToSmart` enum variants. +- Added `access_tier`, `access_tier_changed_on`, `access_tier_inferred`, and `smart_access_tier` to `BlobDownloadProperties` and marked the struct as non-exhaustive. + ### Bugs Fixed ### Other Changes diff --git a/sdk/storage/azure_storage_blob/Cargo.toml b/sdk/storage/azure_storage_blob/Cargo.toml index bdc8b1d718a..de8fade483b 100644 --- a/sdk/storage/azure_storage_blob/Cargo.toml +++ b/sdk/storage/azure_storage_blob/Cargo.toml @@ -55,6 +55,9 @@ tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] } [package.metadata.docs.rs] all-features = true +[package.metadata.cargo-semver-checks.lints] +enum_variant_added = "allow" + [[bench]] name = "perf" path = "perf/perf_tests.rs" diff --git a/sdk/storage/azure_storage_blob/assets.json b/sdk/storage/azure_storage_blob/assets.json index 0c82784dd24..78bf634d994 100644 --- a/sdk/storage/azure_storage_blob/assets.json +++ b/sdk/storage/azure_storage_blob/assets.json @@ -1,6 +1,6 @@ { "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "rust", - "Tag": "rust/azure_storage_blob_eecd071a79", + "Tag": "rust/azure_storage_blob_6870e8ba42", "TagPrefix": "rust/azure_storage_blob" } diff --git a/sdk/storage/azure_storage_blob/src/generated/clients/append_blob_client.rs b/sdk/storage/azure_storage_blob/src/generated/clients/append_blob_client.rs index 8ed37265897..29e4247558b 100644 --- a/sdk/storage/azure_storage_blob/src/generated/clients/append_blob_client.rs +++ b/sdk/storage/azure_storage_blob/src/generated/clients/append_blob_client.rs @@ -581,7 +581,7 @@ impl AppendBlobClient { } /// Default value for [`AppendBlobClientOptions::version`]. -pub(crate) const DEFAULT_VERSION: &str = "2026-04-06"; +pub(crate) const DEFAULT_VERSION: &str = "2026-10-06"; impl Default for AppendBlobClientOptions { fn default() -> Self { diff --git a/sdk/storage/azure_storage_blob/src/generated/clients/blob_client.rs b/sdk/storage/azure_storage_blob/src/generated/clients/blob_client.rs index f99b3d04f6a..e6191f14ecc 100644 --- a/sdk/storage/azure_storage_blob/src/generated/clients/blob_client.rs +++ b/sdk/storage/azure_storage_blob/src/generated/clients/blob_client.rs @@ -584,6 +584,9 @@ impl BlobClient { /// * [`content_range`()](crate::generated::models::BlobClientDownloadInternalResultHeaders::content_range) - content-range /// * [`etag`()](crate::generated::models::BlobClientDownloadInternalResultHeaders::etag) - etag /// * [`last_modified`()](crate::generated::models::BlobClientDownloadInternalResultHeaders::last_modified) - last-modified + /// * [`access_tier`()](crate::generated::models::BlobClientDownloadInternalResultHeaders::access_tier) - x-ms-access-tier + /// * [`access_tier_change_time`()](crate::generated::models::BlobClientDownloadInternalResultHeaders::access_tier_change_time) - x-ms-access-tier-change-time + /// * [`access_tier_inferred`()](crate::generated::models::BlobClientDownloadInternalResultHeaders::access_tier_inferred) - x-ms-access-tier-inferred /// * [`blob_committed_block_count`()](crate::generated::models::BlobClientDownloadInternalResultHeaders::blob_committed_block_count) - x-ms-blob-committed-block-count /// * [`blob_content_md5`()](crate::generated::models::BlobClientDownloadInternalResultHeaders::blob_content_md5) - x-ms-blob-content-md5 /// * [`is_sealed`()](crate::generated::models::BlobClientDownloadInternalResultHeaders::is_sealed) - x-ms-blob-sealed @@ -611,6 +614,7 @@ impl BlobClient { /// * [`object_replication_rules`()](crate::generated::models::BlobClientDownloadInternalResultHeaders::object_replication_rules) - x-ms-or /// * [`object_replication_policy_id`()](crate::generated::models::BlobClientDownloadInternalResultHeaders::object_replication_policy_id) - x-ms-or-policy-id /// * [`is_server_encrypted`()](crate::generated::models::BlobClientDownloadInternalResultHeaders::is_server_encrypted) - x-ms-server-encrypted + /// * [`smart_access_tier`()](crate::generated::models::BlobClientDownloadInternalResultHeaders::smart_access_tier) - x-ms-smart-access-tier /// * [`tag_count`()](crate::generated::models::BlobClientDownloadInternalResultHeaders::tag_count) - x-ms-tag-count /// * [`version_id`()](crate::generated::models::BlobClientDownloadInternalResultHeaders::version_id) - x-ms-version-id /// @@ -845,6 +849,7 @@ impl BlobClient { /// * [`object_replication_policy_id`()](crate::generated::models::BlobClientGetPropertiesResultHeaders::object_replication_policy_id) - x-ms-or-policy-id /// * [`rehydrate_priority`()](crate::generated::models::BlobClientGetPropertiesResultHeaders::rehydrate_priority) - x-ms-rehydrate-priority /// * [`is_server_encrypted`()](crate::generated::models::BlobClientGetPropertiesResultHeaders::is_server_encrypted) - x-ms-server-encrypted + /// * [`smart_access_tier`()](crate::generated::models::BlobClientGetPropertiesResultHeaders::smart_access_tier) - x-ms-smart-access-tier /// * [`tag_count`()](crate::generated::models::BlobClientGetPropertiesResultHeaders::tag_count) - x-ms-tag-count /// * [`version_id`()](crate::generated::models::BlobClientGetPropertiesResultHeaders::version_id) - x-ms-version-id /// @@ -1574,7 +1579,7 @@ impl BlobClient { } /// Default value for [`BlobClientOptions::version`]. -pub(crate) const DEFAULT_VERSION: &str = "2026-04-06"; +pub(crate) const DEFAULT_VERSION: &str = "2026-10-06"; impl Default for BlobClientOptions { fn default() -> Self { diff --git a/sdk/storage/azure_storage_blob/src/generated/clients/blob_container_client.rs b/sdk/storage/azure_storage_blob/src/generated/clients/blob_container_client.rs index 27b8df7b071..70e7107b4f6 100644 --- a/sdk/storage/azure_storage_blob/src/generated/clients/blob_container_client.rs +++ b/sdk/storage/azure_storage_blob/src/generated/clients/blob_container_client.rs @@ -1158,7 +1158,7 @@ impl BlobContainerClient { } /// Default value for [`BlobContainerClientOptions::version`]. -pub(crate) const DEFAULT_VERSION: &str = "2026-04-06"; +pub(crate) const DEFAULT_VERSION: &str = "2026-10-06"; impl Default for BlobContainerClientOptions { fn default() -> Self { diff --git a/sdk/storage/azure_storage_blob/src/generated/clients/blob_service_client.rs b/sdk/storage/azure_storage_blob/src/generated/clients/blob_service_client.rs index 81ac1357d8e..f69d1c595c9 100644 --- a/sdk/storage/azure_storage_blob/src/generated/clients/blob_service_client.rs +++ b/sdk/storage/azure_storage_blob/src/generated/clients/blob_service_client.rs @@ -454,7 +454,7 @@ impl BlobServiceClient { } /// Default value for [`BlobServiceClientOptions::version`]. -pub(crate) const DEFAULT_VERSION: &str = "2026-04-06"; +pub(crate) const DEFAULT_VERSION: &str = "2026-10-06"; impl Default for BlobServiceClientOptions { fn default() -> Self { diff --git a/sdk/storage/azure_storage_blob/src/generated/clients/block_blob_client.rs b/sdk/storage/azure_storage_blob/src/generated/clients/block_blob_client.rs index 08a85830528..8140b0689d8 100644 --- a/sdk/storage/azure_storage_blob/src/generated/clients/block_blob_client.rs +++ b/sdk/storage/azure_storage_blob/src/generated/clients/block_blob_client.rs @@ -587,6 +587,7 @@ impl BlockBlobClient { /// * [`content_md5`()](crate::generated::models::BlockBlobClientUploadBlobFromUrlResultHeaders::content_md5) - content-md5 /// * [`etag`()](crate::generated::models::BlockBlobClientUploadBlobFromUrlResultHeaders::etag) - etag /// * [`last_modified`()](crate::generated::models::BlockBlobClientUploadBlobFromUrlResultHeaders::last_modified) - last-modified + /// * [`content_crc64`()](crate::generated::models::BlockBlobClientUploadBlobFromUrlResultHeaders::content_crc64) - x-ms-content-crc64 /// * [`encryption_key_sha256`()](crate::generated::models::BlockBlobClientUploadBlobFromUrlResultHeaders::encryption_key_sha256) - x-ms-encryption-key-sha256 /// * [`encryption_scope`()](crate::generated::models::BlockBlobClientUploadBlobFromUrlResultHeaders::encryption_scope) - x-ms-encryption-scope /// * [`is_server_encrypted`()](crate::generated::models::BlockBlobClientUploadBlobFromUrlResultHeaders::is_server_encrypted) - x-ms-request-server-encrypted @@ -931,7 +932,7 @@ impl BlockBlobClient { } /// Default value for [`BlockBlobClientOptions::version`]. -pub(crate) const DEFAULT_VERSION: &str = "2026-04-06"; +pub(crate) const DEFAULT_VERSION: &str = "2026-10-06"; impl Default for BlockBlobClientOptions { fn default() -> Self { diff --git a/sdk/storage/azure_storage_blob/src/generated/clients/page_blob_client.rs b/sdk/storage/azure_storage_blob/src/generated/clients/page_blob_client.rs index b6cae2d72b1..0b5bccc4ff2 100644 --- a/sdk/storage/azure_storage_blob/src/generated/clients/page_blob_client.rs +++ b/sdk/storage/azure_storage_blob/src/generated/clients/page_blob_client.rs @@ -1005,7 +1005,7 @@ impl PageBlobClient { } /// Default value for [`PageBlobClientOptions::version`]. -pub(crate) const DEFAULT_VERSION: &str = "2026-04-06"; +pub(crate) const DEFAULT_VERSION: &str = "2026-10-06"; impl Default for PageBlobClientOptions { fn default() -> Self { diff --git a/sdk/storage/azure_storage_blob/src/generated/models/enums.rs b/sdk/storage/azure_storage_blob/src/generated/models/enums.rs index bfcba38e578..fa368fc1ac2 100644 --- a/sdk/storage/azure_storage_blob/src/generated/models/enums.rs +++ b/sdk/storage/azure_storage_blob/src/generated/models/enums.rs @@ -54,6 +54,9 @@ pub enum AccessTier { /// The Premium access tier. Premium, + /// The Smart access tier. + Smart, + /// Any other value not defined in `AccessTier`. UnknownValue(String), } @@ -89,6 +92,9 @@ pub enum ArchiveStatus { /// The archive status is rehydrating pending to Hot. RehydratePendingToHot, + /// The archive status is rehydrating pending to Smart. + RehydratePendingToSmart, + /// Any other value not defined in `ArchiveStatus`. UnknownValue(String), } diff --git a/sdk/storage/azure_storage_blob/src/generated/models/enums_impl.rs b/sdk/storage/azure_storage_blob/src/generated/models/enums_impl.rs index 31a2b6cf273..0ee7be028d0 100644 --- a/sdk/storage/azure_storage_blob/src/generated/models/enums_impl.rs +++ b/sdk/storage/azure_storage_blob/src/generated/models/enums_impl.rs @@ -37,6 +37,7 @@ impl<'a> From<&'a AccessTier> for &'a str { AccessTier::P70 => "P70", AccessTier::P80 => "P80", AccessTier::Premium => "Premium", + AccessTier::Smart => "Smart", AccessTier::UnknownValue(s) => s.as_ref(), } } @@ -62,6 +63,7 @@ impl FromStr for AccessTier { "P70" => AccessTier::P70, "P80" => AccessTier::P80, "Premium" => AccessTier::Premium, + "Smart" => AccessTier::Smart, _ => AccessTier::UnknownValue(s.to_string()), }) } @@ -86,6 +88,7 @@ impl AsRef for AccessTier { AccessTier::P70 => "P70", AccessTier::P80 => "P80", AccessTier::Premium => "Premium", + AccessTier::Smart => "Smart", AccessTier::UnknownValue(s) => s.as_str(), } } @@ -110,6 +113,7 @@ impl Display for AccessTier { AccessTier::P70 => f.write_str("P70"), AccessTier::P80 => f.write_str("P80"), AccessTier::Premium => f.write_str("Premium"), + AccessTier::Smart => f.write_str("Smart"), AccessTier::UnknownValue(s) => f.write_str(s.as_str()), } } @@ -163,6 +167,7 @@ impl<'a> From<&'a ArchiveStatus> for &'a str { ArchiveStatus::RehydratePendingToCold => "rehydrate-pending-to-cold", ArchiveStatus::RehydratePendingToCool => "rehydrate-pending-to-cool", ArchiveStatus::RehydratePendingToHot => "rehydrate-pending-to-hot", + ArchiveStatus::RehydratePendingToSmart => "rehydrate-pending-to-smart", ArchiveStatus::UnknownValue(s) => s.as_ref(), } } @@ -175,6 +180,7 @@ impl FromStr for ArchiveStatus { "rehydrate-pending-to-cold" => ArchiveStatus::RehydratePendingToCold, "rehydrate-pending-to-cool" => ArchiveStatus::RehydratePendingToCool, "rehydrate-pending-to-hot" => ArchiveStatus::RehydratePendingToHot, + "rehydrate-pending-to-smart" => ArchiveStatus::RehydratePendingToSmart, _ => ArchiveStatus::UnknownValue(s.to_string()), }) } @@ -186,6 +192,7 @@ impl AsRef for ArchiveStatus { ArchiveStatus::RehydratePendingToCold => "rehydrate-pending-to-cold", ArchiveStatus::RehydratePendingToCool => "rehydrate-pending-to-cool", ArchiveStatus::RehydratePendingToHot => "rehydrate-pending-to-hot", + ArchiveStatus::RehydratePendingToSmart => "rehydrate-pending-to-smart", ArchiveStatus::UnknownValue(s) => s.as_str(), } } @@ -197,6 +204,7 @@ impl Display for ArchiveStatus { ArchiveStatus::RehydratePendingToCold => f.write_str("rehydrate-pending-to-cold"), ArchiveStatus::RehydratePendingToCool => f.write_str("rehydrate-pending-to-cool"), ArchiveStatus::RehydratePendingToHot => f.write_str("rehydrate-pending-to-hot"), + ArchiveStatus::RehydratePendingToSmart => f.write_str("rehydrate-pending-to-smart"), ArchiveStatus::UnknownValue(s) => f.write_str(s.as_str()), } } diff --git a/sdk/storage/azure_storage_blob/src/generated/models/header_traits.rs b/sdk/storage/azure_storage_blob/src/generated/models/header_traits.rs index e00b24232e6..ac7ce0a6e2d 100644 --- a/sdk/storage/azure_storage_blob/src/generated/models/header_traits.rs +++ b/sdk/storage/azure_storage_blob/src/generated/models/header_traits.rs @@ -100,6 +100,7 @@ const REQUEST_SERVER_ENCRYPTED: HeaderName = HeaderName::from_static("x-ms-request-server-encrypted"); const SERVER_ENCRYPTED: HeaderName = HeaderName::from_static("x-ms-server-encrypted"); const SKU_NAME: HeaderName = HeaderName::from_static("x-ms-sku-name"); +const SMART_ACCESS_TIER: HeaderName = HeaderName::from_static("x-ms-smart-access-tier"); const SNAPSHOT: HeaderName = HeaderName::from_static("x-ms-snapshot"); const TAG_COUNT: HeaderName = HeaderName::from_static("x-ms-tag-count"); const VERSION_ID: HeaderName = HeaderName::from_static("x-ms-version-id"); @@ -628,6 +629,9 @@ pub(crate) trait BlobClientDownloadInternalResultHeaders: private::Sealed { fn content_range(&self) -> Result>; fn etag(&self) -> Result>; fn last_modified(&self) -> Result>; + fn access_tier(&self) -> Result>; + fn access_tier_change_time(&self) -> Result>; + fn access_tier_inferred(&self) -> Result>; fn blob_committed_block_count(&self) -> Result>; fn blob_content_md5(&self) -> Result>>; fn is_sealed(&self) -> Result>; @@ -655,6 +659,7 @@ pub(crate) trait BlobClientDownloadInternalResultHeaders: private::Sealed { fn object_replication_rules(&self) -> Result>; fn object_replication_policy_id(&self) -> Result>; fn is_server_encrypted(&self) -> Result>; + fn smart_access_tier(&self) -> Result>; fn tag_count(&self) -> Result>; fn version_id(&self) -> Result>; } @@ -707,6 +712,23 @@ impl BlobClientDownloadInternalResultHeaders for AsyncResponse Result> { + Headers::get_optional_as(self.headers(), &ACCESS_TIER) + } + + /// The time the tier was changed on the blob. This is only returned if the tier on the blob was ever set. + fn access_tier_change_time(&self) -> Result> { + Headers::get_optional_with(self.headers(), &ACCESS_TIER_CHANGE_TIME, |h| { + parse_rfc7231(h.as_str()) + }) + } + + /// Included if the access tier is inferred. + fn access_tier_inferred(&self) -> Result> { + Headers::get_optional_as(self.headers(), &ACCESS_TIER_INFERRED) + } + /// The number of committed blocks present in the blob. fn blob_committed_block_count(&self) -> Result> { Headers::get_optional_as(self.headers(), &BLOB_COMMITTED_BLOCK_COUNT) @@ -869,6 +891,11 @@ impl BlobClientDownloadInternalResultHeaders for AsyncResponse Result> { + Headers::get_optional_as(self.headers(), &SMART_ACCESS_TIER) + } + /// The number of tags associated with the blob. fn tag_count(&self) -> Result> { Headers::get_optional_as(self.headers(), &TAG_COUNT) @@ -990,6 +1017,7 @@ pub trait BlobClientGetPropertiesResultHeaders: private::Sealed { fn object_replication_policy_id(&self) -> Result>; fn rehydrate_priority(&self) -> Result>; fn is_server_encrypted(&self) -> Result>; + fn smart_access_tier(&self) -> Result>; fn tag_count(&self) -> Result>; fn version_id(&self) -> Result>; } @@ -1231,6 +1259,11 @@ impl BlobClientGetPropertiesResultHeaders for Response Result> { + Headers::get_optional_as(self.headers(), &SMART_ACCESS_TIER) + } + /// The number of tags associated with the blob. fn tag_count(&self) -> Result> { Headers::get_optional_as(self.headers(), &TAG_COUNT) @@ -1996,6 +2029,7 @@ pub trait BlockBlobClientUploadBlobFromUrlResultHeaders: private::Sealed { fn content_md5(&self) -> Result>>; fn etag(&self) -> Result>; fn last_modified(&self) -> Result>; + fn content_crc64(&self) -> Result>>; fn encryption_key_sha256(&self) -> Result>; fn encryption_scope(&self) -> Result>; fn is_server_encrypted(&self) -> Result>; @@ -2022,6 +2056,13 @@ impl BlockBlobClientUploadBlobFromUrlResultHeaders }) } + /// The CRC64 hash of the content. + fn content_crc64(&self) -> Result>> { + Headers::get_optional_with(self.headers(), &CONTENT_CRC64, |h| { + base64::decode(h.as_str()) + }) + } + /// The SHA-256 hash of the provided encryption key. fn encryption_key_sha256(&self) -> Result> { Headers::get_optional_as(self.headers(), &ENCRYPTION_KEY_SHA256) diff --git a/sdk/storage/azure_storage_blob/src/generated/models/models.rs b/sdk/storage/azure_storage_blob/src/generated/models/models.rs index 44557b18ef0..6acb6809679 100644 --- a/sdk/storage/azure_storage_blob/src/generated/models/models.rs +++ b/sdk/storage/azure_storage_blob/src/generated/models/models.rs @@ -441,6 +441,10 @@ pub struct BlobProperties { #[serde(rename = "ServerEncrypted", skip_serializing_if = "Option::is_none")] pub server_encrypted: Option, + /// The smart access tier of the blob. + #[serde(rename = "SmartAccessTier", skip_serializing_if = "Option::is_none")] + pub smart_access_tier: Option, + /// The number of tags for the blob. #[serde(rename = "TagCount", skip_serializing_if = "Option::is_none")] pub tag_count: Option, diff --git a/sdk/storage/azure_storage_blob/src/logging.rs b/sdk/storage/azure_storage_blob/src/logging.rs index 5c7752040fb..b6a77c9a5c7 100644 --- a/sdk/storage/azure_storage_blob/src/logging.rs +++ b/sdk/storage/azure_storage_blob/src/logging.rs @@ -59,6 +59,7 @@ pub static STORAGE_ALLOWED_HEADERS: &[&str] = &[ "x-ms-access-tier", "x-ms-access-tier-change-time", "x-ms-access-tier-inferred", + "x-ms-smart-access-tier", "x-ms-archive-status", "x-ms-rehydrate-priority", // Blob metadata diff --git a/sdk/storage/azure_storage_blob/src/models/download_result.rs b/sdk/storage/azure_storage_blob/src/models/download_result.rs index afb7ae39517..92abda1aa93 100644 --- a/sdk/storage/azure_storage_blob/src/models/download_result.rs +++ b/sdk/storage/azure_storage_blob/src/models/download_result.rs @@ -15,7 +15,8 @@ use std::collections::HashMap; use time::OffsetDateTime; use crate::generated::models::{ - BlobType, CopyStatus, ImmutabilityPolicyMode, LeaseDuration, LeaseState, LeaseStatus, + AccessTier, BlobType, CopyStatus, ImmutabilityPolicyMode, LeaseDuration, LeaseState, + LeaseStatus, }; /// Result of a `BlobClient::download()` operation. @@ -56,6 +57,7 @@ pub struct BlobClientDownloadIntoResult { /// Blob properties parsed from the initial response headers of a `BlobClient::download()` operation. #[derive(SafeDebug)] +#[non_exhaustive] pub struct BlobDownloadProperties { /// The blob's ETag (`ETag` header). pub etag: Option, @@ -180,6 +182,23 @@ pub struct BlobDownloadProperties { /// Base64-encoded SHA-256 hash of the customer-provided encryption key /// used to encrypt the blob (`x-ms-encryption-key-sha256` header). pub encryption_key_sha256: Option, + + /// The access tier of the blob (`x-ms-access-tier` header). + pub access_tier: Option, + + /// Date/time the access tier was last changed (`x-ms-access-tier-change-time` header). + /// + /// Only returned if the tier on the blob was ever set. + pub access_tier_changed_on: Option, + + /// Whether the access tier was inferred rather than explicitly set + /// (`x-ms-access-tier-inferred` header). + pub access_tier_inferred: Option, + + /// The underlying tier of a smart tier blob (`x-ms-smart-access-tier` header). + /// + /// Only returned if the blob is in the Smart tier. + pub smart_access_tier: Option, } impl BlobClientDownloadResult { @@ -270,6 +289,15 @@ impl BlobDownloadProperties { .get_optional_as(&HeaderName::from_static("x-ms-encryption-scope"))?, encryption_key_sha256: headers .get_optional_as(&HeaderName::from_static("x-ms-encryption-key-sha256"))?, + access_tier: headers.get_optional_as(&HeaderName::from_static("x-ms-access-tier"))?, + access_tier_changed_on: headers.get_optional_with( + &HeaderName::from_static("x-ms-access-tier-change-time"), + |h| parse_rfc7231(h.as_str()), + )?, + access_tier_inferred: headers + .get_optional_as(&HeaderName::from_static("x-ms-access-tier-inferred"))?, + smart_access_tier: headers + .get_optional_as(&HeaderName::from_static("x-ms-smart-access-tier"))?, metadata, object_replication_rules, }) diff --git a/sdk/storage/azure_storage_blob/tests/append_blob_client.rs b/sdk/storage/azure_storage_blob/tests/append_blob_client.rs index b451c4397ab..be04a7b3f09 100644 --- a/sdk/storage/azure_storage_blob/tests/append_blob_client.rs +++ b/sdk/storage/azure_storage_blob/tests/append_blob_client.rs @@ -6,7 +6,8 @@ mod common; use azure_core::http::{headers::CONTENT_TYPE, RequestContent, StatusCode}; use azure_core_test::{recorded, TestContext}; use azure_storage_blob::models::{ - AppendBlobClientAppendBlockFromUrlOptions, AppendBlobClientAppendBlockOptions, + AppendBlobClientAppendBlockFromUrlOptions, AppendBlobClientAppendBlockFromUrlResultHeaders, + AppendBlobClientAppendBlockOptions, AppendBlobClientAppendBlockResultHeaders, AppendBlobClientCreateOptions, BlobClientGetPropertiesResultHeaders, BlobType, }; use common::{create_test_blob, get_blob_name, get_container_client, StorageAccount}; @@ -87,16 +88,49 @@ async fn test_append_block_from_url(ctx: TestContext) -> Result<(), Box = vec![ + 0x5d, 0x41, 0x40, 0x2a, 0xbc, 0x4b, 0x2a, 0x76, 0xb9, 0x71, 0x9d, 0x91, 0x10, 0x17, 0xc5, + 0x92, + ]; + let md5_append_blob_client = container_client + .blob_client(&get_blob_name(recording)) + .append_blob_client(); + md5_append_blob_client.create(None).await?; + let combined_response = md5_append_blob_client + .append_block_from_url( + md5_source_blob_client.url().as_str().into(), + 5, + Some(AppendBlobClientAppendBlockFromUrlOptions { + source_content_md5: Some(source_md5), + ..Default::default() + }), + ) + .await?; + assert!(combined_response.content_md5()?.is_some()); + assert!(combined_response.content_crc64()?.is_some()); + container_client.delete(None).await?; Ok(()) } @@ -301,7 +335,7 @@ async fn test_append_block_transactional_checksums(ctx: TestContext) -> Result<( ); // MD5 Match Scenario - append_blob_client + let response = append_blob_client .append_block( RequestContent::from(content.clone()), u64::try_from(content.len())?, @@ -311,6 +345,8 @@ async fn test_append_block_transactional_checksums(ctx: TestContext) -> Result<( }), ) .await?; + assert!(response.content_md5()?.is_some()); + assert!(response.content_crc64()?.is_some()); // CRC64 Mismatch Scenario let response = append_blob_client @@ -329,7 +365,7 @@ async fn test_append_block_transactional_checksums(ctx: TestContext) -> Result<( ); // CRC64 Match Scenario - append_blob_client + let response = append_blob_client .append_block( RequestContent::from(content.clone()), u64::try_from(content.len())?, @@ -339,6 +375,8 @@ async fn test_append_block_transactional_checksums(ctx: TestContext) -> Result<( }), ) .await?; + assert!(response.content_crc64()?.is_some()); + assert!(response.content_md5()?.is_none()); container_client.delete(None).await?; Ok(()) diff --git a/sdk/storage/azure_storage_blob/tests/blob_client.rs b/sdk/storage/azure_storage_blob/tests/blob_client.rs index bd3e4e4a227..d8ae643756c 100644 --- a/sdk/storage/azure_storage_blob/tests/blob_client.rs +++ b/sdk/storage/azure_storage_blob/tests/blob_client.rs @@ -12,21 +12,21 @@ use azure_core::{ use azure_core_test::{recorded, Matcher, TestContext, TestMode, VarOptions}; use azure_storage_blob::{ models::{ - AccessTier, AccountKind, BlobClientAcquireLeaseOptions, + AccessTier, AccountKind, ArchiveStatus, BlobClientAcquireLeaseOptions, BlobClientAcquireLeaseResultHeaders, BlobClientChangeLeaseResultHeaders, BlobClientDownloadOptions, BlobClientGetAccountInfoResultHeaders, BlobClientGetPropertiesOptions, BlobClientGetPropertiesResultHeaders, BlobClientSetImmutabilityPolicyOptions, BlobClientSetMetadataOptions, BlobClientSetPropertiesOptions, BlobClientSetTierOptions, BlobTags, - BlockBlobClientUploadOptions, ImmutabilityPolicyMode, LeaseState, RehydratePriority, - StorageErrorCode, + BlockBlobClientCommitBlockListOptions, BlockBlobClientUploadOptions, + ImmutabilityPolicyMode, LeaseState, RehydratePriority, StorageErrorCode, }, BlobClient, BlobClientOptions, BlobContainerClient, BlobContainerClientOptions, StorageError, }; use bytes::{BufMut, BytesMut}; use common::{ - create_test_blob, get_blob_name, get_container_client, get_container_name, ClientOptionsExt, - StorageAccount, TestPolicy, + block_lookup, create_test_blob, get_blob_name, get_container_client, get_container_name, + ClientOptionsExt, StorageAccount, TestPolicy, }; use flate2::{write::GzEncoder, Compression}; use futures::TryStreamExt; @@ -240,6 +240,61 @@ async fn test_download_blob(ctx: TestContext) -> Result<(), Box> { Ok(()) } +#[recorded::test] +async fn test_download_blob_ranged_checksums(ctx: TestContext) -> Result<(), Box> { + // Recording Setup + let recording = ctx.recording(); + let container_client = + get_container_client(recording, true, StorageAccount::Standard, None).await?; + let blob_client = container_client.blob_client(&get_blob_name(recording)); + let data = b"hello rusty world"; + + blob_client + .upload(RequestContent::from(data.to_vec()), None) + .await?; + + // Range Download with CRC64 Scenario + let response = blob_client + .download(Some(BlobClientDownloadOptions { + range: Some((0..17u64).into()), + range_get_content_crc64: Some(true), + ..Default::default() + })) + .await?; + assert!(response.properties.content_crc64.is_some()); + assert!(response.properties.content_md5.is_none()); + let body_data = response.body.collect().await?; + assert_eq!(data.as_ref(), &body_data[..]); + + // Range Download with MD5 Scenario + let response = blob_client + .download(Some(BlobClientDownloadOptions { + range: Some((0..17u64).into()), + range_get_content_md5: Some(true), + ..Default::default() + })) + .await?; + assert!(response.properties.content_md5.is_some()); + assert!(response.properties.content_crc64.is_none()); + let body_data = response.body.collect().await?; + assert_eq!(data.as_ref(), &body_data[..]); + + // Range Download without Checksum Scenario (neither requested, partial range) + let response = blob_client + .download(Some(BlobClientDownloadOptions { + range: Some((0..10u64).into()), + ..Default::default() + })) + .await?; + assert!(response.properties.content_crc64.is_none()); + assert!(response.properties.content_md5.is_none()); + let body_data = response.body.collect().await?; + assert_eq!(&data[..10], &body_data[..]); + + container_client.delete(None).await?; + Ok(()) +} + #[recorded::test] async fn test_set_blob_metadata(ctx: TestContext) -> Result<(), Box> { // Recording Setup @@ -1590,3 +1645,187 @@ async fn test_gzip_blob_with_metadata_roundtrip(ctx: TestContext) -> Result<(), container_client.delete(None).await?; Ok(()) } + +#[recorded::test] +async fn test_set_access_tier_smart(ctx: TestContext) -> Result<(), Box> { + // Recording Setup + let recording = ctx.recording(); + let container_client = + get_container_client(recording, true, StorageAccount::Standard, None).await?; + + // Create Two Blobs + let blob_name_1 = "smart-blob1".to_string(); + let blob_name_2 = "smart-blob2".to_string(); + let blob_client_1 = container_client.blob_client(&blob_name_1); + let blob_client_2 = container_client.blob_client(&blob_name_2); + create_test_blob(&blob_client_1, None, None).await?; + create_test_blob(&blob_client_2, None, None).await?; + + // Set Smart Tier + blob_client_1.set_tier(AccessTier::Smart, None).await?; + blob_client_2.set_tier(AccessTier::Smart, None).await?; + + // Assert via get_properties + let response = blob_client_1.get_properties(None).await?; + assert_eq!( + AccessTier::Smart.to_string(), + response.access_tier()?.unwrap() + ); + assert!(response.smart_access_tier()?.is_some()); + + // Assert via list_blobs + let mut list_blobs_response = container_client.list_blobs(None)?.into_pages(); + let page = list_blobs_response.try_next().await?; + let list_blob_segment_response = page.unwrap().into_model()?; + assert_eq!(2, list_blob_segment_response.blob_items.len()); + for blob in &list_blob_segment_response.blob_items { + let properties = blob.properties.as_ref().unwrap(); + assert_eq!(Some(AccessTier::Smart), properties.access_tier); + assert!(properties.smart_access_tier.is_some()); + } + + container_client.delete(None).await?; + Ok(()) +} + +#[recorded::test] +async fn test_set_access_tier_smart_rehydrate(ctx: TestContext) -> Result<(), Box> { + // Recording Setup + let recording = ctx.recording(); + let container_client = + get_container_client(recording, true, StorageAccount::Standard, None).await?; + + // Create Two Blobs + let blob_name_1 = "smart-blob1".to_string(); + let blob_name_2 = "smart-blob2".to_string(); + let blob_client_1 = container_client.blob_client(&blob_name_1); + let blob_client_2 = container_client.blob_client(&blob_name_2); + create_test_blob(&blob_client_1, None, None).await?; + create_test_blob(&blob_client_2, None, None).await?; + + // Move Blobs to Archive Tier + blob_client_1.set_tier(AccessTier::Archive, None).await?; + blob_client_2.set_tier(AccessTier::Archive, None).await?; + + // Rehydrate to Smart Tier (High Priority) + blob_client_1 + .set_tier( + AccessTier::Smart, + Some(BlobClientSetTierOptions { + rehydrate_priority: Some(RehydratePriority::High), + ..Default::default() + }), + ) + .await?; + blob_client_2 + .set_tier( + AccessTier::Smart, + Some(BlobClientSetTierOptions { + rehydrate_priority: Some(RehydratePriority::High), + ..Default::default() + }), + ) + .await?; + + // Assert via get_properties + let response = blob_client_1.get_properties(None).await?; + assert_eq!( + Some(ArchiveStatus::RehydratePendingToSmart), + response.archive_status()? + ); + + // Assert via list_blobs + let mut list_blobs_response = container_client.list_blobs(None)?.into_pages(); + let page = list_blobs_response.try_next().await?; + let list_blob_segment_response = page.unwrap().into_model()?; + assert_eq!(2, list_blob_segment_response.blob_items.len()); + for blob in &list_blob_segment_response.blob_items { + let properties = blob.properties.as_ref().unwrap(); + assert_eq!( + Some(ArchiveStatus::RehydratePendingToSmart), + properties.archive_status + ); + } + + container_client.delete(None).await?; + Ok(()) +} + +#[recorded::test] +async fn test_smart_access_tier(ctx: TestContext) -> Result<(), Box> { + // Recording Setup + let recording = ctx.recording(); + let container_client = + get_container_client(recording, true, StorageAccount::Standard, None).await?; + + // Upload with Smart Tier + let upload_blob_client = container_client.blob_client(&get_blob_name(recording)); + upload_blob_client + .upload( + RequestContent::from(b"hello smart tier".to_vec()), + Some(BlockBlobClientUploadOptions { + tier: Some(AccessTier::Smart), + ..Default::default() + }), + ) + .await?; + + // Assert + let response = upload_blob_client.get_properties(None).await?; + assert_eq!( + AccessTier::Smart.to_string(), + response.access_tier()?.unwrap() + ); + assert!(response.smart_access_tier()?.is_some()); + + // Commit Block List with Smart Tier + let commit_blob_client = container_client.blob_client(&get_blob_name(recording)); + let block_blob_client = commit_blob_client.block_blob_client(); + let block_id: Vec = b"1".to_vec(); + let data = b"smart tier block data"; + block_blob_client + .stage_block( + &block_id, + u64::try_from(data.len())?, + RequestContent::from(data.to_vec()), + None, + ) + .await?; + let block_lookup_list = block_lookup(block_id); + block_blob_client + .commit_block_list( + block_lookup_list.try_into()?, + Some(BlockBlobClientCommitBlockListOptions { + tier: Some(AccessTier::Smart), + ..Default::default() + }), + ) + .await?; + + // Assert + let response = commit_blob_client.get_properties(None).await?; + assert_eq!( + AccessTier::Smart.to_string(), + response.access_tier()?.unwrap() + ); + assert!(response.smart_access_tier()?.is_some()); + + // Set Smart Tier + let download_blob_client = container_client.blob_client(&get_blob_name(recording)); + download_blob_client + .upload(RequestContent::from(b"smart tier download".to_vec()), None) + .await?; + download_blob_client + .set_tier(AccessTier::Smart, None) + .await?; + let response = download_blob_client.download(None).await?; + + // Assert + assert_eq!(Some(AccessTier::Smart), response.properties.access_tier); + assert!(response.properties.smart_access_tier.is_some()); + assert!(response.properties.access_tier_changed_on.is_some()); + assert!(!response.properties.access_tier_inferred.unwrap_or(false)); + + container_client.delete(None).await?; + Ok(()) +} diff --git a/sdk/storage/azure_storage_blob/tests/block_blob_client.rs b/sdk/storage/azure_storage_blob/tests/block_blob_client.rs index aaee771fee0..08f4156bb67 100644 --- a/sdk/storage/azure_storage_blob/tests/block_blob_client.rs +++ b/sdk/storage/azure_storage_blob/tests/block_blob_client.rs @@ -11,9 +11,10 @@ use azure_core_test::{recorded, TestContext}; use azure_storage_blob::{ models::{ BlobClientGetPropertiesResultHeaders, BlockBlobClientCommitBlockListOptions, - BlockBlobClientStageBlockFromUrlOptions, BlockBlobClientStageBlockOptions, - BlockBlobClientUploadBlobFromUrlOptions, BlockBlobClientUploadOptions, BlockListType, - BlockLookupList, + BlockBlobClientStageBlockFromUrlOptions, BlockBlobClientStageBlockFromUrlResultHeaders, + BlockBlobClientStageBlockOptions, BlockBlobClientStageBlockResultHeaders, + BlockBlobClientUploadBlobFromUrlOptions, BlockBlobClientUploadBlobFromUrlResultHeaders, + BlockBlobClientUploadOptions, BlockListType, BlockLookupList, }, BlobContainerClientOptions, }; @@ -154,10 +155,39 @@ async fn test_upload_blob_from_url(ctx: TestContext) -> Result<(), Box = vec![ + 0x5d, 0x41, 0x40, 0x2a, 0xbc, 0x4b, 0x2a, 0x76, 0xb9, 0x71, 0x9d, 0x91, 0x10, 0x17, 0xc5, + 0x92, + ]; + let md5_dest_blob_client = container_client.blob_client(&get_blob_name(recording)); + let combined_response = md5_dest_blob_client + .block_blob_client() + .upload_blob_from_url( + md5_source_blob_client.url().as_str().into(), + Some(BlockBlobClientUploadBlobFromUrlOptions { + source_content_md5: Some(source_md5), + ..Default::default() + }), + ) + .await?; + assert!(combined_response.content_md5()?.is_some()); + assert!(combined_response.content_crc64()?.is_some()); let create_options = BlockBlobClientUploadBlobFromUrlOptions::default().if_not_exists(); @@ -228,7 +258,7 @@ async fn test_stage_block_from_url(ctx: TestContext) -> Result<(), Box = b"block1".to_vec(); // Regular Scenario - block_blob_client + let stage_response = block_blob_client .stage_block_from_url( &block_id, u64::try_from(source_content.len())?, @@ -236,6 +266,40 @@ async fn test_stage_block_from_url(ctx: TestContext) -> Result<(), Box = vec![ + 0x5d, 0x41, 0x40, 0x2a, 0xbc, 0x4b, 0x2a, 0x76, 0xb9, 0x71, 0x9d, 0x91, 0x10, 0x17, 0xc5, + 0x92, + ]; + let md5_block_id: Vec = b"md5block".to_vec(); + let md5_block_blob_client = container_client + .blob_client(&get_blob_name(recording)) + .block_blob_client(); + let combined_response = md5_block_blob_client + .stage_block_from_url( + &md5_block_id, + u64::try_from(md5_source_content.len())?, + md5_source_blob_client.url().as_str().into(), + Some(BlockBlobClientStageBlockFromUrlOptions { + source_content_md5: Some(source_md5), + ..Default::default() + }), + ) + .await?; + assert!(combined_response.content_md5()?.is_some()); + assert!(combined_response.content_crc64()?.is_some()); // Staged Block Scenario let block_list = block_blob_client @@ -349,6 +413,25 @@ async fn upload(ctx: TestContext) -> Result<(), Box> { let data: [u8; 1024] = recording.random(); let bytes: Bytes = data.to_vec().into(); + // MD5("hello") - well-known test vector + let content_md5 = vec![ + 0x5d, 0x41, 0x40, 0x2a, 0xbc, 0x4b, 0x2a, 0x76, 0xb9, 0x71, 0x9d, 0x91, 0x10, 0x17, 0xc5, + 0x92, + ]; + // Combined MD5/CRC64 Single-Shot Scenario + let combined_response = block_blob_client + .upload( + Bytes::from_static(b"hello").into(), + Some(BlockBlobClientUploadOptions { + blob_content_md5: Some(content_md5), + partition_size: Some(NonZero::new(2048).unwrap()), + ..Default::default() + }), + ) + .await?; + assert!(combined_response.content_md5.is_some()); + assert!(combined_response.content_crc64.is_some()); + for (parallel, partition_size, expected_stage_block_calls) in [ (1, 2048, 0), // put blob expected (2, 1024, 0), // put blob expected @@ -364,9 +447,19 @@ async fn upload(ctx: TestContext) -> Result<(), Box> { }; { let _scope = count_policy.check_request_scope(); - block_blob_client + let upload_result = block_blob_client .upload(bytes.clone().into(), Some(options)) .await?; + // Note: x-ms-content-crc64 is only returned for single-shot (Put Blob) uploads, + // not partitioned uploads committed via Put Block List. + if expected_stage_block_calls == 0 { + assert!( + upload_result.content_crc64.is_some(), + "Failed parallel={},partition_size={}", + parallel, + partition_size + ); + } } let body_data = blob_client.download(None).await?.body.collect().await?; assert_eq!( @@ -622,7 +715,7 @@ async fn test_stage_block_transactional_checksums(ctx: TestContext) -> Result<() ); // MD5 Match Scenario - block_blob_client + let response = block_blob_client .stage_block( &block_id, u64::try_from(content.len())?, @@ -633,6 +726,8 @@ async fn test_stage_block_transactional_checksums(ctx: TestContext) -> Result<() }), ) .await?; + assert!(response.content_md5()?.is_some()); + assert!(response.content_crc64()?.is_some()); // CRC64 Mismatch Scenario let response = block_blob_client @@ -652,7 +747,7 @@ async fn test_stage_block_transactional_checksums(ctx: TestContext) -> Result<() ); // CRC64 Match Scenario - block_blob_client + let response = block_blob_client .stage_block( &block_id, u64::try_from(content.len())?, @@ -663,6 +758,8 @@ async fn test_stage_block_transactional_checksums(ctx: TestContext) -> Result<() }), ) .await?; + assert!(response.content_crc64()?.is_some()); + assert!(response.content_md5()?.is_none()); container_client.delete(None).await?; Ok(()) diff --git a/sdk/storage/azure_storage_blob/tests/page_blob_client.rs b/sdk/storage/azure_storage_blob/tests/page_blob_client.rs index e28911309fd..be5fde99aad 100644 --- a/sdk/storage/azure_storage_blob/tests/page_blob_client.rs +++ b/sdk/storage/azure_storage_blob/tests/page_blob_client.rs @@ -9,7 +9,8 @@ use azure_storage_blob::models::{ BlobClientGetPropertiesResultHeaders, BlobType, HttpRange, PageBlobClientCreateOptions, PageBlobClientListPageRangesOptions, PageBlobClientSetSequenceNumberOptions, PageBlobClientSetSequenceNumberResultHeaders, PageBlobClientUploadPagesFromUrlOptions, - PageBlobClientUploadPagesOptions, PageListHeaders, SequenceNumberActionType, + PageBlobClientUploadPagesFromUrlResultHeaders, PageBlobClientUploadPagesOptions, + PageBlobClientUploadPagesResultHeaders, PageListHeaders, SequenceNumberActionType, }; use common::{get_blob_name, get_container_client, StorageAccount}; use futures::TryStreamExt; @@ -244,7 +245,7 @@ async fn test_upload_page_from_url(ctx: TestContext) -> Result<(), Box Result<(), Box = vec![ + 220, 80, 134, 184, 71, 40, 155, 168, 184, 189, 225, 73, 184, 56, 129, 117, + ]; + let md5_dest_blob = container_client.blob_client(&get_blob_name(recording)); + let md5_dest_page_client = md5_dest_blob.page_blob_client(); + md5_dest_page_client.create(512, None).await?; + let combined_response = md5_dest_page_client + .upload_pages_from_url( + md5_source_blob.url().as_str().into(), + HttpRange::new(0, 512), + 512, + HttpRange::new(0, 512), + Some(PageBlobClientUploadPagesFromUrlOptions { + source_content_md5: Some(source_md5), + ..Default::default() + }), + ) + .await?; + assert!(combined_response.content_md5()?.is_some()); + assert!(combined_response.content_crc64()?.is_some()); + container_client.delete(None).await?; Ok(()) } @@ -464,7 +503,7 @@ async fn test_upload_pages_transactional_checksums(ctx: TestContext) -> Result<( ); // MD5 Match Scenario - page_blob_client + let response = page_blob_client .upload_pages( RequestContent::from(data.clone()), 512, @@ -475,6 +514,8 @@ async fn test_upload_pages_transactional_checksums(ctx: TestContext) -> Result<( }), ) .await?; + assert!(response.content_md5()?.is_some()); + assert!(response.content_crc64()?.is_some()); // CRC64 Mismatch Scenario let response = page_blob_client @@ -494,7 +535,7 @@ async fn test_upload_pages_transactional_checksums(ctx: TestContext) -> Result<( ); // CRC64 Match Scenario - page_blob_client + let response = page_blob_client .upload_pages( RequestContent::from(data.clone()), 512, @@ -505,6 +546,8 @@ async fn test_upload_pages_transactional_checksums(ctx: TestContext) -> Result<( }), ) .await?; + assert!(response.content_crc64()?.is_some()); + assert!(response.content_md5()?.is_none()); // No Checksum Scenario page_blob_client diff --git a/sdk/storage/azure_storage_blob/tsp-location.yaml b/sdk/storage/azure_storage_blob/tsp-location.yaml index 08a7c84f91d..35dca8303d7 100644 --- a/sdk/storage/azure_storage_blob/tsp-location.yaml +++ b/sdk/storage/azure_storage_blob/tsp-location.yaml @@ -1,4 +1,4 @@ directory: specification/storage/data-plane/BlobStorage -commit: 2cbf8269594238dba4c0211f821ac36c0bc9544f +commit: 7ef6eeb53f24e514ddcd879df3eae5ce6fd4e651 repo: Azure/azure-rest-api-specs additionalDirectories: diff --git a/sdk/storage/azure_storage_queue/Cargo.toml b/sdk/storage/azure_storage_queue/Cargo.toml index 457bdf3be31..3eac9aaee63 100644 --- a/sdk/storage/azure_storage_queue/Cargo.toml +++ b/sdk/storage/azure_storage_queue/Cargo.toml @@ -36,3 +36,6 @@ tokio = { workspace = true, features = ["macros"] } [package.metadata.docs.rs] all-features = true + +[package.metadata.cargo-semver-checks.lints] +enum_variant_added = "allow"