UNPKG

botbuilder-azure

Version:

Azure extensions for Microsoft BotBuilder.

254 lines 10.7 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BlobStorage = void 0; /** * @module botbuilder-azure */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ const storage_blob_1 = require("@azure/storage-blob"); const querystring_1 = require("querystring"); const consumers_1 = __importDefault(require("stream/consumers")); /** * @private */ const ContainerNameCheck = new RegExp('^[a-z0-9](?!.*--)[a-z0-9-]{1,61}[a-z0-9]$'); /** * @private */ // tslint:disable-next-line:max-line-length typedef align no-shadowed-variable const ResolvePromisesSerial = (values, promise) => values .map((value) => () => promise(value)) .reduce((promise, func) => promise.then((result) => func().then(Array.prototype.concat.bind(result))), Promise.resolve([])); /** * @private */ // tslint:disable-next-line: typedef align const ResolvePromisesParallel = (values, promise) => Promise.all(values.map(promise)); /** * @private * Internal dictionary with the containers where entities will be stored. */ const checkedCollections = {}; /** * Middleware that implements a BlobStorage based storage provider for a bot. * * @remarks * The BlobStorage implements its storage using a single Azure Storage Blob Container. Each entity * is serialized into a JSON string and stored in an individual text blob. Each blob * is named after the key which is encoded and ensure it conforms a valid blob name. * * @deprecated This class is deprecated in favor of [BlobsStorage](xref:botbuilder-azure-blobs.BlobsStorage) */ class BlobStorage { /** * Creates a new BlobStorage instance. * * @param settings Settings for configuring an instance of BlobStorage. */ constructor(settings) { if (!settings) { throw new Error('The settings parameter is required.'); } if (!settings.containerName) { throw new Error('The containerName is required.'); } if (!this.checkContainerName(settings.containerName)) { throw new Error('Invalid container name.'); } if (!settings.storageAccountOrConnectionString) { throw new Error('The storageAccountOrConnectionString is required.'); } this.settings = Object.assign({}, settings); const pipeline = (0, storage_blob_1.newPipeline)(new storage_blob_1.AnonymousCredential(), { retryOptions: { retryPolicyType: storage_blob_1.StorageRetryPolicyType.FIXED, maxTries: 5, retryDelayInMs: 500, }, // Retry options }); this.containerClient = new storage_blob_1.ContainerClient(this.settings.storageAccountOrConnectionString, this.settings.containerName, pipeline.options); this.useEmulator = settings.storageAccountOrConnectionString === 'UseDevelopmentStorage=true;'; } /** * Retrieve entities from the configured blob container. * * @param keys An array of entity keys. * @returns The read items. */ read(keys) { if (!keys) { throw new Error('Please provide at least one key to read from storage.'); } const sanitizedKeys = keys.filter((k) => k).map((key) => this.sanitizeKey(key)); return this.ensureContainerExists() .then(() => { return new Promise((resolve, reject) => { Promise.all(sanitizedKeys.map((key) => __awaiter(this, void 0, void 0, function* () { const blob = this.containerClient.getBlobClient(key); if (yield blob.exists()) { const result = yield blob.download(); const { etag: eTag, readableStreamBody } = result; if (!readableStreamBody) { return { document: {} }; } const document = (yield consumers_1.default.json(readableStreamBody)); document.document.eTag = eTag; return document; } else { // If blob does not exist, return an empty DocumentStoreItem. return { document: {} }; } }))) .then((items) => { if (items !== null && items.length > 0) { const storeItems = {}; items .filter((x) => x) .forEach((item) => { storeItems[item.realId] = item.document; }); resolve(storeItems); } }) .catch((error) => { reject(error); }); }); }) .catch((error) => { throw error; }); } /** * Store a new entity in the configured blob container. * * @param changes The changes to write to storage. * @returns A promise representing the asynchronous operation. */ write(changes) { if (!changes) { throw new Error('Please provide a StoreItems with changes to persist.'); } return this.ensureContainerExists().then(() => { const blobs = Object.keys(changes).map((key) => { const documentChange = { id: this.sanitizeKey(key), realId: key, document: changes[key], }; const payload = JSON.stringify(documentChange); const options = { conditions: changes[key].eTag === '*' ? {} : { ifMatch: changes[key].eTag }, }; return { id: documentChange.id, data: payload, options: options, }; }); // A block blob can be uploaded using a single PUT operation or divided into multiple PUT block operations // depending on the payload's size. The default maximum size for a single blob upload is 128MB. // An 'InvalidBlockList' error is commonly caused due to concurrently uploading an object larger than 128MB in size. const promise = (blob) => { const blockBlobClient = this.containerClient.getBlockBlobClient(blob.id); const uploadBlobResponse = blockBlobClient.upload(blob.data, blob.data.length, blob.options); return uploadBlobResponse; }; // if the blob service client is using the storage emulator, all write operations must be performed in a sequential mode // because of the storage emulator internal implementation, that includes a SQL LocalDb // that crash with a deadlock when performing parallel uploads. // This behavior does not occur when using an Azure Blob Storage account. const results = this.useEmulator ? ResolvePromisesSerial(blobs, promise) : ResolvePromisesParallel(blobs, promise); return results .then(() => { return; }) .catch((error) => { throw error; }); }); } /** * Delete entity blobs from the configured container. * * @param keys An array of entity keys. * @returns A promise representing the asynchronous operation. */ delete(keys) { if (!keys) { throw new Error('Please provide at least one key to delete from storage.'); } const sanitizedKeys = keys.filter((k) => k).map((key) => this.sanitizeKey(key)); return this.ensureContainerExists() .then(() => { return Promise.all(sanitizedKeys.map((key) => __awaiter(this, void 0, void 0, function* () { const blockBlobClient = this.containerClient.getBlockBlobClient(key); return yield blockBlobClient.deleteIfExists(); }))); }) .then(() => { return; }) .catch((error) => { throw error; }); } /** * Get a blob name validated representation of an entity to be used as a key. * * @param key The key used to identify the entity. * @returns An appropriately escaped version of the key. */ sanitizeKey(key) { if (!key || key.length < 1) { throw new Error('Please provide a not empty key.'); } const segments = key.split('/').filter((x) => x); const base = segments.splice(0, 1)[0]; // The number of path segments comprising the blob name cannot exceed 254 const validKey = segments.reduce((acc, curr, index) => [acc, curr].join(index < 255 ? '/' : ''), base); // Reserved URL characters must be escaped. return (0, querystring_1.escape)(validKey).substr(0, 1024); } /** * Check if a container name is valid. * * @param container String representing the container name to validate. * @returns A boolean value that indicates whether or not the name is valid. */ checkContainerName(container) { return ContainerNameCheck.test(container); } /** * Delay Container creation if it does not exist. * * @returns A promise representing the asynchronous operation. */ ensureContainerExists() { const key = this.settings.containerName; if (!checkedCollections[key]) { checkedCollections[key] = this.containerClient.createIfNotExists(); } return checkedCollections[key]; } } exports.BlobStorage = BlobStorage; //# sourceMappingURL=blobStorage.js.map