diff --git a/package-lock.json b/package-lock.json index b3cfd585..e47f444a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,6 +73,31 @@ "adal-node": "^0.1.28" } }, + "@azure/storage-blob": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-10.3.0.tgz", + "integrity": "sha512-KZbJ3q8RpAdeIB5Em1lgXkiq7Mll9bSHHbHavOFMepkkF7HQa3Sez9FdkAVIkVVWK5YoBlshBGZ+mtiSQiS9Fw==", + "requires": { + "@azure/ms-rest-js": "1.2.3", + "events": "3.0.0", + "tslib": "^1.9.3" + }, + "dependencies": { + "@azure/ms-rest-js": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-1.2.3.tgz", + "integrity": "sha512-eROQ034b+9v0Hd3wETKi/EwF5pqS3VRAk1Lm8iKVPOP8v30f6Zfzsi420MRfBMsbNCx/mE2N0L65Px7tvcGfVg==", + "requires": { + "axios": "^0.18.0", + "form-data": "^2.3.2", + "tough-cookie": "^2.4.3", + "tslib": "^1.9.2", + "uuid": "^3.2.1", + "xml2js": "^0.4.19" + } + } + } + }, "@babel/code-frame": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", @@ -1759,7 +1784,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "optional": true, "requires": { "kind-of": "^3.0.2", "longest": "^1.0.1", @@ -1932,7 +1956,6 @@ "version": "4.10.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", - "optional": true, "requires": { "bn.js": "^4.0.0", "inherits": "^2.0.1", @@ -2402,8 +2425,7 @@ "bn.js": { "version": "4.11.8", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", - "optional": true + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" }, "body-parser": { "version": "1.19.0", @@ -2508,8 +2530,7 @@ "brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", - "optional": true + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" }, "browser-process-hrtime": { "version": "0.1.3", @@ -2538,7 +2559,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "optional": true, "requires": { "buffer-xor": "^1.0.3", "cipher-base": "^1.0.0", @@ -2575,7 +2595,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", - "optional": true, "requires": { "bn.js": "^4.1.0", "randombytes": "^2.0.1" @@ -2672,8 +2691,7 @@ "buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", - "optional": true + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" }, "builtin-modules": { "version": "1.1.1", @@ -2855,7 +2873,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "optional": true, "requires": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -3233,7 +3250,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "optional": true, "requires": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", @@ -3246,7 +3262,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "optional": true, "requires": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", @@ -3705,7 +3720,6 @@ "version": "6.4.1", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==", - "optional": true, "requires": { "bn.js": "^4.4.0", "brorand": "^1.0.1", @@ -3767,7 +3781,6 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", - "optional": true, "requires": { "prr": "~1.0.1" } @@ -4147,7 +4160,6 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "optional": true, "requires": { "d": "1", "es5-ext": "~0.10.14" @@ -4156,14 +4168,12 @@ "events": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", - "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==", - "optional": true + "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==" }, "evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "optional": true, "requires": { "md5.js": "^1.3.4", "safe-buffer": "^5.1.1" @@ -5461,7 +5471,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", - "optional": true, "requires": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -5471,7 +5480,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "optional": true, "requires": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" @@ -5481,7 +5489,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "optional": true, "requires": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", @@ -5800,8 +5807,7 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "optional": true + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, "is-fullwidth-code-point": { "version": "1.0.0", @@ -7441,8 +7447,7 @@ "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "optional": true + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" }, "loose-envify": { "version": "1.4.0", @@ -7533,7 +7538,6 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "optional": true, "requires": { "hash-base": "^3.0.0", "inherits": "^2.0.1", @@ -7559,7 +7563,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", - "optional": true, "requires": { "errno": "^0.1.3", "readable-stream": "^2.0.1" @@ -7650,14 +7653,12 @@ "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "optional": true + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "minimalistic-crypto-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", - "optional": true + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" }, "minimatch": { "version": "3.0.4", @@ -8255,7 +8256,6 @@ "version": "5.1.4", "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz", "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==", - "optional": true, "requires": { "asn1.js": "^4.0.0", "browserify-aes": "^1.0.0", @@ -8364,7 +8364,6 @@ "version": "3.0.17", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", - "optional": true, "requires": { "create-hash": "^1.1.2", "create-hmac": "^1.1.4", @@ -8567,8 +8566,7 @@ "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", - "optional": true + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" }, "pseudomap": { "version": "1.0.2", @@ -8635,7 +8633,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "optional": true, "requires": { "safe-buffer": "^5.1.0" } @@ -9048,7 +9045,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "optional": true, "requires": { "hash-base": "^3.0.0", "inherits": "^2.0.1" @@ -9438,7 +9434,6 @@ "version": "2.4.11", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "optional": true, "requires": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -9634,8 +9629,7 @@ "source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", - "optional": true + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" }, "source-map": { "version": "0.7.3", @@ -10192,8 +10186,7 @@ "tapable": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.9.tgz", - "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A==", - "optional": true + "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A==" }, "tar-stream": { "version": "1.6.2", @@ -11043,7 +11036,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz", "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==", - "optional": true, "requires": { "source-list-map": "^2.0.0", "source-map": "~0.6.1" @@ -11052,8 +11044,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, diff --git a/package.json b/package.json index fa52a26c..80c7824c 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@azure/arm-appservice": "^5.7.0", "@azure/arm-resources": "^1.0.1", "@azure/ms-rest-nodeauth": "^1.0.1", + "@azure/storage-blob": "^10.3.0", "axios": "^0.18.0", "js-yaml": "^3.13.1", "jsonpath": "^1.0.1", diff --git a/src/services/azureBlobStorageService.test.ts b/src/services/azureBlobStorageService.test.ts new file mode 100644 index 00000000..9302e146 --- /dev/null +++ b/src/services/azureBlobStorageService.test.ts @@ -0,0 +1,92 @@ +import { MockFactory } from "../test/mockFactory" +import mockFs from "mock-fs"; + +jest.mock("@azure/storage-blob"); +import { BlockBlobURL, ContainerURL, ServiceURL, Aborter, uploadFileToBlockBlob } from "@azure/storage-blob"; +import { AzureBlobStorageService } from "./azureBlobStorageService"; + +describe("Azure Blob Storage Service", () => { + + const filePath = "deployments/deployment.zip"; + const fileName = "deployment.zip"; + const fileContents = "contents"; + const containerName = "DEPLOYMENTS"; + + const containers = MockFactory.createTestAzureContainers(); + const sls = MockFactory.createTestServerless(); + const options = MockFactory.createTestServerlessOptions(); + const blockBlobUrl = MockFactory.createTestBlockBlobUrl(containerName, filePath); + + let service: AzureBlobStorageService; + + beforeAll(() => { + BlockBlobURL.fromContainerURL = jest.fn(() => blockBlobUrl) as any; + }); + + beforeAll(() => { + mockFs({ + "deployments/deployment.zip": fileContents + }) + }); + + afterAll(() => { + mockFs.restore(); + }); + + beforeEach(() => { + service = new AzureBlobStorageService(sls, options); + }); + + it("should upload a file", async () => { + uploadFileToBlockBlob.prototype = jest.fn(); + ContainerURL.fromServiceURL = jest.fn((serviceUrl, containerName) => (containerName as any)); + await service.uploadFile(filePath, containerName); + expect(uploadFileToBlockBlob).toBeCalledWith( + Aborter.none, + filePath, + blockBlobUrl + ); + }); + + it("should delete a file", async () => { + ContainerURL.fromServiceURL = jest.fn((serviceUrl, containerName) => (containerName as any)); + await service.deleteFile(containerName, fileName); + expect(blockBlobUrl.delete).toBeCalledWith(Aborter.none) + }); + + it("should list files of container", async () => { + const blobs = MockFactory.createTestAzureBlobItems(); + ContainerURL.prototype.listBlobFlatSegment = jest.fn(() => Promise.resolve(blobs)) as any; + ContainerURL.fromServiceURL = jest.fn(() => new ContainerURL(null, null)); + const files = await service.listFiles(containerName); + expect(files.length).toEqual(5); + + const otherFiles = await service.listFiles(containerName, "jpeg"); + expect(otherFiles.length).toEqual(0); + }); + + it("should list containers", async () => { + ServiceURL.prototype.listContainersSegment = jest.fn(() => Promise.resolve(containers)); + const containerList = await service.listContainers(); + expect(containerList).toEqual( + containers.containerItems.map((container) => container.name)); + }); + + it("should create a container", async () => { + ContainerURL.fromServiceURL = jest.fn(() => new ContainerURL(null, null)); + ContainerURL.prototype.create = jest.fn(() => Promise.resolve({ statusCode: 201 })) as any; + const newContainerName = "newContainer"; + await service.createContainer(newContainerName); + expect(ContainerURL.fromServiceURL).toBeCalledWith(expect.anything(), newContainerName); + expect(ContainerURL.prototype.create).toBeCalledWith(Aborter.none); + }); + + it("should delete a container", async () => { + const containerToDelete = "delete container"; + ContainerURL.fromServiceURL = jest.fn(() => new ContainerURL(null, null)); + ContainerURL.prototype.delete = jest.fn(() => Promise.resolve({ statusCode: 204 })) as any; + await service.deleteContainer(containerToDelete); + expect(ContainerURL.fromServiceURL).toBeCalledWith(expect.anything(), containerToDelete); + expect(ContainerURL.prototype.delete).toBeCalledWith(Aborter.none); + }); +}); diff --git a/src/services/azureBlobStorageService.ts b/src/services/azureBlobStorageService.ts new file mode 100644 index 00000000..9946cea5 --- /dev/null +++ b/src/services/azureBlobStorageService.ts @@ -0,0 +1,152 @@ +import { Aborter, BlockBlobURL, ContainerURL, ServiceURL, StorageURL, uploadFileToBlockBlob } from "@azure/storage-blob"; +import Serverless from "serverless"; +import { BaseService } from "./baseService"; +import { Guard } from "../shared/guard"; + +/** + * Wrapper for operations on Azure Blob Storage account + */ +export class AzureBlobStorageService extends BaseService { + + /** + * Account URL for Azure Blob Storage account. Depends on `storageAccountName` being set in baseService + */ + private accountUrl: string; + + public constructor(serverless: Serverless, options: Serverless.Options) { + super(serverless, options); + this.accountUrl = `https://${this.storageAccountName}.blob.core.windows.net`; + } + + /** + * Upload a file to Azure Blob Storage + * @param path Path of file to upload + * @param containerName Name of container in Azure Blob storage for upload + * @param blobName Name of blob file created as a result of upload + */ + public async uploadFile(path: string, containerName: string, blobName?: string) { + Guard.empty(path); + Guard.empty(containerName); + // Use specified blob name or replace `/` in path with `-` + const name = blobName || path.replace(/^.*[\\\/]/, "-"); + uploadFileToBlockBlob(Aborter.none, path, this.getBlockBlobURL(containerName, name)); + }; + + /** + * Delete a blob from Azure Blob Storage + * @param containerName Name of container containing blob + * @param blobName Blob to delete + */ + public async deleteFile(containerName: string, blobName: string): Promise { + Guard.empty(containerName); + Guard.empty(blobName); + const blockBlobUrl = await this.getBlockBlobURL(containerName, blobName) + await blockBlobUrl.delete(Aborter.none); + } + + /** + * Lists files in container + * @param ext - Extension of files to filter on when retrieving files + * from container + */ + public async listFiles(containerName: string, ext?: string): Promise { + Guard.empty(containerName, "containerName"); + const result: string[] = []; + let marker; + const containerURL = this.getContainerURL(containerName); + do { + const listBlobsResponse = await containerURL.listBlobFlatSegment( + Aborter.none, + marker, + ); + marker = listBlobsResponse.nextMarker; + for (const blob of listBlobsResponse.segment.blobItems) { + if ((ext && blob.name.endsWith(ext)) || !ext) { + result.push(blob.name); + } + } + } while (marker); + + return result; + } + + /** + * Lists the containers within the Azure Blob Storage account + */ + public async listContainers() { + const result: string[] = []; + let marker; + do { + const listContainersResponse = await this.getServiceURL().listContainersSegment( + Aborter.none, + marker, + ); + marker = listContainersResponse.nextMarker; + for (const container of listContainersResponse.containerItems) { + result.push(container.name); + } + } while (marker); + + return result; + } + + /** + * Creates container specified in Azure Cloud Storage options + * @param containerName - Name of container to create + */ + public async createContainer(containerName: string): Promise { + Guard.empty(containerName); + const containerURL = this.getContainerURL(containerName); + await containerURL.create(Aborter.none); + } + + /** + * Delete a container from Azure Blob Storage Account + * @param containerName Name of container to delete + */ + public async deleteContainer(containerName: string): Promise { + Guard.empty(containerName); + const containerUrl = await this.getContainerURL(containerName) + await containerUrl.delete(Aborter.none); + } + + /** + * Get ServiceURL object for Azure Blob Storage Account + */ + private getServiceURL(): ServiceURL { + const pipeline = StorageURL.newPipeline(this.credentials); + const accountUrl = this.accountUrl; + const serviceUrl = new ServiceURL( + accountUrl, + pipeline, + ); + return serviceUrl; + } + + /** + * Get a ContainerURL object to perform operations on Azure Blob Storage container + * @param containerName Name of container + * @param serviceURL Previously created ServiceURL object (will create if undefined) + */ + private getContainerURL(containerName: string): ContainerURL { + Guard.empty(containerName); + return ContainerURL.fromServiceURL( + this.getServiceURL(), + containerName + ); + } + + /** + * Get a BlockBlobURL object to perform operations on Azure Blob Storage Blob + * @param containerName Name of container containing blob + * @param blobName Name of blob + */ + private getBlockBlobURL(containerName: string, blobName: string): BlockBlobURL { + Guard.empty(containerName); + Guard.empty(blobName); + return BlockBlobURL.fromContainerURL( + this.getContainerURL(containerName), + blobName, + ); + } +} diff --git a/src/services/baseService.ts b/src/services/baseService.ts index 44b42310..c45a86a8 100644 --- a/src/services/baseService.ts +++ b/src/services/baseService.ts @@ -12,6 +12,8 @@ export abstract class BaseService { protected subscriptionId: string; protected resourceGroup: string; protected deploymentName: string; + protected deploymentContainerName: string; + protected storageAccountName: string; protected config: ServerlessAzureConfig; protected constructor( diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts index d6fa7a80..7962cd74 100644 --- a/src/test/mockFactory.ts +++ b/src/test/mockFactory.ts @@ -1,20 +1,14 @@ -import { ApiContract, ApiManagementServiceResource } from "@azure/arm-apimanagement/esm/models"; -import { Site, FunctionEnvelope } from "@azure/arm-appservice/esm/models"; +import { DeploymentsListByResourceGroupResponse } from "@azure/arm-resources/esm/models"; import { HttpHeaders, HttpOperationResponse, HttpResponse, WebResource } from "@azure/ms-rest-js"; -import { AuthResponse, LinkedSubscription, TokenCredentialsBase } from "@azure/ms-rest-nodeauth"; -import { TokenClientCredentials, TokenResponse } from "@azure/ms-rest-nodeauth/dist/lib/credentials/tokenClientCredentials"; -import { AxiosRequestConfig, AxiosResponse } from "axios"; +import { LinkedSubscription, TokenCredentialsBase } from "@azure/ms-rest-nodeauth"; +import { TokenResponse } from "@azure/ms-rest-nodeauth/dist/lib/credentials/tokenClientCredentials"; +import { ServiceListContainersSegmentResponse } from "@azure/storage-blob/typings/lib/generated/lib/models"; +import { AxiosResponse } from "axios"; import yaml from "js-yaml"; import Serverless from "serverless"; import Service from "serverless/classes/Service"; import Utils from "serverless/classes/Utils"; import PluginManager from "serverless/lib/classes/PluginManager"; -import { ServerlessAzureConfig } from "../models/serverless"; -import { AzureServiceProvider, ServicePrincipalEnvVariables } from "../models/azureProvider" -import { Logger } from "../models/generic"; -import { ApiCorsPolicy, ApiManagementConfig } from "../models/apiManagement"; -import { DeploymentsListByResourceGroupResponse } from "@azure/arm-resources/esm/models"; -import { ArmResourceTemplate } from "../models/armTemplates"; function getAttribute(object: any, prop: string, defaultValue: any): any { if (object && object[prop]) { @@ -183,6 +177,39 @@ export class MockFactory { return Promise.resolve(response); } + public static createTestAzureContainers(count: number = 5): ServiceListContainersSegmentResponse { + const result = []; + for (let i = 0; i < count; i++) { + result.push({ + name: `container${i}`, + blobs: MockFactory.createTestAzureBlobItems(i), + }); + } + return { containerItems: result } as ServiceListContainersSegmentResponse; + } + + public static createTestBlockBlobUrl(containerName: string, blobName: string) { + return { + containerName, + blobName, + delete: jest.fn(), + } + } + + public static createTestAzureBlobItems(id: number = 1, count: number = 5) { + const result = []; + for (let i = 0; i < count; i++) { + result.push(MockFactory.createTestAzureBlobItem(id, i)); + } + return { segment: { blobItems: result } }; + } + + public static createTestAzureBlobItem(id: number = 1, index: number = 1, ext: string = ".zip") { + return { + name: `blob-${id}-${index}${ext}` + } + } + public static createTestAzureClientResponse(responseJson: T, statusCode: number = 200): Promise { const response: HttpOperationResponse = { request: new WebResource(),