From 7ab154e733c3d0b967386af589d5b0c92fd27d02 Mon Sep 17 00:00:00 2001 From: XiaoningLiu Date: Wed, 10 Jun 2020 22:38:20 +0800 Subject: [PATCH 1/3] [Storage] Support blob tags (recording will be added later) --- sdk/storage/storage-blob/CHANGELOG.md | 1 + .../storage-blob/review/storage-blob.api.md | 122 +++++- .../storage-blob/src/BlobDownloadResponse.ts | 11 + .../storage-blob/src/BlobSASPermissions.ts | 14 + .../src/BlobSASSignatureValues.ts | 23 +- .../storage-blob/src/BlobServiceClient.ts | 275 ++++++++++++- sdk/storage/storage-blob/src/Clients.ts | 379 +++++++++++++----- .../src/ContainerSASPermissions.ts | 14 + .../storage-blob/src/generatedModels.ts | 12 +- .../storage-blob/src/utils/utils.common.ts | 22 +- .../storage-blob/test/blobclient.spec.ts | 92 +++++ .../test/blobserviceclient.spec.ts | 81 +++- .../test/browser/highlevel.browser.spec.ts | 20 + .../test/node/highlevel.node.spec.ts | 40 ++ .../storage-blob/test/node/sas.spec.ts | 62 +++ .../test/utils/testutils.common.ts | 13 + 16 files changed, 1067 insertions(+), 114 deletions(-) diff --git a/sdk/storage/storage-blob/CHANGELOG.md b/sdk/storage/storage-blob/CHANGELOG.md index e41d10721778..1a946b8a0595 100644 --- a/sdk/storage/storage-blob/CHANGELOG.md +++ b/sdk/storage/storage-blob/CHANGELOG.md @@ -5,6 +5,7 @@ - Supported quick query. Added a new API `BlockBlobClient.query()`. - Increased the maximum block size for Block Blob from 100MiB to 4000MiB(~4GB). And thereby supporting ~200TB maximum size for Block Blob. - Added support for blob versioning. +- Supported blob tags. ## 12.1.2 (2020.05) diff --git a/sdk/storage/storage-blob/review/storage-blob.api.md b/sdk/storage/storage-blob/review/storage-blob.api.md index 76f5117189c7..e0e464fed15d 100644 --- a/sdk/storage/storage-blob/review/storage-blob.api.md +++ b/sdk/storage/storage-blob/review/storage-blob.api.md @@ -208,6 +208,7 @@ export interface AppendBlobCreateOptions extends CommonOptions { customerProvidedKey?: CpkInfo; encryptionScope?: string; metadata?: Metadata; + tags?: BlobTags; } // @public @@ -373,10 +374,12 @@ export class BlobClient extends StorageClient { getBlockBlobClient(): BlockBlobClient; getPageBlobClient(): PageBlobClient; getProperties(options?: BlobGetPropertiesOptions): Promise; + getTags(options?: BlobGetTagsOptions): Promise; get name(): string; setAccessTier(tier: BlockBlobTier | PremiumPageBlobTier | string, options?: BlobSetTierOptions): Promise; setHTTPHeaders(blobHTTPHeaders?: BlobHTTPHeaders, options?: BlobSetHTTPHeadersOptions): Promise; setMetadata(metadata?: Metadata, options?: BlobSetMetadataOptions): Promise; + setTags(tags: BlobTags, options?: BlobSetTagsOptions): Promise; syncCopyFromURL(copySource: string, options?: BlobSyncCopyFromURLOptions): Promise; undelete(options?: BlobUndeleteOptions): Promise; withSnapshot(snapshot: string): BlobClient; @@ -642,6 +645,30 @@ export type BlobGetPropertiesResponse = BlobGetPropertiesHeaders & { }; }; +// @public +export interface BlobGetTagsHeaders { + clientRequestId?: string; + date?: Date; + // (undocumented) + errorCode?: string; + requestId?: string; + version?: string; +} + +// @public +export interface BlobGetTagsOptions extends CommonOptions { + abortSignal?: AbortSignalLike; +} + +// @public +export type BlobGetTagsResponse = BlobTags & BlobGetTagsHeaders & { + _response: coreHttp.HttpResponse & { + parsedHeaders: BlobGetTagsHeaders; + bodyAsText: string; + parsedBody: BlobTags; + }; +}; + // @public export interface BlobHierarchyListSegment { // (undocumented) @@ -662,8 +689,6 @@ export interface BlobHTTPHeaders { // @public export interface BlobItem { - // Warning: (ae-forgotten-export) The symbol "BlobTags" needs to be exported by the entry point index.d.ts - // // (undocumented) blobTags?: BlobTags; // (undocumented) @@ -833,6 +858,7 @@ export class BlobSASPermissions { deleteVersion: boolean; static parse(permissions: string): BlobSASPermissions; read: boolean; + tag: boolean; toString(): string; write: boolean; } @@ -866,6 +892,7 @@ export class BlobServiceClient extends StorageClient { containerCreateResponse: ContainerCreateResponse; }>; deleteContainer(containerName: string, options?: ContainerDeleteMethodOptions): Promise; + findBlobsByTags(tagFilterSqlExpression: string, options?: ServiceFindBlobByTagsOptions): PagedAsyncIterableIterator; static fromConnectionString(connectionString: string, options?: StoragePipelineOptions): BlobServiceClient; getAccountInfo(options?: ServiceGetAccountInfoOptions): Promise; getBlobBatchClient(): BlobBatchClient; @@ -957,6 +984,28 @@ export type BlobSetMetadataResponse = BlobSetMetadataHeaders & { }; }; +// @public +export interface BlobSetTagsHeaders { + clientRequestId?: string; + date?: Date; + // (undocumented) + errorCode?: string; + requestId?: string; + version?: string; +} + +// @public +export interface BlobSetTagsOptions extends CommonOptions { + abortSignal?: AbortSignalLike; +} + +// @public +export type BlobSetTagsResponse = BlobSetTagsHeaders & { + _response: coreHttp.HttpResponse & { + parsedHeaders: BlobSetTagsHeaders; + }; +}; + // @public export interface BlobSetTierHeaders { clientRequestId?: string; @@ -1002,6 +1051,7 @@ export interface BlobStartCopyFromURLOptions extends CommonOptions { metadata?: Metadata; rehydratePriority?: RehydratePriority; sourceConditions?: ModifiedAccessConditions; + tags?: BlobTags; tier?: BlockBlobTier | PremiumPageBlobTier | string; } @@ -1019,6 +1069,21 @@ export interface BlobSyncCopyFromURLOptions extends CommonOptions { metadata?: Metadata; sourceConditions?: ModifiedAccessConditions; sourceContentMD5?: Uint8Array; + tags?: BlobTags; +} + +// @public +export interface BlobTag { + // (undocumented) + key: string; + // (undocumented) + value: string; +} + +// @public +export interface BlobTags { + // (undocumented) + blobTagSet: BlobTag[]; } // @public @@ -1101,6 +1166,7 @@ export interface BlockBlobCommitBlockListOptions extends CommonOptions { customerProvidedKey?: CpkInfo; encryptionScope?: string; metadata?: Metadata; + tags?: BlobTags; tier?: BlockBlobTier | string; } @@ -1153,6 +1219,7 @@ export interface BlockBlobParallelUploadOptions extends CommonOptions { [propertyName: string]: string; }; onProgress?: (progress: TransferProgressEvent) => void; + tags?: BlobTags; } // @public @@ -1265,6 +1332,7 @@ export interface BlockBlobUploadOptions extends CommonOptions { encryptionScope?: string; metadata?: Metadata; onProgress?: (progress: TransferProgressEvent) => void; + tags?: BlobTags; tier?: BlockBlobTier | string; } @@ -1285,6 +1353,7 @@ export interface BlockBlobUploadStreamOptions extends CommonOptions { [propertyName: string]: string; }; onProgress?: (progress: TransferProgressEvent) => void; + tags?: BlobTags; } // @public @@ -1556,6 +1625,7 @@ export interface ContainerListBlobsOptions extends CommonOptions { includeDeleted?: boolean; includeMetadata?: boolean; includeSnapshots?: boolean; + includeTags?: boolean; includeUncommitedBlobs?: boolean; includeVersions?: boolean; prefix?: string; @@ -1605,6 +1675,7 @@ export class ContainerSASPermissions { list: boolean; static parse(permissions: string): ContainerSASPermissions; read: boolean; + tag: boolean; toString(): string; write: boolean; } @@ -1705,6 +1776,28 @@ export { deserializationPolicy } // @public export type EncryptionAlgorithmType = 'AES256'; +// @public +export interface FilterBlobItem { + // (undocumented) + containerName: string; + // (undocumented) + name: string; + // (undocumented) + tagValue: string; +} + +// @public +export interface FilterBlobSegment { + // (undocumented) + blobs: FilterBlobItem[]; + // (undocumented) + continuationToken?: string; + // (undocumented) + serviceEndpoint: string; + // (undocumented) + where: string; +} + // @public export function generateAccountSASQueryParameters(accountSASSignatureValues: AccountSASSignatureValues, sharedKeyCredential: StorageSharedKeyCredential): SASQueryParameters; @@ -1964,6 +2057,7 @@ export interface PageBlobCreateOptions extends CommonOptions { customerProvidedKey?: CpkInfo; encryptionScope?: string; metadata?: Metadata; + tags?: BlobTags; tier?: PremiumPageBlobTier | string; } @@ -2280,6 +2374,30 @@ export interface SequenceNumberAccessConditions { // @public export type SequenceNumberActionType = 'max' | 'update' | 'increment'; +// @public +export interface ServiceFilterBlobsHeaders { + clientRequestId?: string; + date?: Date; + // (undocumented) + errorCode?: string; + requestId?: string; + version?: string; +} + +// @public +export interface ServiceFindBlobByTagsOptions extends CommonOptions { + abortSignal?: AbortSignalLike; +} + +// @public +export type ServiceFindBlobsByTagsSegmentResponse = FilterBlobSegment & ServiceFilterBlobsHeaders & { + _response: coreHttp.HttpResponse & { + parsedHeaders: ServiceFilterBlobsHeaders; + bodyAsText: string; + parsedBody: FilterBlobSegment; + }; +}; + // @public export interface ServiceGetAccountInfoHeaders { accountKind?: AccountKind; diff --git a/sdk/storage/storage-blob/src/BlobDownloadResponse.ts b/sdk/storage/storage-blob/src/BlobDownloadResponse.ts index 1f11f951f733..4bb14770c1aa 100644 --- a/sdk/storage/storage-blob/src/BlobDownloadResponse.ts +++ b/sdk/storage/storage-blob/src/BlobDownloadResponse.ts @@ -322,6 +322,17 @@ export class BlobDownloadResponse implements BlobDownloadResponseModel { return this.originalResponse.etag; } + /** + * The number of tags associated with the blob + * + * @readonly + * @type {(number | undefined)} + * @memberof BlobDownloadResponse + */ + public get tagCount(): number | undefined { + return this.originalResponse.tagCount; + } + /** * The error code. * diff --git a/sdk/storage/storage-blob/src/BlobSASPermissions.ts b/sdk/storage/storage-blob/src/BlobSASPermissions.ts index 792577c496e1..6c12d7cd7639 100644 --- a/sdk/storage/storage-blob/src/BlobSASPermissions.ts +++ b/sdk/storage/storage-blob/src/BlobSASPermissions.ts @@ -46,6 +46,9 @@ export class BlobSASPermissions { case "x": blobSASPermissions.deleteVersion = true; break; + case "t": + blobSASPermissions.tag = true; + break; default: throw new RangeError(`Invalid permission: ${char}`); } @@ -102,6 +105,14 @@ export class BlobSASPermissions { */ public deleteVersion: boolean = false; + /** + * Specfies Tag access granted. + * + * @type {boolean} + * @memberof BlobSASPermissions + */ + public tag: boolean = false; + /** * Converts the given permissions to a string. Using this method will guarantee the permissions are in an * order accepted by the service. @@ -129,6 +140,9 @@ export class BlobSASPermissions { if (this.deleteVersion) { permissions.push("x"); } + if (this.tag) { + permissions.push("t"); + } return permissions.join(""); } } diff --git a/sdk/storage/storage-blob/src/BlobSASSignatureValues.ts b/sdk/storage/storage-blob/src/BlobSASSignatureValues.ts index c9970f3be8b7..25b7cfd0cafb 100644 --- a/sdk/storage/storage-blob/src/BlobSASSignatureValues.ts +++ b/sdk/storage/storage-blob/src/BlobSASSignatureValues.ts @@ -306,6 +306,7 @@ export function generateBlobSASQueryParameters( throw TypeError("Invalid sharedKeyCredential, userDelegationKey or accountName."); } + // Version 2019-12-12 adds support for the blob tags permission. // Version 2018-11-09 adds support for the signed resource and signed blob snapshot time fields. // https://docs.microsoft.com/en-us/rest/api/storageservices/constructing-a-service-sas#constructing-the-signature-string if (version >= "2018-11-09") { @@ -355,7 +356,8 @@ function generateBlobSASQueryParameters20150405( ): SASQueryParameters { if ( !blobSASSignatureValues.identifier && - !blobSASSignatureValues.permissions && !blobSASSignatureValues.expiresOn + !blobSASSignatureValues.permissions && + !blobSASSignatureValues.expiresOn ) { throw new RangeError( "Must provide 'permissions' and 'expiresOn' for Blob SAS generation when 'identifier' is not provided." @@ -374,6 +376,10 @@ function generateBlobSASQueryParameters20150405( throw RangeError("'version' must be >= '2019-10-10' when provided 'versionId'."); } + if (blobSASSignatureValues.permissions && blobSASSignatureValues.permissions.tag) { + throw RangeError("'version' must be >= '2019-12-12' when provided 't' permission."); + } + if (blobSASSignatureValues.blobName) { resource = "b"; } @@ -461,13 +467,22 @@ function generateBlobSASQueryParameters20181109( ): SASQueryParameters { if ( !blobSASSignatureValues.identifier && - !blobSASSignatureValues.permissions && !blobSASSignatureValues.expiresOn + !blobSASSignatureValues.permissions && + !blobSASSignatureValues.expiresOn ) { throw new RangeError( "Must provide 'permissions' and 'expiresOn' for Blob SAS generation when 'identifier' is not provided." ); } + if (blobSASSignatureValues.versionId) { + throw RangeError("'version' must be >= '2019-10-10' when provided 'versionId'."); + } + + if (blobSASSignatureValues.permissions && blobSASSignatureValues.permissions.tag) { + throw RangeError("'version' must be >= '2019-12-12' when provided 't' permission."); + } + const version = blobSASSignatureValues.version ? blobSASSignatureValues.version : SERVICE_VERSION; let resource: string = "c"; let verifiedPermissions: string | undefined; @@ -578,6 +593,10 @@ function generateBlobSASQueryParametersUDK20181109( ); } + if (blobSASSignatureValues.permissions && blobSASSignatureValues.permissions.tag) { + throw RangeError("'version' must be >= '2019-12-12' when provided 't' permission."); + } + const version = blobSASSignatureValues.version ? blobSASSignatureValues.version : SERVICE_VERSION; let resource: string = "c"; let verifiedPermissions: string | undefined; diff --git a/sdk/storage/storage-blob/src/BlobServiceClient.ts b/sdk/storage/storage-blob/src/BlobServiceClient.ts index 4ad33f0eb288..e969d543f92e 100644 --- a/sdk/storage/storage-blob/src/BlobServiceClient.ts +++ b/sdk/storage/storage-blob/src/BlobServiceClient.ts @@ -21,7 +21,9 @@ import { ServiceListContainersSegmentResponse, ContainerItem, ListContainersIncludeType, - UserDelegationKeyModel + UserDelegationKeyModel, + ServiceFindBlobsByTagsSegmentResponse, + FilterBlobItem } from "./generatedModels"; import { Service } from "./generated/src/operations"; import { newPipeline, StoragePipelineOptions, Pipeline } from "./Pipeline"; @@ -158,6 +160,34 @@ interface ServiceListContainersSegmentOptions extends CommonOptions { include?: ListContainersIncludeType | ListContainersIncludeType[]; } +/** + * Options to configure the {@link BlobServiceClient.findBlobsByTagsSegment} operation. + * + * @interface ServiceFindBlobsByTagsSegmentOptions + */ +interface ServiceFindBlobsByTagsSegmentOptions extends CommonOptions { + /** + * An implementation of the `AbortSignalLike` interface to signal the request to cancel the operation. + * For example, use the @azure/abort-controller to create an `AbortSignal`. + * + * @type {AbortSignalLike} + * @memberof ServiceFindBlobsByTagsSegmentOptions + */ + abortSignal?: AbortSignalLike; + /** + * Specifies the maximum number of blobs + * to return. If the request does not specify maxPageSize, or specifies a + * value greater than 5000, the server will return up to 5000 items. Note + * that if the listing operation crosses a partition boundary, then the + * service will return a continuation token for retrieving the remainder of + * the results. For this reason, it is possible that the service will return + * fewer results than specified by maxPageSize, or than the default of 5000. + * @type {number} + * @memberof ServiceFindBlobsByTagsSegmentOptions + */ + maxPageSize?: number; +} + /** * Options to configure the {@link BlobServiceClient.listContainers} operation. * @@ -185,6 +215,23 @@ export interface ServiceListContainersOptions extends CommonOptions { includeMetadata?: boolean; } +/** + * Options to configure the {@link BlobServiceClient.findBlobsByTags} operation. + * + * @export + * @interface ServiceFindBlobByTagsOptions + */ +export interface ServiceFindBlobByTagsOptions extends CommonOptions { + /** + * An implementation of the `AbortSignalLike` interface to signal the request to cancel the operation. + * For example, use the @azure/abort-controller to create an `AbortSignal`. + * + * @type {AbortSignalLike} + * @memberof ServiceListContainersOptions + */ + abortSignal?: AbortSignalLike; +} + /** * A user delegation key. */ @@ -669,6 +716,232 @@ export class BlobServiceClient extends StorageClient { } } + /** + * The Filter Blobs operation enables callers to list blobs across all containers whose tags + * match a given search expression. Filter blobs searches across all containers within a + * storage account but can be scoped within the expression to a single container. + * + * @private + * @param {string} tagFilterSqlExpression The where parameter enables the caller to query blobs whose tags match a given expression. + * The given expression must evaluate to true for a blob to be returned in the results. + * The[OData - ABNF] filter syntax rule defines the formal grammar for the value of the where query parameter; + * however, only a subset of the OData filter syntax is supported in the Blob service. + * @param {string} [marker] A string value that identifies the portion of + * the list of blobs to be returned with the next listing operation. The + * operation returns the NextMarker value within the response body if the + * listing operation did not return all blobs remaining to be listed + * with the current page. The NextMarker value can be used as the value for + * the marker parameter in a subsequent call to request the next page of list + * items. The marker value is opaque to the client. + * @param {ServiceFindBlobsByTagsSegmentOptions} [options={}] Options to find blobs by tags. + * @returns {Promise} + * @memberof BlobServiceClient + */ + private async findBlobsByTagsSegment( + tagFilterSqlExpression: string, + marker?: string, + options: ServiceFindBlobsByTagsSegmentOptions = {} + ): Promise { + // TODO: Rename response.blobs to response.blobItems? + const { span, spanOptions } = createSpan( + "BlobServiceClient-findBlobsByTagsSegment", + options.tracingOptions + ); + + try { + return await this.serviceContext.filterBlobs({ + abortSignal: options.abortSignal, + where: tagFilterSqlExpression, + marker, + maxPageSize: options.maxPageSize, + spanOptions + }); + } catch (e) { + span.setStatus({ + code: CanonicalCode.UNKNOWN, + message: e.message + }); + throw e; + } finally { + span.end(); + } + } + + /** + * Returns an AsyncIterableIterator for ServiceFindBlobsByTagsSegmentResponse. + * + * @private + * @param {string} tagFilterSqlExpression The where parameter enables the caller to query blobs whose tags match a given expression. + * The given expression must evaluate to true for a blob to be returned in the results. + * The[OData - ABNF] filter syntax rule defines the formal grammar for the value of the where query parameter; + * however, only a subset of the OData filter syntax is supported in the Blob service. + * @param {string} [marker] A string value that identifies the portion of + * the list of blobs to be returned with the next listing operation. The + * operation returns the NextMarker value within the response body if the + * listing operation did not return all blobs remaining to be listed + * with the current page. The NextMarker value can be used as the value for + * the marker parameter in a subsequent call to request the next page of list + * items. The marker value is opaque to the client. + * @param {ServiceFindBlobsByTagsSegmentOptions} [options={}] Options to find blobs by tags. + * @returns {AsyncIterableIterator} + * @memberof BlobServiceClient + */ + private async *findBlobsByTagsSegments( + tagFilterSqlExpression: string, + marker?: string, + options: ServiceFindBlobsByTagsSegmentOptions = {} + ): AsyncIterableIterator { + let response; + if (!!marker || marker === undefined) { + do { + response = await this.findBlobsByTagsSegment(tagFilterSqlExpression, marker, options); + response.blobs = response.blobs || []; + marker = response.continuationToken; + yield response; + } while (marker); + } + } + + /** + * Returns an AsyncIterableIterator for blobs. + * + * @private + * @param {string} tagFilterSqlExpression The where parameter enables the caller to query blobs whose tags match a given expression. + * The given expression must evaluate to true for a blob to be returned in the results. + * The[OData - ABNF] filter syntax rule defines the formal grammar for the value of the where query parameter; + * however, only a subset of the OData filter syntax is supported in the Blob service. + * @param {ServiceFindBlobsByTagsSegmentOptions} [options={}] Options to findBlobsByTagsItems. + * @returns {AsyncIterableIterator} + * @memberof BlobServiceClient + */ + private async *findBlobsByTagsItems( + tagFilterSqlExpression: string, + options: ServiceFindBlobsByTagsSegmentOptions = {} + ): AsyncIterableIterator { + let marker: string | undefined; + for await (const segment of this.findBlobsByTagsSegments( + tagFilterSqlExpression, + marker, + options + )) { + yield* segment.blobs; + } + } + + /** + * Returns an async iterable iterator to find all blobs with specified tag + * under the specified account. + * + * .byPage() returns an async iterable iterator to list the blobs in pages. + * + * Example using `for await` syntax: + * + * ```js + * let i = 1; + * for await (const blob of blobServiceClient.findBlobsByTags("tagkey='tagvalue'")) { + * console.log(`Blob ${i++}: ${container.name}`); + * } + * ``` + * + * Example using `iter.next()`: + * + * ```js + * let i = 1; + * const iter = blobServiceClient.findBlobsByTags("tagkey='tagvalue'"); + * let blobItem = await iter.next(); + * while (!blobItem.done) { + * console.log(`Blob ${i++}: ${blobItem.value.name}`); + * blobItem = await iter.next(); + * } + * ``` + * + * Example using `byPage()`: + * + * ```js + * // passing optional maxPageSize in the page settings + * let i = 1; + * for await (const response of blobServiceClient.findBlobsByTags("tagkey='tagvalue'").byPage({ maxPageSize: 20 })) { + * if (response.blobs) { + * for (const blob of response.blobs) { + * console.log(`Blob ${i++}: ${blob.name}`); + * } + * } + * } + * ``` + * + * Example using paging with a marker: + * + * ```js + * let i = 1; + * let iterator = blobServiceClient.findBlobsByTags("tagkey='tagvalue'").byPage({ maxPageSize: 2 }); + * let response = (await iterator.next()).value; + * + * // Prints 2 blob names + * if (response.blobs) { + * for (const blob of response.blobs) { + * console.log(`Blob ${i++}: ${blob.name}`); + * } + * } + * + * // Gets next marker + * let marker = response.continuationToken; + * // Passing next marker as continuationToken + * iterator = blobServiceClient + * .findBlobsByTags("tagkey='tagvalue'") + * .byPage({ continuationToken: marker, maxPageSize: 10 }); + * response = (await iterator.next()).value; + * + * // Prints blob names + * if (response.blobs) { + * for (const blob of response.blobs) { + * console.log(`Blob ${i++}: ${blob.name}`); + * } + * } + * ``` + * + * @param {string} tagFilterSqlExpression The where parameter enables the caller to query blobs whose tags match a given expression. + * The given expression must evaluate to true for a blob to be returned in the results. + * The[OData - ABNF] filter syntax rule defines the formal grammar for the value of the where query parameter; + * however, only a subset of the OData filter syntax is supported in the Blob service. + * @param {ServiceFindBlobByTagsOptions} [options={}] Options to find blobs by tags. + * @returns {PagedAsyncIterableIterator} + * @memberof BlobServiceClient + */ + public findBlobsByTags( + tagFilterSqlExpression: string, + options: ServiceFindBlobByTagsOptions = {} + ): PagedAsyncIterableIterator { + // AsyncIterableIterator to iterate over blobs + const listSegmentOptions: ServiceFindBlobsByTagsSegmentOptions = { + ...options + }; + + const iter = this.findBlobsByTagsItems(tagFilterSqlExpression, listSegmentOptions); + return { + /** + * @member {Promise} [next] The next method, part of the iteration protocol + */ + next() { + return iter.next(); + }, + /** + * @member {Symbol} [asyncIterator] The connection to the async iterator, part of the iteration protocol + */ + [Symbol.asyncIterator]() { + return this; + }, + /** + * @member {Function} [byPage] Return an AsyncIterableIterator that works a page at a time + */ + byPage: (settings: PageSettings = {}) => { + return this.findBlobsByTagsSegments(tagFilterSqlExpression, settings.continuationToken, { + maxPageSize: settings.maxPageSize, + ...listSegmentOptions + }); + } + }; + } + /** * Returns an AsyncIterableIterator for ServiceListContainersSegmentResponses * diff --git a/sdk/storage/storage-blob/src/Clients.ts b/sdk/storage/storage-blob/src/Clients.ts index 0e3df04274d5..3fda999920f2 100644 --- a/sdk/storage/storage-blob/src/Clients.ts +++ b/sdk/storage/storage-blob/src/Clients.ts @@ -1,142 +1,141 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import "@azure/core-paging"; + +import * as fs from "fs"; +import { Readable } from "stream"; + +import { AbortSignalLike } from "@azure/abort-controller"; import { + generateUuid, + getDefaultProxySettings, + HttpRequestBody, + HttpResponse, isNode, - TransferProgressEvent, - TokenCredential, isTokenCredential, - getDefaultProxySettings, + TokenCredential, + TransferProgressEvent, URLBuilder } from "@azure/core-http"; +import { PollerLike, PollOperationState } from "@azure/core-lro"; import { CanonicalCode } from "@opentelemetry/api"; + +import { BlobDownloadResponse } from "./BlobDownloadResponse"; +import { BlobQueryResponse } from "./BlobQueryResponse"; +import { AnonymousCredential } from "./credentials/AnonymousCredential"; +import { StorageSharedKeyCredential } from "./credentials/StorageSharedKeyCredential"; import { - BlobDownloadResponseModel, - CpkInfo, - DeleteSnapshotsOptionType, - ModifiedAccessConditions, - RehydratePriority, - LeaseAccessConditions, + AppendBlob, + Blob as StorageBlob, + BlockBlob, + Container, + PageBlob +} from "./generated/src/operations"; +import { StorageClientContext } from "./generated/src/storageClient"; +import { + AppendBlobAppendBlockFromUrlResponse, + AppendBlobAppendBlockResponse, + AppendBlobCreateResponse, + BlobAbortCopyFromURLResponse, + BlobCopyFromURLResponse, + BlobCreateSnapshotResponse, + BlobDeleteResponse, BlobDownloadOptionalParams, + BlobDownloadResponseModel, BlobGetPropertiesResponse, - BlobDeleteResponse, - BlobUndeleteResponse, BlobHTTPHeaders, + BlobItem, + BlobPrefix, BlobSetHTTPHeadersResponse, BlobSetMetadataResponse, - BlobCreateSnapshotResponse, - BlobStartCopyFromURLResponse, - BlobAbortCopyFromURLResponse, - BlobCopyFromURLResponse, + BlobSetTagsResponse, BlobSetTierResponse, - ContainerEncryptionScope + BlobStartCopyFromURLResponse, + BlobTags, + BlobUndeleteResponse, + BlockBlobCommitBlockListResponse, + BlockBlobGetBlockListResponse, + BlockBlobStageBlockFromURLResponse, + BlockBlobStageBlockResponse, + BlockBlobUploadHeaders, + BlockBlobUploadResponse, + BlockListType, + ContainerBreakLeaseOptionalParams, + ContainerCreateResponse, + ContainerDeleteResponse, + ContainerEncryptionScope, + ContainerGetAccessPolicyHeaders, + ContainerGetPropertiesResponse, + ContainerListBlobFlatSegmentResponse, + ContainerListBlobHierarchySegmentResponse, + ContainerSetAccessPolicyResponse, + ContainerSetMetadataResponse, + CpkInfo, + DeleteSnapshotsOptionType, + LeaseAccessConditions, + ListBlobsIncludeItem, + ModifiedAccessConditions, + PageBlobClearPagesResponse, + PageBlobCopyIncrementalResponse, + PageBlobCreateResponse, + PageBlobResizeResponse, + PageBlobUpdateSequenceNumberResponse, + PageBlobUploadPagesFromURLResponse, + PageBlobUploadPagesResponse, + PublicAccessType, + RehydratePriority, + SequenceNumberActionType, + SignedIdentifierModel, + BlobGetTagsResponse } from "./generatedModels"; -import { AbortSignalLike } from "@azure/abort-controller"; -import { BlobDownloadResponse } from "./BlobDownloadResponse"; -import { Blob as StorageBlob } from "./generated/src/operations"; -import { rangeToString } from "./Range"; import { + AppendBlobRequestConditions, BlobRequestConditions, - Metadata, - ensureCpkIfSpecified, BlockBlobTier, + ensureCpkIfSpecified, + Metadata, + PageBlobRequestConditions, PremiumPageBlobTier, toAccessTier } from "./models"; -import { newPipeline, StoragePipelineOptions, Pipeline } from "./Pipeline"; -import { - DEFAULT_MAX_DOWNLOAD_RETRY_REQUESTS, - URLConstants, - DEFAULT_BLOB_DOWNLOAD_BLOCK_BYTES, - DEFAULT_BLOCK_BUFFER_SIZE_BYTES -} from "./utils/constants"; -import { - setURLParameter, - extractConnectionStringParts, - appendToURLPath, - toQuerySerialization -} from "./utils/utils.common"; -import { fsStat, readStreamToLocalFile, streamToBuffer } from "./utils/utils.node"; -import { StorageSharedKeyCredential } from "./credentials/StorageSharedKeyCredential"; -import { AnonymousCredential } from "./credentials/AnonymousCredential"; -import { Batch } from "./utils/Batch"; -import { createSpan } from "./utils/tracing"; -import { HttpRequestBody } from "@azure/core-http"; -import { - AppendBlobCreateResponse, - AppendBlobAppendBlockFromUrlResponse, - AppendBlobAppendBlockResponse -} from "./generatedModels"; -import { AppendBlob } from "./generated/src/operations"; -import { AppendBlobRequestConditions } from "./models"; -import { CommonOptions, StorageClient } from "./StorageClient"; -import * as fs from "fs"; -import { generateUuid, HttpResponse } from "@azure/core-http"; -import { - BlockBlobUploadHeaders, - BlockBlobUploadResponse, - BlockBlobStageBlockResponse, - BlockBlobStageBlockFromURLResponse, - BlockBlobCommitBlockListResponse, - BlockBlobGetBlockListResponse, - BlockListType -} from "./generatedModels"; -import { BlockBlob } from "./generated/src/operations"; -import { Range } from "./Range"; -import { generateBlockID } from "./utils/utils.common"; -import { - BLOCK_BLOB_MAX_STAGE_BLOCK_BYTES, - BLOCK_BLOB_MAX_UPLOAD_BLOB_BYTES, - BLOCK_BLOB_MAX_BLOCKS -} from "./utils/constants"; -import { BufferScheduler } from "./utils/BufferScheduler"; -import { Readable } from "stream"; -import { - PageBlobCreateResponse, - PageBlobUploadPagesResponse, - PageBlobUploadPagesFromURLResponse, - PageBlobClearPagesResponse, - PageBlobResizeResponse, - SequenceNumberActionType, - PageBlobUpdateSequenceNumberResponse, - PageBlobCopyIncrementalResponse -} from "./generatedModels"; -import { PageBlob } from "./generated/src/operations"; -import { PageBlobRequestConditions } from "./models"; import { PageBlobGetPageRangesDiffResponse, PageBlobGetPageRangesResponse, rangeResponseFromModel } from "./PageBlobRangeResponse"; +import { newPipeline, Pipeline, StoragePipelineOptions } from "./Pipeline"; import { BlobBeginCopyFromUrlPoller, BlobBeginCopyFromUrlPollState, CopyPollerBlobClient } from "./pollers/BlobStartCopyFromUrlPoller"; -import { PollerLike, PollOperationState } from "@azure/core-lro"; -import { ContainerBreakLeaseOptionalParams } from "./generatedModels"; -import { StorageClientContext } from "./generated/src/storageClient"; +import { Range, rangeToString } from "./Range"; +import { CommonOptions, StorageClient } from "./StorageClient"; +import { Batch } from "./utils/Batch"; +import { BufferScheduler } from "./utils/BufferScheduler"; import { - ContainerGetAccessPolicyHeaders, - SignedIdentifierModel, - PublicAccessType, - ListBlobsIncludeItem, - ContainerCreateResponse, - ContainerGetPropertiesResponse, - ContainerDeleteResponse, - ContainerSetMetadataResponse, - ContainerSetAccessPolicyResponse, - ContainerListBlobFlatSegmentResponse, - ContainerListBlobHierarchySegmentResponse, - BlobItem, - BlobPrefix -} from "./generatedModels"; -import { Container } from "./generated/src/operations"; -import { ETagNone } from "./utils/constants"; -import { truncatedISO8061Date } from "./utils/utils.common"; -import "@azure/core-paging"; -import { PagedAsyncIterableIterator, PageSettings } from "@azure/core-paging"; -import { BlobQueryResponse } from "./BlobQueryResponse"; + BLOCK_BLOB_MAX_BLOCKS, + BLOCK_BLOB_MAX_STAGE_BLOCK_BYTES, + BLOCK_BLOB_MAX_UPLOAD_BLOB_BYTES, + DEFAULT_BLOB_DOWNLOAD_BLOCK_BYTES, + DEFAULT_BLOCK_BUFFER_SIZE_BYTES, + DEFAULT_MAX_DOWNLOAD_RETRY_REQUESTS, + ETagNone, + URLConstants +} from "./utils/constants"; +import { createSpan } from "./utils/tracing"; +import { + appendToURLPath, + extractConnectionStringParts, + generateBlockID, + setURLParameter, + toBlobTagsString, + toQuerySerialization, + truncatedISO8061Date +} from "./utils/utils.common"; +import { fsStat, readStreamToLocalFile, streamToBuffer } from "./utils/utils.node"; +import { PageSettings, PagedAsyncIterableIterator } from "@azure/core-paging"; /** * Options to configure the {@link BlobClient.beginCopyFromURL} operation. @@ -461,6 +460,40 @@ export interface BlobSetMetadataOptions extends CommonOptions { encryptionScope?: string; } +/** + * Options to configure the {@link BlobClient.setTags} operation. + * + * @export + * @interface BlobSetTagsOptions + */ +export interface BlobSetTagsOptions extends CommonOptions { + /** + * An implementation of the `AbortSignalLike` interface to signal the request to cancel the operation. + * For example, use the @azure/abort-controller to create an `AbortSignal`. + * + * @type {AbortSignalLike} + * @memberof BlobSetTagsOptions + */ + abortSignal?: AbortSignalLike; +} + +/** + * Options to configure the {@link BlobClient.getTags} operation. + * + * @export + * @interface BlobGetTagsOptions + */ +export interface BlobGetTagsOptions extends CommonOptions { + /** + * An implementation of the `AbortSignalLike` interface to signal the request to cancel the operation. + * For example, use the @azure/abort-controller to create an `AbortSignal`. + * + * @type {AbortSignalLike} + * @memberof BlobGetTagsOptions + */ + abortSignal?: AbortSignalLike; +} + /** * Options to configure Blob - Acquire Lease operation. * @@ -681,6 +714,13 @@ export interface BlobStartCopyFromURLOptions extends CommonOptions { * @memberof BlobStartCopyFromURLOptions */ rehydratePriority?: RehydratePriority; + /** + * Blob tags. + * + * @type {BlobTags} + * @memberof BlobStartCopyFromURLOptions + */ + tags?: BlobTags; } /** @@ -751,6 +791,13 @@ export interface BlobSyncCopyFromURLOptions extends CommonOptions { * @memberof BlobSyncCopyFromURLOptions */ sourceContentMD5?: Uint8Array; + /** + * Blob tags. + * + * @type {BlobTags} + * @memberof BlobSyncCopyFromURLOptions + */ + tags?: BlobTags; } /** @@ -1497,6 +1544,64 @@ export class BlobClient extends StorageClient { } } + /** + * Sets tags on the underlying blob. + * A blob can have up to 10 tags. Tag keys must be between 1 and 128 characters. Tag values must be between 0 and 256 characters. + * Valid tag key and value characters include lower and upper case letters, digits (0-9), + * space (' '), plus ('+'), minus ('-'), period ('.'), foward slash ('/'), colon (':'), equals ('='), and underscore ('_'). + * + * @param {BlobTags} tags + * @param {BlobSetTagsOptions} [options={}] + * @returns {Promise} + * @memberof BlobClient + */ + public async setTags( + tags: BlobTags, + options: BlobSetTagsOptions = {} + ): Promise { + const { span, spanOptions } = createSpan("BlobClient-setTags", options.tracingOptions); + try { + return await this.blobContext.setTags({ + abortSignal: options.abortSignal, + spanOptions, + tags + }); + } catch (e) { + span.setStatus({ + code: CanonicalCode.UNKNOWN, + message: e.message + }); + throw e; + } finally { + span.end(); + } + } + + /** + * Gets the tags associated with the underlying blob. + * + * @param {BlobGetTagsOptions} [options={}] + * @returns {Promise} + * @memberof BlobClient + */ + public async getTags(options: BlobGetTagsOptions = {}): Promise { + const { span, spanOptions } = createSpan("BlobClient-getTags", options.tracingOptions); + try { + return await this.blobContext.getTags({ + abortSignal: options.abortSignal, + spanOptions + }); + } catch (e) { + span.setStatus({ + code: CanonicalCode.UNKNOWN, + message: e.message + }); + throw e; + } finally { + span.end(); + } + } + /** * Get a {@link BlobLeaseClient} that manages leases on the blob. * @@ -1705,6 +1810,7 @@ export class BlobClient extends StorageClient { sourceIfUnmodifiedSince: options.sourceConditions.ifUnmodifiedSince }, sourceContentMD5: options.sourceContentMD5, + blobTagsString: toBlobTagsString(options.tags), spanOptions }); } catch (e) { @@ -2059,6 +2165,7 @@ export class BlobClient extends StorageClient { }, rehydratePriority: options.rehydratePriority, tier: toAccessTier(options.tier), + blobTagsString: toBlobTagsString(options.tags), spanOptions }); } catch (e) { @@ -2127,6 +2234,13 @@ export interface AppendBlobCreateOptions extends CommonOptions { * @memberof AppendBlobCreateOptions */ encryptionScope?: string; + /** + * Blob tags. + * + * @type {BlobTags} + * @memberof AppendBlobCreateOptions + */ + tags?: BlobTags; } /** @@ -2480,6 +2594,7 @@ export class AppendBlobClient extends BlobClient { modifiedAccessConditions: options.conditions, cpkInfo: options.customerProvidedKey, encryptionScope: options.encryptionScope, + blobTagsString: toBlobTagsString(options.tags), spanOptions }); } catch (e) { @@ -2683,6 +2798,13 @@ export interface BlockBlobUploadOptions extends CommonOptions { * @memberof BlockBlobUploadOptions */ tier?: BlockBlobTier | string; + /** + * Blob tags. + * + * @type {BlobTags} + * @memberof BlockBlobUploadOptions + */ + tags?: BlobTags; } /** @@ -3058,6 +3180,14 @@ export interface BlockBlobCommitBlockListOptions extends CommonOptions { * @memberof BlockBlobCommitBlockListOptions */ tier?: BlockBlobTier | string; + + /** + * Blob tags. + * + * @type {BlobTags} + * @memberof BlockBlobCommitBlockListOptions + */ + tags?: BlobTags; } /** @@ -3143,6 +3273,14 @@ export interface BlockBlobUploadStreamOptions extends CommonOptions { * @memberof BlockBlobUploadStreamOptions */ encryptionScope?: string; + + /** + * Blob tags. + * + * @type {BlobTags} + * @memberof BlockBlobUploadStreamOptions + */ + tags?: BlobTags; } /** * Option interface for {@link BlockBlobClient.uploadFile} and {@link BlockBlobClient.uploadSeekableStream}. @@ -3229,6 +3367,14 @@ export interface BlockBlobParallelUploadOptions extends CommonOptions { * @memberof BlockBlobParallelUploadOptions */ encryptionScope?: string; + + /** + * Blob tags. + * + * @type {BlobTags} + * @memberof BlockBlobParallelUploadOptions + */ + tags?: BlobTags; } /** @@ -3557,6 +3703,7 @@ export class BlockBlobClient extends BlobClient { cpkInfo: options.customerProvidedKey, encryptionScope: options.encryptionScope, tier: toAccessTier(options.tier), + blobTagsString: toBlobTagsString(options.tags), spanOptions }); } catch (e) { @@ -3703,6 +3850,7 @@ export class BlockBlobClient extends BlobClient { cpkInfo: options.customerProvidedKey, encryptionScope: options.encryptionScope, tier: toAccessTier(options.tier), + blobTagsString: toBlobTagsString(options.tags), spanOptions } ); @@ -4266,6 +4414,13 @@ export interface PageBlobCreateOptions extends CommonOptions { * @memberof PageBlobCreateOptions */ tier?: PremiumPageBlobTier | string; + /** + * Blob tags. + * + * @type {BlobTags} + * @memberof PageBlobCreateOptions + */ + tags?: BlobTags; } /** @@ -4788,6 +4943,7 @@ export class PageBlobClient extends BlobClient { cpkInfo: options.customerProvidedKey, encryptionScope: options.encryptionScope, tier: toAccessTier(options.tier), + blobTagsString: toBlobTagsString(options.tags), spanOptions }); } catch (e) { @@ -5983,6 +6139,10 @@ export interface ContainerListBlobsOptions extends CommonOptions { * Specifies whether blobs for which blocks have been uploaded, but which have not been committed using Put Block List, be included in the response. */ includeUncommitedBlobs?: boolean; + /** + * Specifies whether blob tags be returned in the response. + */ + includeTags?: boolean; } /** @@ -6878,6 +7038,9 @@ export class ContainerClient extends StorageClient { if (options.includeUncommitedBlobs) { include.push("uncommittedblobs"); } + if (options.includeTags) { + include.push("tags"); + } if (options.prefix === "") { options.prefix = undefined; } @@ -6929,7 +7092,8 @@ export class ContainerClient extends StorageClient { * @param {ContainerListBlobsSegmentOptions} [options] Options to list blobs operation. * @returns {AsyncIterableIterator} * @memberof ContainerClient - */ private async *listHierarchySegments( + */ + private async *listHierarchySegments( delimiter: string, marker?: string, options: ContainerListBlobsSegmentOptions = {} @@ -7086,6 +7250,9 @@ export class ContainerClient extends StorageClient { if (options.includeUncommitedBlobs) { include.push("uncommittedblobs"); } + if (options.includeTags) { + include.push("tags"); + } if (options.prefix === "") { options.prefix = undefined; } diff --git a/sdk/storage/storage-blob/src/ContainerSASPermissions.ts b/sdk/storage/storage-blob/src/ContainerSASPermissions.ts index c4ea3fc04731..299ba0df129c 100644 --- a/sdk/storage/storage-blob/src/ContainerSASPermissions.ts +++ b/sdk/storage/storage-blob/src/ContainerSASPermissions.ts @@ -44,6 +44,9 @@ export class ContainerSASPermissions { case "l": containerSASPermissions.list = true; break; + case "t": + containerSASPermissions.tag = true; + break; default: throw new RangeError(`Invalid permission ${char}`); } @@ -100,6 +103,14 @@ export class ContainerSASPermissions { */ public list: boolean = false; + /** + * Specfies Tag access granted. + * + * @type {boolean} + * @memberof BlobSASPermissions + */ + public tag: boolean = false; + /** * Converts the given permissions to a string. Using this method will guarantee the permissions are in an * order accepted by the service. @@ -130,6 +141,9 @@ export class ContainerSASPermissions { if (this.list) { permissions.push("l"); } + if (this.tag) { + permissions.push("t"); + } return permissions.join(""); } } diff --git a/sdk/storage/storage-blob/src/generatedModels.ts b/sdk/storage/storage-blob/src/generatedModels.ts index ebd570f16948..014208f40a72 100644 --- a/sdk/storage/storage-blob/src/generatedModels.ts +++ b/sdk/storage/storage-blob/src/generatedModels.ts @@ -26,6 +26,7 @@ export { BlobHTTPHeaders, BlobSetHTTPHeadersResponse, BlobSetMetadataResponse, + BlobSetTagsResponse, BlobCreateSnapshotResponse, BlobFlatListSegment, BlobStartCopyFromURLHeaders, @@ -36,6 +37,7 @@ export { BlobSetMetadataHeaders, BlobSetTierHeaders, BlobSetTierResponse, + BlobSetTagsHeaders, BlobHierarchyListSegment, BlobItemInternal as BlobItem, BlobPrefix, @@ -43,6 +45,7 @@ export { BlobQueryResponse as BlobQueryResponseModel, BlobDownloadResponse as BlobDownloadResponseModel, BlobType, + BlobTags, BlobUndeleteHeaders, Block, BlockBlobCommitBlockListHeaders, @@ -59,6 +62,9 @@ export { BlockBlobGetBlockListResponse, BlobServiceProperties, BlobServiceStatistics, + BlobGetTagsHeaders, + BlobGetTagsResponse, + BlobTag, ContainerCreateHeaders, ContainerCreateResponse, ContainerDeleteHeaders, @@ -91,6 +97,8 @@ export { ListBlobsIncludeItem, ListContainersIncludeType, ListContainersSegmentResponse, + FilterBlobSegment, + ServiceFilterBlobsHeaders, Logging, Metrics, ModifiedAccessConditions, @@ -138,5 +146,7 @@ export { ServiceSubmitBatchOptionalParams as ServiceSubmitBatchOptionalParamsModel, SignedIdentifier as SignedIdentifierModel, UserDelegationKey as UserDelegationKeyModel, - ContainerEncryptionScope + ContainerEncryptionScope, + ServiceFilterBlobsResponse as ServiceFindBlobsByTagsSegmentResponse, + FilterBlobItem } from "./generated/src/models"; diff --git a/sdk/storage/storage-blob/src/utils/utils.common.ts b/sdk/storage/storage-blob/src/utils/utils.common.ts index 917424b68477..ee3b27de268d 100644 --- a/sdk/storage/storage-blob/src/utils/utils.common.ts +++ b/sdk/storage/storage-blob/src/utils/utils.common.ts @@ -5,7 +5,7 @@ import { AbortSignalLike } from "@azure/abort-controller"; import { HttpHeaders, isNode, URLBuilder } from "@azure/core-http"; import { BlobQueryCsvTextConfiguration, BlobQueryJsonTextConfiguration } from "../Clients"; -import { QuerySerialization } from "../generated/src/models"; +import { QuerySerialization, BlobTags } from "../generated/src/models"; import { DevelopmentConnectionString, HeaderConstants, URLConstants } from "./constants"; /** @@ -556,6 +556,26 @@ export function getAccountNameFromUrl(url: string): string { } } +/** + * Convert BlobTags to encoded string. + * + * @export + * @param {BlobTags} tags + * @returns {string | undefined} + */ +export function toBlobTagsString(tags?: BlobTags): string | undefined { + if (tags === undefined || tags.blobTagSet.length === 0) { + return undefined; + } + + const tagParis = []; + for (const blobTag of tags.blobTagSet) { + tagParis.push(`${encodeURIComponent(blobTag.key)}=${encodeURIComponent(blobTag.value)}`); + } + + return tagParis.join("&"); +} + /** * Convert BlobQueryTextConfiguration to QuerySerialization type. * diff --git a/sdk/storage/storage-blob/test/blobclient.spec.ts b/sdk/storage/storage-blob/test/blobclient.spec.ts index faf6b862b9c6..96853ea734b7 100644 --- a/sdk/storage/storage-blob/test/blobclient.spec.ts +++ b/sdk/storage/storage-blob/test/blobclient.spec.ts @@ -52,6 +52,98 @@ describe("BlobClient", () => { } }); + it.only("Set blob tags should work", async () => { + const tags = { + blobTagSet: [ + { key: "tag1", value: "val1" }, + { key: "tag2", value: "val2" } + ] + }; + await blockBlobClient.setTags(tags); + + const response = await blockBlobClient.getTags(); + assert.deepStrictEqual(response.blobTagSet, tags.blobTagSet); + + const properties = await blockBlobClient.getProperties(); + assert.deepStrictEqual(properties.tagCount, 2); + + const download = await blockBlobClient.download(); + assert.deepStrictEqual(download.tagCount, 2); + + const listblob = containerClient.listBlobsFlat({ includeTags: true }); + + const iter = listblob.byPage(); + const segment = await iter.next(); + + // TODO: Make blob tag type consistency cross all request or response + assert.deepStrictEqual(segment.value.segment.blobItems[0].blobTags.blobTagSet, tags.blobTagSet); + }); + + it.only("Get blob tags should work with a snapshot", async () => { + const tags = { + blobTagSet: [ + { key: "tag1", value: "val1" }, + { key: "tag2", value: "val2" } + ] + }; + await blockBlobClient.setTags(tags); + + const snapshotResponse = await blockBlobClient.createSnapshot(); + const blockBlobClientSnapshot = blockBlobClient.withSnapshot(snapshotResponse.snapshot!); + + const response = await blockBlobClientSnapshot.getTags(); + assert.deepStrictEqual(response.blobTagSet, tags.blobTagSet); + }); + + it.only("Create block blob blob should work with tags", async () => { + await blockBlobClient.delete(); + + const tags = { + blobTagSet: [ + { key: "tag1", value: "val1" }, + { key: "tag2", value: "val2" } + ] + }; + await blockBlobClient.upload("hello", 5, { tags }); + + const response = await blockBlobClient.getTags(); + assert.deepStrictEqual(response.blobTagSet, tags.blobTagSet); + }); + + it.only("Create append blob should work with tags", async () => { + await blockBlobClient.delete(); + + const tags = { + blobTagSet: [ + { key: "tag1", value: "val1" }, + { key: "tag2", value: "val2" } + ] + }; + + const appendBlobClient = blobClient.getAppendBlobClient(); + await appendBlobClient.create({ tags }); + + const response = await appendBlobClient.getTags(); + assert.deepStrictEqual(response.blobTagSet, tags.blobTagSet); + }); + + it.only("Create page blob should work with tags", async () => { + await blockBlobClient.delete(); + + const tags = { + blobTagSet: [ + { key: "tag1", value: "val1" }, + { key: "tag2", value: "val2" } + ] + }; + + const pageBlobClient = blobClient.getPageBlobClient(); + await pageBlobClient.create(512, { tags }); + + const response = await pageBlobClient.getTags(); + assert.deepStrictEqual(response.blobTagSet, tags.blobTagSet); + }); + it("download with with default parameters", async () => { const result = await blobClient.download(); assert.deepStrictEqual(await bodyToString(result, content.length), content); diff --git a/sdk/storage/storage-blob/test/blobserviceclient.spec.ts b/sdk/storage/storage-blob/test/blobserviceclient.spec.ts index 06fd1574700e..f98ef015650e 100644 --- a/sdk/storage/storage-blob/test/blobserviceclient.spec.ts +++ b/sdk/storage/storage-blob/test/blobserviceclient.spec.ts @@ -7,7 +7,8 @@ import { getBSU, getSASConnectionStringFromEnvironment, getTokenBSU, - recorderEnvSetup + recorderEnvSetup, + sleep } from "./utils"; import { record, delay, Recorder } from "@azure/test-utils-recorder"; dotenv.config(); @@ -466,4 +467,82 @@ describe("BlobServiceClient", () => { assert.notDeepStrictEqual(response.signedObjectId, undefined); assert.notDeepStrictEqual(response.signedExpiresOn, undefined); }); + + it.only("Find blob by tags should work", async () => { + const blobServiceClient = getBSU(); + + const containerName = recorder.getUniqueName("container1"); + const containerClient = blobServiceClient.getContainerClient(containerName); + await containerClient.create(); + + const key1 = recorder.getUniqueName("key"); + const key2 = recorder.getUniqueName("key2"); + + const blobName1 = recorder.getUniqueName("blobname1"); + const appendBlobClient1 = containerClient.getAppendBlobClient(blobName1); + const tags1 = { + blobTagSet: [ + { key: key1, value: recorder.getUniqueName("val1") }, + { key: key2, value: "default" } + ] + }; + await appendBlobClient1.create({ tags: tags1 }); + + const blobName2 = recorder.getUniqueName("blobname2"); + const appendBlobClient2 = containerClient.getAppendBlobClient(blobName2); + const tags2 = { + blobTagSet: [ + { key: key1, value: recorder.getUniqueName("val2") }, + { key: key2, value: "default" } + ] + }; + await appendBlobClient2.create({ tags: tags2 }); + + const blobName3 = recorder.getUniqueName("blobname3"); + const appendBlobClient3 = containerClient.getAppendBlobClient(blobName3); + const tags3 = { + blobTagSet: [ + { key: key1, value: recorder.getUniqueName("val3") }, + { key: key2, value: "default" } + ] + }; + await appendBlobClient3.create({ tags: tags3 }); + + // Wait for indexing tags + await sleep(2); + + for await (const blob of blobServiceClient.findBlobsByTags( + `${tags1.blobTagSet[0].key}='${tags1.blobTagSet[0].value}'` + )) { + assert.deepStrictEqual(blob.containerName, containerName); + assert.deepStrictEqual(blob.name, blobName1); + assert.deepStrictEqual(blob.tagValue, tags1.blobTagSet[0].value); + } + + const blobs = []; + for await (const segment of blobServiceClient + .findBlobsByTags(`${tags2.blobTagSet[0].key}='${tags2.blobTagSet[0].value}'`) + .byPage()) { + for (const blob of segment.blobs) { + blobs.push(blob); + } + } + assert.deepStrictEqual(blobs.length, 1); + assert.deepStrictEqual(blobs[0].containerName, containerName); + assert.deepStrictEqual(blobs[0].name, blobName2); + assert.deepStrictEqual(blobs[0].tagValue, tags2.blobTagSet[0].value); + + const blobsWithTag2 = []; + for await (const segment of blobServiceClient.findBlobsByTags(`${key2}='default'`).byPage({ + maxPageSize: 1 + })) { + assert.ok(segment.blobs.length <= 1); + for (const blob of segment.blobs) { + blobsWithTag2.push(blob); + } + } + assert.deepStrictEqual(blobsWithTag2.length, 3); + + await containerClient.delete(); + }); }); diff --git a/sdk/storage/storage-blob/test/browser/highlevel.browser.spec.ts b/sdk/storage/storage-blob/test/browser/highlevel.browser.spec.ts index 73615d5d21fc..8288d3f0ae63 100644 --- a/sdk/storage/storage-blob/test/browser/highlevel.browser.spec.ts +++ b/sdk/storage/storage-blob/test/browser/highlevel.browser.spec.ts @@ -154,6 +154,26 @@ describe("Highlevel", () => { assert.equal(uploadedString, downloadedString); }); + it.only("uploadBrowserDataToBlockBlob should work with tags", async () => { + recorder.skip("browser", "Temp file - recorder doesn't support saving the file"); + + const tags = { + blobTagSet: [ + { key: "tag1", value: "val1" }, + { key: "tag2", value: "val2" } + ] + }; + + await blockBlobClient.uploadBrowserData(tempFile2, { + blockSize: 512 * 1024, + maxSingleShotSize: 0, + tags + }); + + const response = await blockBlobClient.getTags(); + assert.deepStrictEqual(response.blobTagSet, tags.blobTagSet); + }); + it("uploadBrowserDataToBlockBlob should success when blob >= BLOCK_BLOB_MAX_UPLOAD_BLOB_BYTES", async function() { recorder.skip("browser", "Temp file - recorder doesn't support saving the file"); if (isIE()) { diff --git a/sdk/storage/storage-blob/test/node/highlevel.node.spec.ts b/sdk/storage/storage-blob/test/node/highlevel.node.spec.ts index e38b26a11c18..9285014f61a1 100644 --- a/sdk/storage/storage-blob/test/node/highlevel.node.spec.ts +++ b/sdk/storage/storage-blob/test/node/highlevel.node.spec.ts @@ -99,6 +99,26 @@ describe("Highlevel", () => { assert.ok(downloadedData.equals(uploadedData)); }).timeout(timeoutForLargeFileUploadingTest); + it.only("uploadFile should work with tags", async () => { + recorder.skip("node", "Temp file - recorder doesn't support saving the file"); + + const tags = { + blobTagSet: [ + { key: "tag1", value: "val1" }, + { key: "tag2", value: "val2" } + ] + }; + + await blockBlobClient.uploadFile(tempFileSmall, { + blockSize: 4 * 1024 * 1024, + concurrency: 20, + tags + }); + + const response = await blockBlobClient.getTags(); + assert.deepStrictEqual(response.blobTagSet, tags.blobTagSet); + }); + it("uploadFile should success when blob < BLOCK_BLOB_MAX_UPLOAD_BLOB_BYTES", async () => { recorder.skip("node", "Temp file - recorder doesn't support saving the file"); await blockBlobClient.uploadFile(tempFileSmall, { @@ -262,6 +282,26 @@ describe("Highlevel", () => { fs.unlinkSync(downloadFilePath); }); + it.only("uploadStream should work with tags", async () => { + recorder.skip("node", "Temp file - recorder doesn't support saving the file"); + + const buf = Buffer.from([0x62, 0x75, 0x66, 0x66, 0x65, 0x72]); + const bufferStream = new PassThrough(); + bufferStream.end(buf); + + const tags = { + blobTagSet: [ + { key: "tag1", value: "val1" }, + { key: "tag2", value: "val2" } + ] + }; + + await blockBlobClient.uploadStream(bufferStream, 4 * 1024 * 1024, 20, { tags }); + + const response = await blockBlobClient.getTags(); + assert.deepStrictEqual(response.blobTagSet, tags.blobTagSet); + }); + it("uploadStream should abort", async () => { recorder.skip("node", "Temp file - recorder doesn't support saving the file"); const rs = fs.createReadStream(tempFileLarge); diff --git a/sdk/storage/storage-blob/test/node/sas.spec.ts b/sdk/storage/storage-blob/test/node/sas.spec.ts index c9b9fb30ce46..59c4f5e8f165 100644 --- a/sdk/storage/storage-blob/test/node/sas.spec.ts +++ b/sdk/storage/storage-blob/test/node/sas.spec.ts @@ -324,6 +324,68 @@ describe("Shared Access Signature (SAS) generation Node.js only", () => { await containerClient.delete(); }); + it.only("generateBlobSASQueryParameters should work for blob tags", async () => { + const now = recorder.newDate("now"); + now.setMinutes(now.getMinutes() - 5); // Skip clock skew with server + + const tmr = recorder.newDate("tmr"); + tmr.setDate(tmr.getDate() + 1); + + // By default, credential is always the last element of pipeline factories + const factories = (blobServiceClient as any).pipeline.factories; + const sharedKeyCredential = factories[factories.length - 1]; + + const containerName = recorder.getUniqueName("container"); + const containerClient = blobServiceClient.getContainerClient(containerName); + await containerClient.create(); + + const blobName = recorder.getUniqueName("blob"); + const blobClient = containerClient.getPageBlobClient(blobName); + await blobClient.create(1024, { + blobHTTPHeaders: { + blobContentType: "content-type-original" + } + }); + + const blobSAS = generateBlobSASQueryParameters( + { + blobName: blobClient.name, + cacheControl: "cache-control-override", + containerName: blobClient.containerName, + contentDisposition: "content-disposition-override", + contentEncoding: "content-encoding-override", + contentLanguage: "content-language-override", + contentType: "content-type-override", + expiresOn: tmr, + ipRange: { start: "0.0.0.0", end: "255.255.255.255" }, + permissions: BlobSASPermissions.parse("racwdt"), + protocol: SASProtocol.HttpsAndHttp, + startsOn: now + }, + sharedKeyCredential as StorageSharedKeyCredential + ); + + const sasURL = `${blobClient.url}?${blobSAS}`; + const blobClientWithSAS = new PageBlobClient(sasURL, newPipeline(new AnonymousCredential())); + + const tags = { + blobTagSet: [ + { key: "tag1", value: "val1" }, + { key: "tag2", value: "val2" } + ] + }; + await blobClientWithSAS.setTags(tags); + + const properties = await blobClientWithSAS.getProperties(); + assert.equal(properties.cacheControl, "cache-control-override"); + assert.equal(properties.contentDisposition, "content-disposition-override"); + assert.equal(properties.contentEncoding, "content-encoding-override"); + assert.equal(properties.contentLanguage, "content-language-override"); + assert.equal(properties.contentType, "content-type-override"); + + await containerClient.delete(); + }); + it("generateBlobSASQueryParameters should work for blob snapshot", async () => { const now = recorder.newDate("now"); now.setMinutes(now.getMinutes() - 5); // Skip clock skew with server diff --git a/sdk/storage/storage-blob/test/utils/testutils.common.ts b/sdk/storage/storage-blob/test/utils/testutils.common.ts index 00c3739088e8..434695078fba 100644 --- a/sdk/storage/storage-blob/test/utils/testutils.common.ts +++ b/sdk/storage/storage-blob/test/utils/testutils.common.ts @@ -134,3 +134,16 @@ export function isSuperSet(m1?: BlobMetadata, m2?: BlobMetadata): boolean { return true; } + +/** + * Sleep for seconds. + * + * @export + * @param {number} seconds + * @returns {Promise} + */ +export function sleep(seconds: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, seconds * 1000); + }); +} From d5b5d2a74ae8d2ad46e8ba44cc4fe998e02d03ce Mon Sep 17 00:00:00 2001 From: XiaoningLiu Date: Thu, 11 Jun 2020 19:16:13 +0800 Subject: [PATCH 2/3] [Storage] Unify blob tags type Tags everywhere in options and responses --- .../storage-blob/review/storage-blob.api.md | 84 +++++-- sdk/storage/storage-blob/src/Clients.ts | 231 +++++++++++++++--- .../storage-blob/src/generatedModels.ts | 10 +- sdk/storage/storage-blob/src/index.browser.ts | 2 +- sdk/storage/storage-blob/src/index.ts | 2 +- sdk/storage/storage-blob/src/models.ts | 10 + .../storage-blob/src/utils/utils.common.ts | 67 ++++- .../storage-blob/test/blobclient.spec.ts | 42 ++-- .../test/blobserviceclient.spec.ts | 38 ++- .../test/browser/highlevel.browser.spec.ts | 8 +- .../test/node/highlevel.node.spec.ts | 16 +- .../storage-blob/test/node/sas.spec.ts | 6 +- 12 files changed, 381 insertions(+), 135 deletions(-) diff --git a/sdk/storage/storage-blob/review/storage-blob.api.md b/sdk/storage/storage-blob/review/storage-blob.api.md index e0e464fed15d..a9e4e14a92d8 100644 --- a/sdk/storage/storage-blob/review/storage-blob.api.md +++ b/sdk/storage/storage-blob/review/storage-blob.api.md @@ -208,7 +208,7 @@ export interface AppendBlobCreateOptions extends CommonOptions { customerProvidedKey?: CpkInfo; encryptionScope?: string; metadata?: Metadata; - tags?: BlobTags; + tags?: Tags; } // @public @@ -379,7 +379,7 @@ export class BlobClient extends StorageClient { setAccessTier(tier: BlockBlobTier | PremiumPageBlobTier | string, options?: BlobSetTierOptions): Promise; setHTTPHeaders(blobHTTPHeaders?: BlobHTTPHeaders, options?: BlobSetHTTPHeadersOptions): Promise; setMetadata(metadata?: Metadata, options?: BlobSetMetadataOptions): Promise; - setTags(tags: BlobTags, options?: BlobSetTagsOptions): Promise; + setTags(tags: Tags, options?: BlobSetTagsOptions): Promise; syncCopyFromURL(copySource: string, options?: BlobSyncCopyFromURLOptions): Promise; undelete(options?: BlobUndeleteOptions): Promise; withSnapshot(snapshot: string): BlobClient; @@ -661,8 +661,10 @@ export interface BlobGetTagsOptions extends CommonOptions { } // @public -export type BlobGetTagsResponse = BlobTags & BlobGetTagsHeaders & { - _response: coreHttp.HttpResponse & { +export type BlobGetTagsResponse = { + tags: Tags; +} & BlobGetTagsHeaders & { + _response: HttpResponse & { parsedHeaders: BlobGetTagsHeaders; bodyAsText: string; parsedBody: BlobTags; @@ -689,8 +691,6 @@ export interface BlobHTTPHeaders { // @public export interface BlobItem { - // (undocumented) - blobTags?: BlobTags; // (undocumented) deleted: boolean; // (undocumented) @@ -710,6 +710,8 @@ export interface BlobItem { // (undocumented) snapshot: string; // (undocumented) + tags?: Tags; + // (undocumented) versionId?: string; } @@ -1051,7 +1053,7 @@ export interface BlobStartCopyFromURLOptions extends CommonOptions { metadata?: Metadata; rehydratePriority?: RehydratePriority; sourceConditions?: ModifiedAccessConditions; - tags?: BlobTags; + tags?: Tags; tier?: BlockBlobTier | PremiumPageBlobTier | string; } @@ -1069,7 +1071,7 @@ export interface BlobSyncCopyFromURLOptions extends CommonOptions { metadata?: Metadata; sourceConditions?: ModifiedAccessConditions; sourceContentMD5?: Uint8Array; - tags?: BlobTags; + tags?: Tags; } // @public @@ -1166,7 +1168,7 @@ export interface BlockBlobCommitBlockListOptions extends CommonOptions { customerProvidedKey?: CpkInfo; encryptionScope?: string; metadata?: Metadata; - tags?: BlobTags; + tags?: Tags; tier?: BlockBlobTier | string; } @@ -1219,7 +1221,7 @@ export interface BlockBlobParallelUploadOptions extends CommonOptions { [propertyName: string]: string; }; onProgress?: (progress: TransferProgressEvent) => void; - tags?: BlobTags; + tags?: Tags; } // @public @@ -1332,7 +1334,7 @@ export interface BlockBlobUploadOptions extends CommonOptions { encryptionScope?: string; metadata?: Metadata; onProgress?: (progress: TransferProgressEvent) => void; - tags?: BlobTags; + tags?: Tags; tier?: BlockBlobTier | string; } @@ -1353,7 +1355,7 @@ export interface BlockBlobUploadStreamOptions extends CommonOptions { [propertyName: string]: string; }; onProgress?: (progress: TransferProgressEvent) => void; - tags?: BlobTags; + tags?: Tags; } // @public @@ -1591,10 +1593,10 @@ export interface ContainerListBlobFlatSegmentHeaders { // @public export type ContainerListBlobFlatSegmentResponse = ListBlobsFlatSegmentResponse & ContainerListBlobFlatSegmentHeaders & { - _response: coreHttp.HttpResponse & { + _response: HttpResponse & { parsedHeaders: ContainerListBlobFlatSegmentHeaders; bodyAsText: string; - parsedBody: ListBlobsFlatSegmentResponse; + parsedBody: ListBlobsFlatSegmentResponseModel; }; }; @@ -1611,10 +1613,10 @@ export interface ContainerListBlobHierarchySegmentHeaders { // @public export type ContainerListBlobHierarchySegmentResponse = ListBlobsHierarchySegmentResponse & ContainerListBlobHierarchySegmentHeaders & { - _response: coreHttp.HttpResponse & { + _response: HttpResponse & { parsedHeaders: ContainerListBlobHierarchySegmentHeaders; bodyAsText: string; - parsedBody: ListBlobsHierarchySegmentResponse; + parsedBody: ListBlobsHierarchySegmentResponseModel; }; }; @@ -1881,6 +1883,26 @@ export interface ListBlobsFlatSegmentResponse { serviceEndpoint: string; } +// @public +export interface ListBlobsFlatSegmentResponseModel { + // (undocumented) + containerName: string; + // (undocumented) + continuationToken?: string; + // (undocumented) + marker?: string; + // (undocumented) + maxPageSize?: number; + // (undocumented) + prefix?: string; + // Warning: (ae-forgotten-export) The symbol "BlobFlatListSegment" needs to be exported by the entry point index.d.ts + // + // (undocumented) + segment: BlobFlatListSegment_2; + // (undocumented) + serviceEndpoint: string; +} + // @public export interface ListBlobsHierarchySegmentResponse { // (undocumented) @@ -1901,6 +1923,28 @@ export interface ListBlobsHierarchySegmentResponse { serviceEndpoint: string; } +// @public +export interface ListBlobsHierarchySegmentResponseModel { + // (undocumented) + containerName: string; + // (undocumented) + continuationToken?: string; + // (undocumented) + delimiter?: string; + // (undocumented) + marker?: string; + // (undocumented) + maxPageSize?: number; + // (undocumented) + prefix?: string; + // Warning: (ae-forgotten-export) The symbol "BlobHierarchyListSegment" needs to be exported by the entry point index.d.ts + // + // (undocumented) + segment: BlobHierarchyListSegment_2; + // (undocumented) + serviceEndpoint: string; +} + // @public export type ListBlobsIncludeItem = 'copy' | 'deleted' | 'metadata' | 'snapshots' | 'uncommittedblobs' | 'versions' | 'tags'; @@ -2057,7 +2101,7 @@ export interface PageBlobCreateOptions extends CommonOptions { customerProvidedKey?: CpkInfo; encryptionScope?: string; metadata?: Metadata; - tags?: BlobTags; + tags?: Tags; tier?: PremiumPageBlobTier | string; } @@ -2661,6 +2705,12 @@ export class StorageSharedKeyCredentialPolicy extends CredentialPolicy { // @public export type SyncCopyStatusType = 'success'; +// @public +export interface Tags { + // (undocumented) + [propertyName: string]: string; +} + // @public export interface UserDelegationKey { signedExpiresOn: Date; diff --git a/sdk/storage/storage-blob/src/Clients.ts b/sdk/storage/storage-blob/src/Clients.ts index 3fda999920f2..4fcc9565e5b6 100644 --- a/sdk/storage/storage-blob/src/Clients.ts +++ b/sdk/storage/storage-blob/src/Clients.ts @@ -45,14 +45,12 @@ import { BlobDownloadResponseModel, BlobGetPropertiesResponse, BlobHTTPHeaders, - BlobItem, BlobPrefix, BlobSetHTTPHeadersResponse, BlobSetMetadataResponse, BlobSetTagsResponse, BlobSetTierResponse, BlobStartCopyFromURLResponse, - BlobTags, BlobUndeleteResponse, BlockBlobCommitBlockListResponse, BlockBlobGetBlockListResponse, @@ -67,8 +65,6 @@ import { ContainerEncryptionScope, ContainerGetAccessPolicyHeaders, ContainerGetPropertiesResponse, - ContainerListBlobFlatSegmentResponse, - ContainerListBlobHierarchySegmentResponse, ContainerSetAccessPolicyResponse, ContainerSetMetadataResponse, CpkInfo, @@ -87,7 +83,13 @@ import { RehydratePriority, SequenceNumberActionType, SignedIdentifierModel, - BlobGetTagsResponse + BlobGetTagsHeaders, + BlobTags, + ListBlobsFlatSegmentResponseModel, + ContainerListBlobFlatSegmentHeaders, + BlobProperties, + ContainerListBlobHierarchySegmentHeaders, + ListBlobsHierarchySegmentResponseModel } from "./generatedModels"; import { AppendBlobRequestConditions, @@ -95,6 +97,7 @@ import { BlockBlobTier, ensureCpkIfSpecified, Metadata, + Tags, PageBlobRequestConditions, PremiumPageBlobTier, toAccessTier @@ -132,7 +135,9 @@ import { setURLParameter, toBlobTagsString, toQuerySerialization, - truncatedISO8061Date + truncatedISO8061Date, + toBlobTags, + toTags } from "./utils/utils.common"; import { fsStat, readStreamToLocalFile, streamToBuffer } from "./utils/utils.node"; import { PageSettings, PagedAsyncIterableIterator } from "@azure/core-paging"; @@ -494,6 +499,31 @@ export interface BlobGetTagsOptions extends CommonOptions { abortSignal?: AbortSignalLike; } +/** + * Contains response data for the {@link ContainerClient.getTags} operation. + */ +export type BlobGetTagsResponse = { tags: Tags } & BlobGetTagsHeaders & { + /** + * The underlying HTTP response. + */ + _response: HttpResponse & { + /** + * The parsed HTTP response headers. + */ + parsedHeaders: BlobGetTagsHeaders; + + /** + * The response body as text (string format) + */ + bodyAsText: string; + + /** + * The response body as parsed JSON or XML + */ + parsedBody: BlobTags; + }; + }; + /** * Options to configure Blob - Acquire Lease operation. * @@ -717,10 +747,10 @@ export interface BlobStartCopyFromURLOptions extends CommonOptions { /** * Blob tags. * - * @type {BlobTags} + * @type {Tags} * @memberof BlobStartCopyFromURLOptions */ - tags?: BlobTags; + tags?: Tags; } /** @@ -794,10 +824,10 @@ export interface BlobSyncCopyFromURLOptions extends CommonOptions { /** * Blob tags. * - * @type {BlobTags} + * @type {Tags} * @memberof BlobSyncCopyFromURLOptions */ - tags?: BlobTags; + tags?: Tags; } /** @@ -1550,21 +1580,18 @@ export class BlobClient extends StorageClient { * Valid tag key and value characters include lower and upper case letters, digits (0-9), * space (' '), plus ('+'), minus ('-'), period ('.'), foward slash ('/'), colon (':'), equals ('='), and underscore ('_'). * - * @param {BlobTags} tags + * @param {Tags} tags * @param {BlobSetTagsOptions} [options={}] * @returns {Promise} * @memberof BlobClient */ - public async setTags( - tags: BlobTags, - options: BlobSetTagsOptions = {} - ): Promise { + public async setTags(tags: Tags, options: BlobSetTagsOptions = {}): Promise { const { span, spanOptions } = createSpan("BlobClient-setTags", options.tracingOptions); try { return await this.blobContext.setTags({ abortSignal: options.abortSignal, spanOptions, - tags + tags: toBlobTags(tags) }); } catch (e) { span.setStatus({ @@ -1587,10 +1614,15 @@ export class BlobClient extends StorageClient { public async getTags(options: BlobGetTagsOptions = {}): Promise { const { span, spanOptions } = createSpan("BlobClient-getTags", options.tracingOptions); try { - return await this.blobContext.getTags({ + const response = await this.blobContext.getTags({ abortSignal: options.abortSignal, spanOptions }); + const wrappedResponse: BlobGetTagsResponse = { + ...response, + tags: toTags({ blobTagSet: response.blobTagSet }) || {} + }; + return wrappedResponse; } catch (e) { span.setStatus({ code: CanonicalCode.UNKNOWN, @@ -2237,10 +2269,10 @@ export interface AppendBlobCreateOptions extends CommonOptions { /** * Blob tags. * - * @type {BlobTags} + * @type {Tags} * @memberof AppendBlobCreateOptions */ - tags?: BlobTags; + tags?: Tags; } /** @@ -2801,10 +2833,10 @@ export interface BlockBlobUploadOptions extends CommonOptions { /** * Blob tags. * - * @type {BlobTags} + * @type {Tags} * @memberof BlockBlobUploadOptions */ - tags?: BlobTags; + tags?: Tags; } /** @@ -3184,10 +3216,10 @@ export interface BlockBlobCommitBlockListOptions extends CommonOptions { /** * Blob tags. * - * @type {BlobTags} + * @type {Tags} * @memberof BlockBlobCommitBlockListOptions */ - tags?: BlobTags; + tags?: Tags; } /** @@ -3277,10 +3309,10 @@ export interface BlockBlobUploadStreamOptions extends CommonOptions { /** * Blob tags. * - * @type {BlobTags} + * @type {Tags} * @memberof BlockBlobUploadStreamOptions */ - tags?: BlobTags; + tags?: Tags; } /** * Option interface for {@link BlockBlobClient.uploadFile} and {@link BlockBlobClient.uploadSeekableStream}. @@ -3371,10 +3403,10 @@ export interface BlockBlobParallelUploadOptions extends CommonOptions { /** * Blob tags. * - * @type {BlobTags} + * @type {Tags} * @memberof BlockBlobParallelUploadOptions */ - tags?: BlobTags; + tags?: Tags; } /** @@ -4417,10 +4449,10 @@ export interface PageBlobCreateOptions extends CommonOptions { /** * Blob tags. * - * @type {BlobTags} + * @type {Tags} * @memberof PageBlobCreateOptions */ - tags?: BlobTags; + tags?: Tags; } /** @@ -6090,6 +6122,115 @@ interface ContainerListBlobsSegmentOptions extends CommonOptions { include?: ListBlobsIncludeItem[]; } +/** + * An interface representing BlobHierarchyListSegment. + */ +export interface BlobHierarchyListSegment { + blobPrefixes?: BlobPrefix[]; + blobItems: BlobItem[]; +} + +/** + * An enumeration of blobs + */ +export interface ListBlobsHierarchySegmentResponse { + serviceEndpoint: string; + containerName: string; + prefix?: string; + marker?: string; + maxPageSize?: number; + delimiter?: string; + segment: BlobHierarchyListSegment; + continuationToken?: string; +} + +/** + * Contains response data for the listBlobHierarchySegment operation. + */ +export type ContainerListBlobHierarchySegmentResponse = ListBlobsHierarchySegmentResponse & + ContainerListBlobHierarchySegmentHeaders & { + /** + * The underlying HTTP response. + */ + _response: HttpResponse & { + /** + * The parsed HTTP response headers. + */ + parsedHeaders: ContainerListBlobHierarchySegmentHeaders; + + /** + * The response body as text (string format) + */ + bodyAsText: string; + + /** + * The response body as parsed JSON or XML + */ + parsedBody: ListBlobsHierarchySegmentResponseModel; + }; + }; + +/** + * An Azure Storage blob + */ +export interface BlobItem { + name: string; + deleted: boolean; + snapshot: string; + versionId?: string; + isCurrentVersion?: boolean; + properties: BlobProperties; + metadata?: { [propertyName: string]: string }; + tags?: Tags; + objectReplicationMetadata?: { [propertyName: string]: string }; +} + +/** + * An interface representing BlobFlatListSegment. + */ +export interface BlobFlatListSegment { + blobItems: BlobItem[]; +} + +/** + * An enumeration of blobs + */ +export interface ListBlobsFlatSegmentResponse { + serviceEndpoint: string; + containerName: string; + prefix?: string; + marker?: string; + maxPageSize?: number; + segment: BlobFlatListSegment; + continuationToken?: string; +} + +/** + * Contains response data for the listBlobFlatSegment operation. + */ +export type ContainerListBlobFlatSegmentResponse = ListBlobsFlatSegmentResponse & + ContainerListBlobFlatSegmentHeaders & { + /** + * The underlying HTTP response. + */ + _response: HttpResponse & { + /** + * The parsed HTTP response headers. + */ + parsedHeaders: ContainerListBlobFlatSegmentHeaders; + + /** + * The response body as text (string format) + */ + bodyAsText: string; + + /** + * The response body as parsed JSON or XML + */ + parsedBody: ListBlobsFlatSegmentResponseModel; + }; + }; + /** * Options to configure Container - List Blobs operations. * @@ -6844,11 +6985,25 @@ export class ContainerClient extends StorageClient { options.tracingOptions ); try { - return await this.containerContext.listBlobFlatSegment({ + const resposne = await this.containerContext.listBlobFlatSegment({ marker, ...options, spanOptions }); + const wrappedResponse: ContainerListBlobFlatSegmentResponse = { + ...resposne, + segment: { + ...resposne.segment, + blobItems: resposne.segment.blobItems.map((blobItemInteral) => { + const blobItem: BlobItem = { + ...blobItemInteral, + tags: toTags(blobItemInteral.blobTags) + }; + return blobItem; + }) + } + }; + return wrappedResponse; } catch (e) { span.setStatus({ code: CanonicalCode.UNKNOWN, @@ -6883,11 +7038,25 @@ export class ContainerClient extends StorageClient { options.tracingOptions ); try { - return await this.containerContext.listBlobHierarchySegment(delimiter, { + const resposne = await this.containerContext.listBlobHierarchySegment(delimiter, { marker, ...options, spanOptions }); + const wrappedResponse: ContainerListBlobHierarchySegmentResponse = { + ...resposne, + segment: { + ...resposne.segment, + blobItems: resposne.segment.blobItems.map((blobItemInteral) => { + const blobItem: BlobItem = { + ...blobItemInteral, + tags: toTags(blobItemInteral.blobTags) + }; + return blobItem; + }) + } + }; + return wrappedResponse; } catch (e) { span.setStatus({ code: CanonicalCode.UNKNOWN, diff --git a/sdk/storage/storage-blob/src/generatedModels.ts b/sdk/storage/storage-blob/src/generatedModels.ts index 014208f40a72..309d24270976 100644 --- a/sdk/storage/storage-blob/src/generatedModels.ts +++ b/sdk/storage/storage-blob/src/generatedModels.ts @@ -12,7 +12,7 @@ export { AppendBlobAppendBlockHeaders, AppendBlobCreateHeaders, ArchiveStatus, - ListBlobsFlatSegmentResponse, + ListBlobsFlatSegmentResponse as ListBlobsFlatSegmentResponseModel, BlobAbortCopyFromURLHeaders, BlobCopyFromURLHeaders, BlobCreateSnapshotHeaders, @@ -28,7 +28,6 @@ export { BlobSetMetadataResponse, BlobSetTagsResponse, BlobCreateSnapshotResponse, - BlobFlatListSegment, BlobStartCopyFromURLHeaders, BlobStartCopyFromURLResponse, BlobAbortCopyFromURLResponse, @@ -38,8 +37,6 @@ export { BlobSetTierHeaders, BlobSetTierResponse, BlobSetTagsHeaders, - BlobHierarchyListSegment, - BlobItemInternal as BlobItem, BlobPrefix, BlobDownloadHeaders, BlobQueryResponse as BlobQueryResponseModel, @@ -63,7 +60,6 @@ export { BlobServiceProperties, BlobServiceStatistics, BlobGetTagsHeaders, - BlobGetTagsResponse, BlobTag, ContainerCreateHeaders, ContainerCreateResponse, @@ -73,9 +69,7 @@ export { ContainerGetPropertiesHeaders, ContainerBreakLeaseOptionalParams, ContainerListBlobFlatSegmentHeaders, - ContainerListBlobFlatSegmentResponse, ContainerListBlobHierarchySegmentHeaders, - ContainerListBlobHierarchySegmentResponse, ContainerGetPropertiesResponse, ContainerProperties, ContainerSetMetadataResponse, @@ -93,7 +87,7 @@ export { LeaseDurationType, LeaseStateType, LeaseStatusType, - ListBlobsHierarchySegmentResponse, + ListBlobsHierarchySegmentResponse as ListBlobsHierarchySegmentResponseModel, ListBlobsIncludeItem, ListContainersIncludeType, ListContainersSegmentResponse, diff --git a/sdk/storage/storage-blob/src/index.browser.ts b/sdk/storage/storage-blob/src/index.browser.ts index 57214f7b5115..ecb4ede68f38 100644 --- a/sdk/storage/storage-blob/src/index.browser.ts +++ b/sdk/storage/storage-blob/src/index.browser.ts @@ -13,7 +13,7 @@ export * from "./credentials/AnonymousCredential"; export * from "./credentials/Credential"; export { SasIPRange } from "./SasIPRange"; export { Range } from "./Range"; -export { BlockBlobTier, PremiumPageBlobTier } from "./models"; +export { BlockBlobTier, PremiumPageBlobTier, Tags } from "./models"; export * from "./Pipeline"; export * from "./policies/AnonymousCredentialPolicy"; export * from "./policies/CredentialPolicy"; diff --git a/sdk/storage/storage-blob/src/index.ts b/sdk/storage/storage-blob/src/index.ts index 6cb9eee5aa44..36220069c67d 100644 --- a/sdk/storage/storage-blob/src/index.ts +++ b/sdk/storage/storage-blob/src/index.ts @@ -22,7 +22,7 @@ export * from "./credentials/Credential"; export * from "./credentials/StorageSharedKeyCredential"; export { SasIPRange } from "./SasIPRange"; export { Range } from "./Range"; -export { BlockBlobTier, PremiumPageBlobTier } from "./models"; +export { BlockBlobTier, PremiumPageBlobTier, Tags } from "./models"; export * from "./Pipeline"; export * from "./policies/AnonymousCredentialPolicy"; export * from "./policies/CredentialPolicy"; diff --git a/sdk/storage/storage-blob/src/models.ts b/sdk/storage/storage-blob/src/models.ts index c253686c2dd7..c6199b36d0cf 100644 --- a/sdk/storage/storage-blob/src/models.ts +++ b/sdk/storage/storage-blob/src/models.ts @@ -11,6 +11,16 @@ import { } from "./generatedModels"; import { EncryptionAlgorithmAES25 } from "./utils/constants"; +/** + * Blob tags. + * + * @export + * @interface Tags + */ +export interface Tags { + [propertyName: string]: string; +} + /** * A map of name-value pairs to associate with the resource. */ diff --git a/sdk/storage/storage-blob/src/utils/utils.common.ts b/sdk/storage/storage-blob/src/utils/utils.common.ts index ee3b27de268d..3889ffa0d62d 100644 --- a/sdk/storage/storage-blob/src/utils/utils.common.ts +++ b/sdk/storage/storage-blob/src/utils/utils.common.ts @@ -7,6 +7,7 @@ import { HttpHeaders, isNode, URLBuilder } from "@azure/core-http"; import { BlobQueryCsvTextConfiguration, BlobQueryJsonTextConfiguration } from "../Clients"; import { QuerySerialization, BlobTags } from "../generated/src/models"; import { DevelopmentConnectionString, HeaderConstants, URLConstants } from "./constants"; +import { Tags } from "../models"; /** * Reserved URL characters must be properly escaped for Storage services like Blob or File. @@ -557,23 +558,73 @@ export function getAccountNameFromUrl(url: string): string { } /** - * Convert BlobTags to encoded string. + * Convert Tags to encoded string. * * @export - * @param {BlobTags} tags + * @param {Tags} tags * @returns {string | undefined} */ -export function toBlobTagsString(tags?: BlobTags): string | undefined { - if (tags === undefined || tags.blobTagSet.length === 0) { +export function toBlobTagsString(tags?: Tags): string | undefined { + if (tags === undefined) { return undefined; } - const tagParis = []; - for (const blobTag of tags.blobTagSet) { - tagParis.push(`${encodeURIComponent(blobTag.key)}=${encodeURIComponent(blobTag.value)}`); + const tagPairs = []; + for (const key in tags) { + if (tags.hasOwnProperty(key)) { + const value = tags[key]; + tagPairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`); + } + } + + return tagPairs.join("&"); +} + +/** + * Convert Tags type to BlobTags. + * + * @export + * @param {Tags} [tags] + * @returns {(BlobTags | undefined)} + */ +export function toBlobTags(tags?: Tags): BlobTags | undefined { + if (tags === undefined) { + return undefined; + } + + const res: BlobTags = { + blobTagSet: [] + }; + + for (const key in tags) { + if (tags.hasOwnProperty(key)) { + const value = tags[key]; + res.blobTagSet.push({ + key, + value + }); + } + } + return res; +} + +/** + * Covert BlobTags to Tags type. + * + * @export + * @param {BlobTags} [tags] + * @returns {(Tags | undefined)} + */ +export function toTags(tags?: BlobTags): Tags | undefined { + if (tags === undefined) { + return undefined; } - return tagParis.join("&"); + const res: Tags = {}; + for (const blobTag of tags.blobTagSet) { + res[blobTag.key] = blobTag.value; + } + return res; } /** diff --git a/sdk/storage/storage-blob/test/blobclient.spec.ts b/sdk/storage/storage-blob/test/blobclient.spec.ts index 96853ea734b7..0c4f34cf1f14 100644 --- a/sdk/storage/storage-blob/test/blobclient.spec.ts +++ b/sdk/storage/storage-blob/test/blobclient.spec.ts @@ -54,15 +54,13 @@ describe("BlobClient", () => { it.only("Set blob tags should work", async () => { const tags = { - blobTagSet: [ - { key: "tag1", value: "val1" }, - { key: "tag2", value: "val2" } - ] + tag1: "val1", + tag2: "val2" }; await blockBlobClient.setTags(tags); const response = await blockBlobClient.getTags(); - assert.deepStrictEqual(response.blobTagSet, tags.blobTagSet); + assert.deepStrictEqual(response.tags, tags); const properties = await blockBlobClient.getProperties(); assert.deepStrictEqual(properties.tagCount, 2); @@ -76,15 +74,13 @@ describe("BlobClient", () => { const segment = await iter.next(); // TODO: Make blob tag type consistency cross all request or response - assert.deepStrictEqual(segment.value.segment.blobItems[0].blobTags.blobTagSet, tags.blobTagSet); + assert.deepStrictEqual(segment.value.segment.blobItems[0].tags, tags); }); it.only("Get blob tags should work with a snapshot", async () => { const tags = { - blobTagSet: [ - { key: "tag1", value: "val1" }, - { key: "tag2", value: "val2" } - ] + tag1: "val1", + tag2: "val2" }; await blockBlobClient.setTags(tags); @@ -92,56 +88,50 @@ describe("BlobClient", () => { const blockBlobClientSnapshot = blockBlobClient.withSnapshot(snapshotResponse.snapshot!); const response = await blockBlobClientSnapshot.getTags(); - assert.deepStrictEqual(response.blobTagSet, tags.blobTagSet); + assert.deepStrictEqual(response.tags, tags); }); it.only("Create block blob blob should work with tags", async () => { await blockBlobClient.delete(); const tags = { - blobTagSet: [ - { key: "tag1", value: "val1" }, - { key: "tag2", value: "val2" } - ] + tag1: "val1", + tag2: "val2" }; await blockBlobClient.upload("hello", 5, { tags }); const response = await blockBlobClient.getTags(); - assert.deepStrictEqual(response.blobTagSet, tags.blobTagSet); + assert.deepStrictEqual(response.tags, tags); }); it.only("Create append blob should work with tags", async () => { await blockBlobClient.delete(); const tags = { - blobTagSet: [ - { key: "tag1", value: "val1" }, - { key: "tag2", value: "val2" } - ] + tag1: "val1", + tag2: "val2" }; const appendBlobClient = blobClient.getAppendBlobClient(); await appendBlobClient.create({ tags }); const response = await appendBlobClient.getTags(); - assert.deepStrictEqual(response.blobTagSet, tags.blobTagSet); + assert.deepStrictEqual(response.tags, tags); }); it.only("Create page blob should work with tags", async () => { await blockBlobClient.delete(); const tags = { - blobTagSet: [ - { key: "tag1", value: "val1" }, - { key: "tag2", value: "val2" } - ] + tag1: "val1", + tag2: "val2" }; const pageBlobClient = blobClient.getPageBlobClient(); await pageBlobClient.create(512, { tags }); const response = await pageBlobClient.getTags(); - assert.deepStrictEqual(response.blobTagSet, tags.blobTagSet); + assert.deepStrictEqual(response.tags, tags); }); it("download with with default parameters", async () => { diff --git a/sdk/storage/storage-blob/test/blobserviceclient.spec.ts b/sdk/storage/storage-blob/test/blobserviceclient.spec.ts index f98ef015650e..1e5f00b2150d 100644 --- a/sdk/storage/storage-blob/test/blobserviceclient.spec.ts +++ b/sdk/storage/storage-blob/test/blobserviceclient.spec.ts @@ -11,6 +11,7 @@ import { sleep } from "./utils"; import { record, delay, Recorder } from "@azure/test-utils-recorder"; +import { Tags } from "../src/models"; dotenv.config(); describe("BlobServiceClient", () => { @@ -480,48 +481,37 @@ describe("BlobServiceClient", () => { const blobName1 = recorder.getUniqueName("blobname1"); const appendBlobClient1 = containerClient.getAppendBlobClient(blobName1); - const tags1 = { - blobTagSet: [ - { key: key1, value: recorder.getUniqueName("val1") }, - { key: key2, value: "default" } - ] - }; + const tags1: Tags = {}; + tags1[key1] = recorder.getUniqueName("val1"); + tags1[key2] = "default"; await appendBlobClient1.create({ tags: tags1 }); const blobName2 = recorder.getUniqueName("blobname2"); const appendBlobClient2 = containerClient.getAppendBlobClient(blobName2); - const tags2 = { - blobTagSet: [ - { key: key1, value: recorder.getUniqueName("val2") }, - { key: key2, value: "default" } - ] - }; + const tags2: Tags = {}; + tags2[key1] = recorder.getUniqueName("val2"); + tags2[key2] = "default"; await appendBlobClient2.create({ tags: tags2 }); const blobName3 = recorder.getUniqueName("blobname3"); const appendBlobClient3 = containerClient.getAppendBlobClient(blobName3); - const tags3 = { - blobTagSet: [ - { key: key1, value: recorder.getUniqueName("val3") }, - { key: key2, value: "default" } - ] - }; + const tags3: Tags = {}; + tags3[key1] = recorder.getUniqueName("val3"); + tags3[key2] = "default"; await appendBlobClient3.create({ tags: tags3 }); // Wait for indexing tags await sleep(2); - for await (const blob of blobServiceClient.findBlobsByTags( - `${tags1.blobTagSet[0].key}='${tags1.blobTagSet[0].value}'` - )) { + for await (const blob of blobServiceClient.findBlobsByTags(`${key1}='${tags1[key1]}'`)) { assert.deepStrictEqual(blob.containerName, containerName); assert.deepStrictEqual(blob.name, blobName1); - assert.deepStrictEqual(blob.tagValue, tags1.blobTagSet[0].value); + assert.deepStrictEqual(blob.tagValue, tags1[key1]); } const blobs = []; for await (const segment of blobServiceClient - .findBlobsByTags(`${tags2.blobTagSet[0].key}='${tags2.blobTagSet[0].value}'`) + .findBlobsByTags(`${key1}='${tags2[key1]}'`) .byPage()) { for (const blob of segment.blobs) { blobs.push(blob); @@ -530,7 +520,7 @@ describe("BlobServiceClient", () => { assert.deepStrictEqual(blobs.length, 1); assert.deepStrictEqual(blobs[0].containerName, containerName); assert.deepStrictEqual(blobs[0].name, blobName2); - assert.deepStrictEqual(blobs[0].tagValue, tags2.blobTagSet[0].value); + assert.deepStrictEqual(blobs[0].tagValue, tags2[key1]); const blobsWithTag2 = []; for await (const segment of blobServiceClient.findBlobsByTags(`${key2}='default'`).byPage({ diff --git a/sdk/storage/storage-blob/test/browser/highlevel.browser.spec.ts b/sdk/storage/storage-blob/test/browser/highlevel.browser.spec.ts index 8288d3f0ae63..0c255eedc278 100644 --- a/sdk/storage/storage-blob/test/browser/highlevel.browser.spec.ts +++ b/sdk/storage/storage-blob/test/browser/highlevel.browser.spec.ts @@ -158,10 +158,8 @@ describe("Highlevel", () => { recorder.skip("browser", "Temp file - recorder doesn't support saving the file"); const tags = { - blobTagSet: [ - { key: "tag1", value: "val1" }, - { key: "tag2", value: "val2" } - ] + tag1: "val1", + tag2: "val2" }; await blockBlobClient.uploadBrowserData(tempFile2, { @@ -171,7 +169,7 @@ describe("Highlevel", () => { }); const response = await blockBlobClient.getTags(); - assert.deepStrictEqual(response.blobTagSet, tags.blobTagSet); + assert.deepStrictEqual(response.tags, tags); }); it("uploadBrowserDataToBlockBlob should success when blob >= BLOCK_BLOB_MAX_UPLOAD_BLOB_BYTES", async function() { diff --git a/sdk/storage/storage-blob/test/node/highlevel.node.spec.ts b/sdk/storage/storage-blob/test/node/highlevel.node.spec.ts index 9285014f61a1..1d67094f2666 100644 --- a/sdk/storage/storage-blob/test/node/highlevel.node.spec.ts +++ b/sdk/storage/storage-blob/test/node/highlevel.node.spec.ts @@ -103,10 +103,8 @@ describe("Highlevel", () => { recorder.skip("node", "Temp file - recorder doesn't support saving the file"); const tags = { - blobTagSet: [ - { key: "tag1", value: "val1" }, - { key: "tag2", value: "val2" } - ] + tag1: "val1", + tag2: "val2" }; await blockBlobClient.uploadFile(tempFileSmall, { @@ -116,7 +114,7 @@ describe("Highlevel", () => { }); const response = await blockBlobClient.getTags(); - assert.deepStrictEqual(response.blobTagSet, tags.blobTagSet); + assert.deepStrictEqual(response.tags, tags); }); it("uploadFile should success when blob < BLOCK_BLOB_MAX_UPLOAD_BLOB_BYTES", async () => { @@ -290,16 +288,14 @@ describe("Highlevel", () => { bufferStream.end(buf); const tags = { - blobTagSet: [ - { key: "tag1", value: "val1" }, - { key: "tag2", value: "val2" } - ] + tag1: "val1", + tag2: "val2" }; await blockBlobClient.uploadStream(bufferStream, 4 * 1024 * 1024, 20, { tags }); const response = await blockBlobClient.getTags(); - assert.deepStrictEqual(response.blobTagSet, tags.blobTagSet); + assert.deepStrictEqual(response.tags, tags); }); it("uploadStream should abort", async () => { diff --git a/sdk/storage/storage-blob/test/node/sas.spec.ts b/sdk/storage/storage-blob/test/node/sas.spec.ts index 59c4f5e8f165..b0e6eb4022dd 100644 --- a/sdk/storage/storage-blob/test/node/sas.spec.ts +++ b/sdk/storage/storage-blob/test/node/sas.spec.ts @@ -369,10 +369,8 @@ describe("Shared Access Signature (SAS) generation Node.js only", () => { const blobClientWithSAS = new PageBlobClient(sasURL, newPipeline(new AnonymousCredential())); const tags = { - blobTagSet: [ - { key: "tag1", value: "val1" }, - { key: "tag2", value: "val2" } - ] + tag1: "val1", + tag2: "val2" }; await blobClientWithSAS.setTags(tags); From fc86b0f64a98fc3c6aec57a270179a3b09f6dba8 Mon Sep 17 00:00:00 2001 From: XiaoningLiu Date: Fri, 12 Jun 2020 16:17:53 +0800 Subject: [PATCH 3/3] [Storage] Blob tags - resolve comments --- .../storage-blob/review/storage-blob.api.md | 5 +-- .../src/BlobSASSignatureValues.ts | 16 +++++--- .../storage-blob/src/BlobServiceClient.ts | 2 + sdk/storage/storage-blob/src/models.ts | 7 +--- .../storage-blob/test/blobclient.spec.ts | 21 ++++++----- .../test/blobserviceclient.spec.ts | 2 +- .../test/browser/highlevel.browser.spec.ts | 2 +- .../test/node/highlevel.node.spec.ts | 37 ++++++++++++------- .../storage-blob/test/node/sas.spec.ts | 26 ++++++++----- 9 files changed, 67 insertions(+), 51 deletions(-) diff --git a/sdk/storage/storage-blob/review/storage-blob.api.md b/sdk/storage/storage-blob/review/storage-blob.api.md index a9e4e14a92d8..fe0da7c71172 100644 --- a/sdk/storage/storage-blob/review/storage-blob.api.md +++ b/sdk/storage/storage-blob/review/storage-blob.api.md @@ -2706,10 +2706,7 @@ export class StorageSharedKeyCredentialPolicy extends CredentialPolicy { export type SyncCopyStatusType = 'success'; // @public -export interface Tags { - // (undocumented) - [propertyName: string]: string; -} +export type Tags = Record; // @public export interface UserDelegationKey { diff --git a/sdk/storage/storage-blob/src/BlobSASSignatureValues.ts b/sdk/storage/storage-blob/src/BlobSASSignatureValues.ts index 25b7cfd0cafb..b3eff14da424 100644 --- a/sdk/storage/storage-blob/src/BlobSASSignatureValues.ts +++ b/sdk/storage/storage-blob/src/BlobSASSignatureValues.ts @@ -475,18 +475,22 @@ function generateBlobSASQueryParameters20181109( ); } - if (blobSASSignatureValues.versionId) { + const version = blobSASSignatureValues.version ? blobSASSignatureValues.version : SERVICE_VERSION; + let resource: string = "c"; + let verifiedPermissions: string | undefined; + + if (blobSASSignatureValues.versionId && version < "2019-10-10") { throw RangeError("'version' must be >= '2019-10-10' when provided 'versionId'."); } - if (blobSASSignatureValues.permissions && blobSASSignatureValues.permissions.tag) { + if ( + blobSASSignatureValues.permissions && + blobSASSignatureValues.permissions.tag && + version < "2019-12-12" + ) { throw RangeError("'version' must be >= '2019-12-12' when provided 't' permission."); } - const version = blobSASSignatureValues.version ? blobSASSignatureValues.version : SERVICE_VERSION; - let resource: string = "c"; - let verifiedPermissions: string | undefined; - if (blobSASSignatureValues.blobName === undefined && blobSASSignatureValues.snapshotTime) { throw RangeError("Must provide 'blobName' when provided 'snapshotTime'."); } diff --git a/sdk/storage/storage-blob/src/BlobServiceClient.ts b/sdk/storage/storage-blob/src/BlobServiceClient.ts index e969d543f92e..fff24db33399 100644 --- a/sdk/storage/storage-blob/src/BlobServiceClient.ts +++ b/sdk/storage/storage-blob/src/BlobServiceClient.ts @@ -834,6 +834,8 @@ export class BlobServiceClient extends StorageClient { * * .byPage() returns an async iterable iterator to list the blobs in pages. * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/get-blob-service-properties + * * Example using `for await` syntax: * * ```js diff --git a/sdk/storage/storage-blob/src/models.ts b/sdk/storage/storage-blob/src/models.ts index c6199b36d0cf..dec8da7e987b 100644 --- a/sdk/storage/storage-blob/src/models.ts +++ b/sdk/storage/storage-blob/src/models.ts @@ -13,13 +13,8 @@ import { EncryptionAlgorithmAES25 } from "./utils/constants"; /** * Blob tags. - * - * @export - * @interface Tags */ -export interface Tags { - [propertyName: string]: string; -} +export type Tags = Record; /** * A map of name-value pairs to associate with the resource. diff --git a/sdk/storage/storage-blob/test/blobclient.spec.ts b/sdk/storage/storage-blob/test/blobclient.spec.ts index 0c4f34cf1f14..f0ebdf745a7a 100644 --- a/sdk/storage/storage-blob/test/blobclient.spec.ts +++ b/sdk/storage/storage-blob/test/blobclient.spec.ts @@ -9,7 +9,7 @@ import { getBSU, getSASConnectionStringFromEnvironment, recorderEnvSetup, - isBlobVersioningDisabled, + isBlobVersioningDisabled } from "./utils"; import { record, delay } from "@azure/test-utils-recorder"; import { @@ -52,7 +52,7 @@ describe("BlobClient", () => { } }); - it.only("Set blob tags should work", async () => { + it("Set blob tags should work", async () => { const tags = { tag1: "val1", tag2: "val2" @@ -77,7 +77,7 @@ describe("BlobClient", () => { assert.deepStrictEqual(segment.value.segment.blobItems[0].tags, tags); }); - it.only("Get blob tags should work with a snapshot", async () => { + it("Get blob tags should work with a snapshot", async () => { const tags = { tag1: "val1", tag2: "val2" @@ -91,7 +91,7 @@ describe("BlobClient", () => { assert.deepStrictEqual(response.tags, tags); }); - it.only("Create block blob blob should work with tags", async () => { + it("Create block blob blob should work with tags", async () => { await blockBlobClient.delete(); const tags = { @@ -104,7 +104,7 @@ describe("BlobClient", () => { assert.deepStrictEqual(response.tags, tags); }); - it.only("Create append blob should work with tags", async () => { + it("Create append blob should work with tags", async () => { await blockBlobClient.delete(); const tags = { @@ -119,7 +119,7 @@ describe("BlobClient", () => { assert.deepStrictEqual(response.tags, tags); }); - it.only("Create page blob should work with tags", async () => { + it("Create page blob should work with tags", async () => { await blockBlobClient.delete(); const tags = { @@ -336,7 +336,7 @@ describe("BlobClient", () => { const iter = containerClient .listBlobsFlat({ includeDeleted: true, - includeVersions: true, // Need this when blob versioning is turned on. + includeVersions: true // Need this when blob versioning is turned on. }) .byPage({ maxPageSize: 1 }); @@ -375,7 +375,10 @@ describe("BlobClient", () => { ); if (isBlobVersioningDisabled()) { - assert.ok(result.segment.blobItems![0].deleted, "Expect that the blob is marked for deletion"); + assert.ok( + result.segment.blobItems![0].deleted, + "Expect that the blob is marked for deletion" + ); } await blobClient.undelete(); @@ -383,7 +386,7 @@ describe("BlobClient", () => { const iter2 = containerClient .listBlobsFlat({ includeDeleted: true, - includeVersions: true, // Need this when blob versioning is turned on. + includeVersions: true // Need this when blob versioning is turned on. }) .byPage(); diff --git a/sdk/storage/storage-blob/test/blobserviceclient.spec.ts b/sdk/storage/storage-blob/test/blobserviceclient.spec.ts index 1e5f00b2150d..d0a541f2b1a3 100644 --- a/sdk/storage/storage-blob/test/blobserviceclient.spec.ts +++ b/sdk/storage/storage-blob/test/blobserviceclient.spec.ts @@ -469,7 +469,7 @@ describe("BlobServiceClient", () => { assert.notDeepStrictEqual(response.signedExpiresOn, undefined); }); - it.only("Find blob by tags should work", async () => { + it("Find blob by tags should work", async () => { const blobServiceClient = getBSU(); const containerName = recorder.getUniqueName("container1"); diff --git a/sdk/storage/storage-blob/test/browser/highlevel.browser.spec.ts b/sdk/storage/storage-blob/test/browser/highlevel.browser.spec.ts index 0c255eedc278..9c14a4736fa9 100644 --- a/sdk/storage/storage-blob/test/browser/highlevel.browser.spec.ts +++ b/sdk/storage/storage-blob/test/browser/highlevel.browser.spec.ts @@ -154,7 +154,7 @@ describe("Highlevel", () => { assert.equal(uploadedString, downloadedString); }); - it.only("uploadBrowserDataToBlockBlob should work with tags", async () => { + it("uploadBrowserDataToBlockBlob should work with tags", async () => { recorder.skip("browser", "Temp file - recorder doesn't support saving the file"); const tags = { diff --git a/sdk/storage/storage-blob/test/node/highlevel.node.spec.ts b/sdk/storage/storage-blob/test/node/highlevel.node.spec.ts index 1d67094f2666..4dcec8d15235 100644 --- a/sdk/storage/storage-blob/test/node/highlevel.node.spec.ts +++ b/sdk/storage/storage-blob/test/node/highlevel.node.spec.ts @@ -4,7 +4,12 @@ import * as path from "path"; import { PassThrough } from "stream"; import { AbortController } from "@azure/abort-controller"; -import { createRandomLocalFile, getBSU, recorderEnvSetup, isBlobVersioningDisabled } from "../utils"; +import { + createRandomLocalFile, + getBSU, + recorderEnvSetup, + isBlobVersioningDisabled +} from "../utils"; import { RetriableReadableStreamOptions } from "../../src/utils/RetriableReadableStream"; import { record, Recorder } from "@azure/test-utils-recorder"; import { ContainerClient, BlobClient, BlockBlobClient, BlobServiceClient } from "../../src"; @@ -28,7 +33,7 @@ describe("Highlevel", () => { let recorder: Recorder; let blobServiceClient: BlobServiceClient; - beforeEach(async function () { + beforeEach(async function() { recorder = record(this, recorderEnvSetup); blobServiceClient = getBSU(); containerName = recorder.getUniqueName("container"); @@ -39,14 +44,14 @@ describe("Highlevel", () => { blockBlobClient = blobClient.getBlockBlobClient(); }); - afterEach(async function () { + afterEach(async function() { if (!this.currentTest?.isPending()) { await containerClient.delete(); recorder.stop(); } }); - before(async function () { + before(async function() { recorder = record(this, recorderEnvSetup); if (!fs.existsSync(tempFolderPath)) { fs.mkdirSync(tempFolderPath); @@ -58,7 +63,7 @@ describe("Highlevel", () => { recorder.stop(); }); - after(async function () { + after(async function() { recorder = record(this, recorderEnvSetup); fs.unlinkSync(tempFileLarge); fs.unlinkSync(tempFileSmall); @@ -67,7 +72,7 @@ describe("Highlevel", () => { it("put blob with maximum size", async () => { recorder.skip("node", "Temp file - recorder doesn't support saving the file"); - const MB = 1024 * 1024 + const MB = 1024 * 1024; const maxPutBlobSizeLimitInMB = 5000; const tempFile = await createRandomLocalFile(tempFolderPath, maxPutBlobSizeLimitInMB, MB); const inputStream = fs.createReadStream(tempFile); @@ -77,7 +82,7 @@ describe("Highlevel", () => { abortSignal: AbortController.timeout(20 * 1000) // takes too long to upload the file }); } catch (err) { - assert.equal(err.name, 'AbortError'); + assert.equal(err.name, "AbortError"); } }).timeout(timeoutForLargeFileUploadingTest); @@ -99,7 +104,7 @@ describe("Highlevel", () => { assert.ok(downloadedData.equals(uploadedData)); }).timeout(timeoutForLargeFileUploadingTest); - it.only("uploadFile should work with tags", async () => { + it("uploadFile should work with tags", async () => { recorder.skip("node", "Temp file - recorder doesn't support saving the file"); const tags = { @@ -201,7 +206,7 @@ describe("Highlevel", () => { aborter.abort(); } }); - } catch (err) { } + } catch (err) {} assert.ok(eventTriggered); }); @@ -224,20 +229,24 @@ describe("Highlevel", () => { aborter.abort(); } }); - } catch (err) { } + } catch (err) {} assert.ok(eventTriggered); }); it("uploadFile should succeed with blockSize = BLOCK_BLOB_MAX_STAGE_BLOCK_BYTES", async () => { recorder.skip("node", "Temp file - recorder doesn't support saving the file"); - const tempFile = await createRandomLocalFile(tempFolderPath, BLOCK_BLOB_MAX_STAGE_BLOCK_BYTES / (1024 * 1024) + 1, 1024 * 1024); + const tempFile = await createRandomLocalFile( + tempFolderPath, + BLOCK_BLOB_MAX_STAGE_BLOCK_BYTES / (1024 * 1024) + 1, + 1024 * 1024 + ); try { await blockBlobClient.uploadFile(tempFile, { blockSize: BLOCK_BLOB_MAX_STAGE_BLOCK_BYTES, abortSignal: AbortController.timeout(20 * 1000) // takes too long to upload the file }); } catch (err) { - assert.equal(err.name, 'AbortError'); + assert.equal(err.name, "AbortError"); } fs.unlinkSync(tempFile); @@ -280,7 +289,7 @@ describe("Highlevel", () => { fs.unlinkSync(downloadFilePath); }); - it.only("uploadStream should work with tags", async () => { + it("uploadStream should work with tags", async () => { recorder.skip("node", "Temp file - recorder doesn't support saving the file"); const buf = Buffer.from([0x62, 0x75, 0x66, 0x66, 0x65, 0x72]); @@ -442,7 +451,7 @@ describe("Highlevel", () => { aborter.abort(); } }); - } catch (err) { } + } catch (err) {} assert.ok(eventTriggered); }); diff --git a/sdk/storage/storage-blob/test/node/sas.spec.ts b/sdk/storage/storage-blob/test/node/sas.spec.ts index b0e6eb4022dd..d8c435f26cf2 100644 --- a/sdk/storage/storage-blob/test/node/sas.spec.ts +++ b/sdk/storage/storage-blob/test/node/sas.spec.ts @@ -324,7 +324,7 @@ describe("Shared Access Signature (SAS) generation Node.js only", () => { await containerClient.delete(); }); - it.only("generateBlobSASQueryParameters should work for blob tags", async () => { + it("generateBlobSASQueryParameters should work for blob tags", async () => { const now = recorder.newDate("now"); now.setMinutes(now.getMinutes() - 5); // Skip clock skew with server @@ -862,8 +862,10 @@ describe("Shared Access Signature (SAS) generation Node.js only", () => { await containerClient.delete(); }); - it("generateAccountSASQueryParameters should work for blob version delete", async function () { - if (isBlobVersioningDisabled()) { this.skip(); } + it("generateAccountSASQueryParameters should work for blob version delete", async function() { + if (isBlobVersioningDisabled()) { + this.skip(); + } // create versions const containerName = recorder.getUniqueName("container"); @@ -906,8 +908,10 @@ describe("Shared Access Signature (SAS) generation Node.js only", () => { await containerClientwithSAS.delete(); }); - it("generateBlobSASQueryParameters should work for blob version delete", async function () { - if (isBlobVersioningDisabled()) { this.skip(); } + it("generateBlobSASQueryParameters should work for blob version delete", async function() { + if (isBlobVersioningDisabled()) { + this.skip(); + } // create versions const containerName = recorder.getUniqueName("container"); @@ -939,7 +943,7 @@ describe("Shared Access Signature (SAS) generation Node.js only", () => { ipRange: { start: "0.0.0.0", end: "255.255.255.255" }, permissions: BlobSASPermissions.parse("racwdx"), protocol: SASProtocol.HttpsAndHttp, - versionId: uploadRes.versionId, + versionId: uploadRes.versionId }, sharedKeyCredential as StorageSharedKeyCredential ); @@ -953,15 +957,17 @@ describe("Shared Access Signature (SAS) generation Node.js only", () => { }); // TODO: prepare ACCOUNT_TOKEN for the test account - it.skip("GenerateUserDelegationSAS should work for blob version delete", async function () { - if (isBlobVersioningDisabled()) { this.skip(); } + it.skip("GenerateUserDelegationSAS should work for blob version delete", async function() { + if (isBlobVersioningDisabled()) { + this.skip(); + } // Try to get blobServiceClient object with TokenCredential // when ACCOUNT_TOKEN environment variable is set let blobServiceClientWithToken: BlobServiceClient | undefined; try { blobServiceClientWithToken = getTokenBSU(); - } catch { } + } catch {} // Requires bearer token for this case which cannot be generated in the runtime // Make sure this case passed in sanity test @@ -1001,7 +1007,7 @@ describe("Shared Access Signature (SAS) generation Node.js only", () => { ipRange: { start: "0.0.0.0", end: "255.255.255.255" }, permissions: BlobSASPermissions.parse("racwdx"), protocol: SASProtocol.HttpsAndHttp, - versionId: uploadRes.versionId, + versionId: uploadRes.versionId }, userDelegationKey, accountName