blob: d5972d0a8c16876092fb34c48614bbc6b2c699dd [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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
//
// http://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.
use super::credential::AzureCredential;
use crate::azure::credential::*;
use crate::azure::{AzureCredentialProvider, STORE};
use crate::client::get::GetClient;
use crate::client::header::{get_put_result, HeaderConfig};
use crate::client::list::ListClient;
use crate::client::retry::RetryExt;
use crate::client::GetOptionsExt;
use crate::multipart::PartId;
use crate::path::DELIMITER;
use crate::util::{deserialize_rfc1123, GetRange};
use crate::{
ClientOptions, GetOptions, ListResult, ObjectMeta, Path, PutMode, PutOptions, PutPayload,
PutResult, Result, RetryConfig,
};
use async_trait::async_trait;
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use bytes::{Buf, Bytes};
use chrono::{DateTime, Utc};
use hyper::http::HeaderName;
use reqwest::header::CONTENT_TYPE;
use reqwest::{
header::{HeaderValue, CONTENT_LENGTH, IF_MATCH, IF_NONE_MATCH},
Client as ReqwestClient, Method, RequestBuilder, Response,
};
use serde::{Deserialize, Serialize};
use snafu::{OptionExt, ResultExt, Snafu};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use url::Url;
const VERSION_HEADER: &str = "x-ms-version-id";
static TAGS_HEADER: HeaderName = HeaderName::from_static("x-ms-tags");
/// A specialized `Error` for object store-related errors
#[derive(Debug, Snafu)]
#[allow(missing_docs)]
pub(crate) enum Error {
#[snafu(display("Error performing get request {}: {}", path, source))]
GetRequest {
source: crate::client::retry::Error,
path: String,
},
#[snafu(display("Error getting get response body {}: {}", path, source))]
GetResponseBody {
source: reqwest::Error,
path: String,
},
#[snafu(display("Error performing put request {}: {}", path, source))]
PutRequest {
source: crate::client::retry::Error,
path: String,
},
#[snafu(display("Error performing delete request {}: {}", path, source))]
DeleteRequest {
source: crate::client::retry::Error,
path: String,
},
#[snafu(display("Error performing list request: {}", source))]
ListRequest { source: crate::client::retry::Error },
#[snafu(display("Error getting list response body: {}", source))]
ListResponseBody { source: reqwest::Error },
#[snafu(display("Got invalid list response: {}", source))]
InvalidListResponse { source: quick_xml::de::DeError },
#[snafu(display("Error authorizing request: {}", source))]
Authorization {
source: crate::azure::credential::Error,
},
#[snafu(display("Unable to extract metadata from headers: {}", source))]
Metadata {
source: crate::client::header::Error,
},
#[snafu(display("ETag required for conditional update"))]
MissingETag,
#[snafu(display("Error requesting user delegation key: {}", source))]
DelegationKeyRequest { source: crate::client::retry::Error },
#[snafu(display("Error getting user delegation key response body: {}", source))]
DelegationKeyResponseBody { source: reqwest::Error },
#[snafu(display("Got invalid user delegation key response: {}", source))]
DelegationKeyResponse { source: quick_xml::de::DeError },
#[snafu(display("Generating SAS keys with SAS tokens auth is not supported"))]
SASforSASNotSupported,
#[snafu(display("Generating SAS keys while skipping signatures is not supported"))]
SASwithSkipSignature,
}
impl From<Error> for crate::Error {
fn from(err: Error) -> Self {
match err {
Error::GetRequest { source, path }
| Error::DeleteRequest { source, path }
| Error::PutRequest { source, path } => source.error(STORE, path),
_ => Self::Generic {
store: STORE,
source: Box::new(err),
},
}
}
}
/// Configuration for [AzureClient]
#[derive(Debug)]
pub(crate) struct AzureConfig {
pub account: String,
pub container: String,
pub credentials: AzureCredentialProvider,
pub retry_config: RetryConfig,
pub service: Url,
pub is_emulator: bool,
pub skip_signature: bool,
pub disable_tagging: bool,
pub client_options: ClientOptions,
}
impl AzureConfig {
pub(crate) fn path_url(&self, path: &Path) -> Url {
let mut url = self.service.clone();
{
let mut path_mut = url.path_segments_mut().unwrap();
if self.is_emulator {
path_mut.push(&self.account);
}
path_mut.push(&self.container).extend(path.parts());
}
url
}
async fn get_credential(&self) -> Result<Option<Arc<AzureCredential>>> {
if self.skip_signature {
Ok(None)
} else {
Some(self.credentials.get_credential().await).transpose()
}
}
}
/// A builder for a put request allowing customisation of the headers and query string
struct PutRequest<'a> {
path: &'a Path,
config: &'a AzureConfig,
payload: PutPayload,
builder: RequestBuilder,
idempotent: bool,
}
impl<'a> PutRequest<'a> {
fn header(self, k: &HeaderName, v: &str) -> Self {
let builder = self.builder.header(k, v);
Self { builder, ..self }
}
fn query<T: Serialize + ?Sized + Sync>(self, query: &T) -> Self {
let builder = self.builder.query(query);
Self { builder, ..self }
}
fn set_idempotent(mut self, idempotent: bool) -> Self {
self.idempotent = idempotent;
self
}
async fn send(self) -> Result<Response> {
let credential = self.config.get_credential().await?;
let response = self
.builder
.header(CONTENT_LENGTH, self.payload.content_length())
.with_azure_authorization(&credential, &self.config.account)
.retryable(&self.config.retry_config)
.idempotent(true)
.payload(Some(self.payload))
.send()
.await
.context(PutRequestSnafu {
path: self.path.as_ref(),
})?;
Ok(response)
}
}
#[derive(Debug)]
pub(crate) struct AzureClient {
config: AzureConfig,
client: ReqwestClient,
}
impl AzureClient {
/// create a new instance of [AzureClient]
pub fn new(config: AzureConfig) -> Result<Self> {
let client = config.client_options.client()?;
Ok(Self { config, client })
}
/// Returns the config
pub fn config(&self) -> &AzureConfig {
&self.config
}
async fn get_credential(&self) -> Result<Option<Arc<AzureCredential>>> {
self.config.get_credential().await
}
fn put_request<'a>(&'a self, path: &'a Path, payload: PutPayload) -> PutRequest<'a> {
let url = self.config.path_url(path);
let mut builder = self.client.request(Method::PUT, url);
if let Some(value) = self.config().client_options.get_content_type(path) {
builder = builder.header(CONTENT_TYPE, value);
}
PutRequest {
path,
builder,
payload,
config: &self.config,
idempotent: false,
}
}
/// Make an Azure PUT request <https://docs.microsoft.com/en-us/rest/api/storageservices/put-blob>
pub async fn put_blob(
&self,
path: &Path,
payload: PutPayload,
opts: PutOptions,
) -> Result<PutResult> {
let builder = self.put_request(path, payload);
let builder = match &opts.mode {
PutMode::Overwrite => builder.set_idempotent(true),
PutMode::Create => builder.header(&IF_NONE_MATCH, "*"),
PutMode::Update(v) => {
let etag = v.e_tag.as_ref().context(MissingETagSnafu)?;
builder.header(&IF_MATCH, etag)
}
};
let builder = match (opts.tags.encoded(), self.config.disable_tagging) {
("", _) | (_, true) => builder,
(tags, false) => builder.header(&TAGS_HEADER, tags),
};
let response = builder.header(&BLOB_TYPE, "BlockBlob").send().await?;
Ok(get_put_result(response.headers(), VERSION_HEADER).context(MetadataSnafu)?)
}
/// PUT a block <https://learn.microsoft.com/en-us/rest/api/storageservices/put-block>
pub async fn put_block(
&self,
path: &Path,
part_idx: usize,
payload: PutPayload,
) -> Result<PartId> {
let content_id = format!("{part_idx:20}");
let block_id = BASE64_STANDARD.encode(&content_id);
self.put_request(path, payload)
.query(&[("comp", "block"), ("blockid", &block_id)])
.set_idempotent(true)
.send()
.await?;
Ok(PartId { content_id })
}
/// PUT a block list <https://learn.microsoft.com/en-us/rest/api/storageservices/put-block-list>
pub async fn put_block_list(&self, path: &Path, parts: Vec<PartId>) -> Result<PutResult> {
let blocks = parts
.into_iter()
.map(|part| BlockId::from(part.content_id))
.collect();
let response = self
.put_request(path, BlockList { blocks }.to_xml().into())
.query(&[("comp", "blocklist")])
.set_idempotent(true)
.send()
.await?;
Ok(get_put_result(response.headers(), VERSION_HEADER).context(MetadataSnafu)?)
}
/// Make an Azure Delete request <https://docs.microsoft.com/en-us/rest/api/storageservices/delete-blob>
pub async fn delete_request<T: Serialize + ?Sized + Sync>(
&self,
path: &Path,
query: &T,
) -> Result<()> {
let credential = self.get_credential().await?;
let url = self.config.path_url(path);
self.client
.request(Method::DELETE, url)
.query(query)
.header(&DELETE_SNAPSHOTS, "include")
.with_azure_authorization(&credential, &self.config.account)
.send_retry(&self.config.retry_config)
.await
.context(DeleteRequestSnafu {
path: path.as_ref(),
})?;
Ok(())
}
/// Make an Azure Copy request <https://docs.microsoft.com/en-us/rest/api/storageservices/copy-blob>
pub async fn copy_request(&self, from: &Path, to: &Path, overwrite: bool) -> Result<()> {
let credential = self.get_credential().await?;
let url = self.config.path_url(to);
let mut source = self.config.path_url(from);
// If using SAS authorization must include the headers in the URL
// <https://docs.microsoft.com/en-us/rest/api/storageservices/copy-blob#request-headers>
if let Some(AzureCredential::SASToken(pairs)) = credential.as_deref() {
source.query_pairs_mut().extend_pairs(pairs);
}
let mut builder = self
.client
.request(Method::PUT, url)
.header(&COPY_SOURCE, source.to_string())
.header(CONTENT_LENGTH, HeaderValue::from_static("0"));
if !overwrite {
builder = builder.header(IF_NONE_MATCH, "*");
}
builder
.with_azure_authorization(&credential, &self.config.account)
.retryable(&self.config.retry_config)
.idempotent(overwrite)
.send()
.await
.map_err(|err| err.error(STORE, from.to_string()))?;
Ok(())
}
/// Make a Get User Delegation Key request
/// <https://docs.microsoft.com/en-us/rest/api/storageservices/get-user-delegation-key>
async fn get_user_delegation_key(
&self,
start: &DateTime<Utc>,
end: &DateTime<Utc>,
) -> Result<UserDelegationKey> {
let credential = self.get_credential().await?;
let url = self.config.service.clone();
let start = start.to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
let expiry = end.to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
let mut body = String::new();
body.push_str("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<KeyInfo>\n");
body.push_str(&format!(
"\t<Start>{start}</Start>\n\t<Expiry>{expiry}</Expiry>\n"
));
body.push_str("</KeyInfo>");
let response = self
.client
.request(Method::POST, url)
.body(body)
.query(&[("restype", "service"), ("comp", "userdelegationkey")])
.with_azure_authorization(&credential, &self.config.account)
.retryable(&self.config.retry_config)
.idempotent(true)
.send()
.await
.context(DelegationKeyRequestSnafu)?
.bytes()
.await
.context(DelegationKeyResponseBodySnafu)?;
let response: UserDelegationKey =
quick_xml::de::from_reader(response.reader()).context(DelegationKeyResponseSnafu)?;
Ok(response)
}
/// Creat an AzureSigner for generating SAS tokens (pre-signed urls).
///
/// Depending on the type of credential, this will either use the account key or a user delegation key.
/// Since delegation keys are acquired ad-hoc, the signer aloows for signing multiple urls with the same key.
pub async fn signer(&self, expires_in: Duration) -> Result<AzureSigner> {
let credential = self.get_credential().await?;
let signed_start = chrono::Utc::now();
let signed_expiry = signed_start + expires_in;
match credential.as_deref() {
Some(AzureCredential::BearerToken(_)) => {
let key = self
.get_user_delegation_key(&signed_start, &signed_expiry)
.await?;
let signing_key = AzureAccessKey::try_new(&key.value)?;
Ok(AzureSigner::new(
signing_key,
self.config.account.clone(),
signed_start,
signed_expiry,
Some(key),
))
}
Some(AzureCredential::AccessKey(key)) => Ok(AzureSigner::new(
key.to_owned(),
self.config.account.clone(),
signed_start,
signed_expiry,
None,
)),
None => Err(Error::SASwithSkipSignature.into()),
_ => Err(Error::SASforSASNotSupported.into()),
}
}
#[cfg(test)]
pub async fn get_blob_tagging(&self, path: &Path) -> Result<Response> {
let credential = self.get_credential().await?;
let url = self.config.path_url(path);
let response = self
.client
.request(Method::GET, url)
.query(&[("comp", "tags")])
.with_azure_authorization(&credential, &self.config.account)
.send_retry(&self.config.retry_config)
.await
.context(GetRequestSnafu {
path: path.as_ref(),
})?;
Ok(response)
}
}
#[async_trait]
impl GetClient for AzureClient {
const STORE: &'static str = STORE;
const HEADER_CONFIG: HeaderConfig = HeaderConfig {
etag_required: true,
last_modified_required: true,
version_header: Some(VERSION_HEADER),
};
/// Make an Azure GET request
/// <https://docs.microsoft.com/en-us/rest/api/storageservices/get-blob>
/// <https://docs.microsoft.com/en-us/rest/api/storageservices/get-blob-properties>
async fn get_request(&self, path: &Path, options: GetOptions) -> Result<Response> {
// As of 2024-01-02, Azure does not support suffix requests,
// so we should fail fast here rather than sending one
if let Some(GetRange::Suffix(_)) = options.range.as_ref() {
return Err(crate::Error::NotSupported {
source: "Azure does not support suffix range requests".into(),
});
}
let credential = self.get_credential().await?;
let url = self.config.path_url(path);
let method = match options.head {
true => Method::HEAD,
false => Method::GET,
};
let mut builder = self
.client
.request(method, url)
.header(CONTENT_LENGTH, HeaderValue::from_static("0"))
.body(Bytes::new());
if let Some(v) = &options.version {
builder = builder.query(&[("versionid", v)])
}
let response = builder
.with_get_options(options)
.with_azure_authorization(&credential, &self.config.account)
.send_retry(&self.config.retry_config)
.await
.context(GetRequestSnafu {
path: path.as_ref(),
})?;
match response.headers().get("x-ms-resource-type") {
Some(resource) if resource.as_ref() != b"file" => Err(crate::Error::NotFound {
path: path.to_string(),
source: format!(
"Not a file, got x-ms-resource-type: {}",
String::from_utf8_lossy(resource.as_ref())
)
.into(),
}),
_ => Ok(response),
}
}
}
#[async_trait]
impl ListClient for AzureClient {
/// Make an Azure List request <https://docs.microsoft.com/en-us/rest/api/storageservices/list-blobs>
async fn list_request(
&self,
prefix: Option<&str>,
delimiter: bool,
token: Option<&str>,
offset: Option<&str>,
) -> Result<(ListResult, Option<String>)> {
assert!(offset.is_none()); // Not yet supported
let credential = self.get_credential().await?;
let url = self.config.path_url(&Path::default());
let mut query = Vec::with_capacity(5);
query.push(("restype", "container"));
query.push(("comp", "list"));
if let Some(prefix) = prefix {
query.push(("prefix", prefix))
}
if delimiter {
query.push(("delimiter", DELIMITER))
}
if let Some(token) = token {
query.push(("marker", token))
}
let response = self
.client
.request(Method::GET, url)
.query(&query)
.with_azure_authorization(&credential, &self.config.account)
.send_retry(&self.config.retry_config)
.await
.context(ListRequestSnafu)?
.bytes()
.await
.context(ListResponseBodySnafu)?;
let mut response: ListResultInternal =
quick_xml::de::from_reader(response.reader()).context(InvalidListResponseSnafu)?;
let token = response.next_marker.take();
Ok((to_list_result(response, prefix)?, token))
}
}
/// Raw / internal response from list requests
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct ListResultInternal {
pub prefix: Option<String>,
pub max_results: Option<u32>,
pub delimiter: Option<String>,
pub next_marker: Option<String>,
pub blobs: Blobs,
}
fn to_list_result(value: ListResultInternal, prefix: Option<&str>) -> Result<ListResult> {
let prefix = prefix.unwrap_or_default();
let common_prefixes = value
.blobs
.blob_prefix
.into_iter()
.map(|x| Ok(Path::parse(x.name)?))
.collect::<Result<_>>()?;
let objects = value
.blobs
.blobs
.into_iter()
// Note: Filters out directories from list results when hierarchical namespaces are
// enabled. When we want directories, its always via the BlobPrefix mechanics,
// and during lists we state that prefixes are evaluated on path segment basis.
.filter(|blob| {
!matches!(blob.properties.resource_type.as_ref(), Some(typ) if typ == "directory")
&& blob.name.len() > prefix.len()
})
.map(ObjectMeta::try_from)
.collect::<Result<_>>()?;
Ok(ListResult {
common_prefixes,
objects,
})
}
/// Collection of blobs and potentially shared prefixes returned from list requests.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct Blobs {
#[serde(default)]
pub blob_prefix: Vec<BlobPrefix>,
#[serde(rename = "Blob", default)]
pub blobs: Vec<Blob>,
}
/// Common prefix in list blobs response
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct BlobPrefix {
pub name: String,
}
/// Details for a specific blob
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct Blob {
pub name: String,
pub version_id: Option<String>,
pub is_current_version: Option<bool>,
pub deleted: Option<bool>,
pub properties: BlobProperties,
pub metadata: Option<HashMap<String, String>>,
}
impl TryFrom<Blob> for ObjectMeta {
type Error = crate::Error;
fn try_from(value: Blob) -> Result<Self> {
Ok(Self {
location: Path::parse(value.name)?,
last_modified: value.properties.last_modified,
size: value.properties.content_length as usize,
e_tag: value.properties.e_tag,
version: None, // For consistency with S3 and GCP which don't include this
})
}
}
/// Properties associated with individual blobs. The actual list
/// of returned properties is much more exhaustive, but we limit
/// the parsed fields to the ones relevant in this crate.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct BlobProperties {
#[serde(deserialize_with = "deserialize_rfc1123", rename = "Last-Modified")]
pub last_modified: DateTime<Utc>,
#[serde(rename = "Content-Length")]
pub content_length: u64,
#[serde(rename = "Content-Type")]
pub content_type: String,
#[serde(rename = "Content-Encoding")]
pub content_encoding: Option<String>,
#[serde(rename = "Content-Language")]
pub content_language: Option<String>,
#[serde(rename = "Etag")]
pub e_tag: Option<String>,
#[serde(rename = "ResourceType")]
pub resource_type: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct BlockId(Bytes);
impl BlockId {
pub fn new(block_id: impl Into<Bytes>) -> Self {
Self(block_id.into())
}
}
impl<B> From<B> for BlockId
where
B: Into<Bytes>,
{
fn from(v: B) -> Self {
Self::new(v)
}
}
impl AsRef<[u8]> for BlockId {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub(crate) struct BlockList {
pub blocks: Vec<BlockId>,
}
impl BlockList {
pub fn to_xml(&self) -> String {
let mut s = String::new();
s.push_str("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<BlockList>\n");
for block_id in &self.blocks {
let node = format!(
"\t<Uncommitted>{}</Uncommitted>\n",
BASE64_STANDARD.encode(block_id)
);
s.push_str(&node);
}
s.push_str("</BlockList>");
s
}
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub(crate) struct UserDelegationKey {
pub signed_oid: String,
pub signed_tid: String,
pub signed_start: String,
pub signed_expiry: String,
pub signed_service: String,
pub signed_version: String,
pub value: String,
}
#[cfg(test)]
mod tests {
use bytes::Bytes;
use super::*;
#[test]
fn deserde_azure() {
const S: &str = "<?xml version=\"1.0\" encoding=\"utf-8\"?>
<EnumerationResults ServiceEndpoint=\"https://azureskdforrust.blob.core.windows.net/\" ContainerName=\"osa2\">
<Blobs>
<Blob>
<Name>blob0.txt</Name>
<Properties>
<Creation-Time>Thu, 01 Jul 2021 10:44:59 GMT</Creation-Time>
<Last-Modified>Thu, 01 Jul 2021 10:44:59 GMT</Last-Modified>
<Expiry-Time>Thu, 07 Jul 2022 14:38:48 GMT</Expiry-Time>
<Etag>0x8D93C7D4629C227</Etag>
<Content-Length>8</Content-Length>
<Content-Type>text/plain</Content-Type>
<Content-Encoding />
<Content-Language />
<Content-CRC64 />
<Content-MD5>rvr3UC1SmUw7AZV2NqPN0g==</Content-MD5>
<Cache-Control />
<Content-Disposition />
<BlobType>BlockBlob</BlobType>
<AccessTier>Hot</AccessTier>
<AccessTierInferred>true</AccessTierInferred>
<LeaseStatus>unlocked</LeaseStatus>
<LeaseState>available</LeaseState>
<ServerEncrypted>true</ServerEncrypted>
</Properties>
<Metadata><userkey>uservalue</userkey></Metadata>
<OrMetadata />
</Blob>
<Blob>
<Name>blob1.txt</Name>
<Properties>
<Creation-Time>Thu, 01 Jul 2021 10:44:59 GMT</Creation-Time>
<Last-Modified>Thu, 01 Jul 2021 10:44:59 GMT</Last-Modified>
<Etag>0x8D93C7D463004D6</Etag>
<Content-Length>8</Content-Length>
<Content-Type>text/plain</Content-Type>
<Content-Encoding />
<Content-Language />
<Content-CRC64 />
<Content-MD5>rvr3UC1SmUw7AZV2NqPN0g==</Content-MD5>
<Cache-Control />
<Content-Disposition />
<BlobType>BlockBlob</BlobType>
<AccessTier>Hot</AccessTier>
<AccessTierInferred>true</AccessTierInferred>
<LeaseStatus>unlocked</LeaseStatus>
<LeaseState>available</LeaseState>
<ServerEncrypted>true</ServerEncrypted>
</Properties>
<OrMetadata />
</Blob>
<Blob>
<Name>blob2.txt</Name>
<Properties>
<Creation-Time>Thu, 01 Jul 2021 10:44:59 GMT</Creation-Time>
<Last-Modified>Thu, 01 Jul 2021 10:44:59 GMT</Last-Modified>
<Etag>0x8D93C7D4636478A</Etag>
<Content-Length>8</Content-Length>
<Content-Type>text/plain</Content-Type>
<Content-Encoding />
<Content-Language />
<Content-CRC64 />
<Content-MD5>rvr3UC1SmUw7AZV2NqPN0g==</Content-MD5>
<Cache-Control />
<Content-Disposition />
<BlobType>BlockBlob</BlobType>
<AccessTier>Hot</AccessTier>
<AccessTierInferred>true</AccessTierInferred>
<LeaseStatus>unlocked</LeaseStatus>
<LeaseState>available</LeaseState>
<ServerEncrypted>true</ServerEncrypted>
</Properties>
<OrMetadata />
</Blob>
</Blobs>
<NextMarker />
</EnumerationResults>";
let mut _list_blobs_response_internal: ListResultInternal =
quick_xml::de::from_str(S).unwrap();
}
#[test]
fn deserde_azurite() {
const S: &str = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>
<EnumerationResults ServiceEndpoint=\"http://127.0.0.1:10000/devstoreaccount1\" ContainerName=\"osa2\">
<Prefix/>
<Marker/>
<MaxResults>5000</MaxResults>
<Delimiter/>
<Blobs>
<Blob>
<Name>blob0.txt</Name>
<Properties>
<Creation-Time>Thu, 01 Jul 2021 10:45:02 GMT</Creation-Time>
<Last-Modified>Thu, 01 Jul 2021 10:45:02 GMT</Last-Modified>
<Etag>0x228281B5D517B20</Etag>
<Content-Length>8</Content-Length>
<Content-Type>text/plain</Content-Type>
<Content-MD5>rvr3UC1SmUw7AZV2NqPN0g==</Content-MD5>
<BlobType>BlockBlob</BlobType>
<LeaseStatus>unlocked</LeaseStatus>
<LeaseState>available</LeaseState>
<ServerEncrypted>true</ServerEncrypted>
<AccessTier>Hot</AccessTier>
<AccessTierInferred>true</AccessTierInferred>
<AccessTierChangeTime>Thu, 01 Jul 2021 10:45:02 GMT</AccessTierChangeTime>
</Properties>
</Blob>
<Blob>
<Name>blob1.txt</Name>
<Properties>
<Creation-Time>Thu, 01 Jul 2021 10:45:02 GMT</Creation-Time>
<Last-Modified>Thu, 01 Jul 2021 10:45:02 GMT</Last-Modified>
<Etag>0x1DD959381A8A860</Etag>
<Content-Length>8</Content-Length>
<Content-Type>text/plain</Content-Type>
<Content-MD5>rvr3UC1SmUw7AZV2NqPN0g==</Content-MD5>
<BlobType>BlockBlob</BlobType>
<LeaseStatus>unlocked</LeaseStatus>
<LeaseState>available</LeaseState>
<ServerEncrypted>true</ServerEncrypted>
<AccessTier>Hot</AccessTier>
<AccessTierInferred>true</AccessTierInferred>
<AccessTierChangeTime>Thu, 01 Jul 2021 10:45:02 GMT</AccessTierChangeTime>
</Properties>
</Blob>
<Blob>
<Name>blob2.txt</Name>
<Properties>
<Creation-Time>Thu, 01 Jul 2021 10:45:02 GMT</Creation-Time>
<Last-Modified>Thu, 01 Jul 2021 10:45:02 GMT</Last-Modified>
<Etag>0x1FBE9C9B0C7B650</Etag>
<Content-Length>8</Content-Length>
<Content-Type>text/plain</Content-Type>
<Content-MD5>rvr3UC1SmUw7AZV2NqPN0g==</Content-MD5>
<BlobType>BlockBlob</BlobType>
<LeaseStatus>unlocked</LeaseStatus>
<LeaseState>available</LeaseState>
<ServerEncrypted>true</ServerEncrypted>
<AccessTier>Hot</AccessTier>
<AccessTierInferred>true</AccessTierInferred>
<AccessTierChangeTime>Thu, 01 Jul 2021 10:45:02 GMT</AccessTierChangeTime>
</Properties>
</Blob>
</Blobs>
<NextMarker/>
</EnumerationResults>";
let _list_blobs_response_internal: ListResultInternal = quick_xml::de::from_str(S).unwrap();
}
#[test]
fn to_xml() {
const S: &str = "<?xml version=\"1.0\" encoding=\"utf-8\"?>
<BlockList>
\t<Uncommitted>bnVtZXJvMQ==</Uncommitted>
\t<Uncommitted>bnVtZXJvMg==</Uncommitted>
\t<Uncommitted>bnVtZXJvMw==</Uncommitted>
</BlockList>";
let mut blocks = BlockList { blocks: Vec::new() };
blocks.blocks.push(Bytes::from_static(b"numero1").into());
blocks.blocks.push("numero2".into());
blocks.blocks.push("numero3".into());
let res: &str = &blocks.to_xml();
assert_eq!(res, S)
}
#[test]
fn test_delegated_key_response() {
const S: &str = r#"<?xml version="1.0" encoding="utf-8"?>
<UserDelegationKey>
<SignedOid>String containing a GUID value</SignedOid>
<SignedTid>String containing a GUID value</SignedTid>
<SignedStart>String formatted as ISO date</SignedStart>
<SignedExpiry>String formatted as ISO date</SignedExpiry>
<SignedService>b</SignedService>
<SignedVersion>String specifying REST api version to use to create the user delegation key</SignedVersion>
<Value>String containing the user delegation key</Value>
</UserDelegationKey>"#;
let _delegated_key_response_internal: UserDelegationKey =
quick_xml::de::from_str(S).unwrap();
}
}